import { Request, Response } from 'express'; import ausruestungsanfrageService from '../services/ausruestungsanfrage.service'; import notificationService from '../services/notification.service'; import { permissionService } from '../services/permission.service'; import logger from '../utils/logger'; class AusruestungsanfrageController { // ------------------------------------------------------------------------- // Categories (DB-backed) // ------------------------------------------------------------------------- async getKategorien(_req: Request, res: Response): Promise { try { const kategorien = await ausruestungsanfrageService.getKategorien(); res.status(200).json({ success: true, data: kategorien }); } catch (error) { logger.error('AusruestungsanfrageController.getKategorien error', { error }); res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' }); } } async createKategorie(req: Request, res: Response): Promise { try { const { name } = req.body; if (!name || name.trim().length === 0) { res.status(400).json({ success: false, message: 'Name ist erforderlich' }); return; } const kategorie = await ausruestungsanfrageService.createKategorie(name.trim()); res.status(201).json({ success: true, data: kategorie }); } catch (error: any) { if (error?.constraint === 'ausruestung_kategorien_katalog_name_key') { res.status(409).json({ success: false, message: 'Kategorie existiert bereits' }); return; } logger.error('AusruestungsanfrageController.createKategorie error', { error }); res.status(500).json({ success: false, message: 'Kategorie konnte nicht erstellt werden' }); } } async updateKategorie(req: Request, res: Response): Promise { try { const id = Number(req.params.id); const { name } = req.body; if (!name || name.trim().length === 0) { res.status(400).json({ success: false, message: 'Name ist erforderlich' }); return; } const kategorie = await ausruestungsanfrageService.updateKategorie(id, name.trim()); if (!kategorie) { res.status(404).json({ success: false, message: 'Kategorie nicht gefunden' }); return; } res.status(200).json({ success: true, data: kategorie }); } catch (error) { logger.error('AusruestungsanfrageController.updateKategorie error', { error }); res.status(500).json({ success: false, message: 'Kategorie konnte nicht aktualisiert werden' }); } } async deleteKategorie(req: Request, res: Response): Promise { try { const id = Number(req.params.id); await ausruestungsanfrageService.deleteKategorie(id); res.status(200).json({ success: true, message: 'Kategorie gelöscht' }); } catch (error) { logger.error('AusruestungsanfrageController.deleteKategorie error', { error }); res.status(500).json({ success: false, message: 'Kategorie konnte nicht gelöscht werden' }); } } // ------------------------------------------------------------------------- // Catalog Items // ------------------------------------------------------------------------- async getItems(req: Request, res: Response): Promise { try { const kategorie = req.query.kategorie as string | undefined; const kategorie_id = req.query.kategorie_id ? Number(req.query.kategorie_id) : undefined; const aktiv = req.query.aktiv !== undefined ? req.query.aktiv === 'true' : undefined; const items = await ausruestungsanfrageService.getItems({ kategorie, kategorie_id, aktiv }); res.status(200).json({ success: true, data: items }); } catch (error) { logger.error('AusruestungsanfrageController.getItems error', { error }); res.status(500).json({ success: false, message: 'Artikel konnten nicht geladen werden' }); } } async getItemById(req: Request, res: Response): Promise { try { const id = Number(req.params.id); const item = await ausruestungsanfrageService.getItemById(id); if (!item) { res.status(404).json({ success: false, message: 'Artikel nicht gefunden' }); return; } res.status(200).json({ success: true, data: item }); } catch (error) { logger.error('AusruestungsanfrageController.getItemById error', { error }); res.status(500).json({ success: false, message: 'Artikel konnte nicht geladen werden' }); } } async createItem(req: Request, res: Response): Promise { try { const { bezeichnung } = req.body; if (!bezeichnung || bezeichnung.trim().length === 0) { res.status(400).json({ success: false, message: 'Bezeichnung ist erforderlich' }); return; } const item = await ausruestungsanfrageService.createItem(req.body, req.user!.id); res.status(201).json({ success: true, data: item }); } catch (error) { logger.error('AusruestungsanfrageController.createItem error', { error }); res.status(500).json({ success: false, message: 'Artikel konnte nicht erstellt werden' }); } } async updateItem(req: Request, res: Response): Promise { try { const id = Number(req.params.id); const item = await ausruestungsanfrageService.updateItem(id, req.body, req.user!.id); if (!item) { res.status(404).json({ success: false, message: 'Artikel nicht gefunden' }); return; } res.status(200).json({ success: true, data: item }); } catch (error) { logger.error('AusruestungsanfrageController.updateItem error', { error }); res.status(500).json({ success: false, message: 'Artikel konnte nicht aktualisiert werden' }); } } async deleteItem(req: Request, res: Response): Promise { try { const id = Number(req.params.id); await ausruestungsanfrageService.deleteItem(id); res.status(200).json({ success: true, message: 'Artikel gelöscht' }); } catch (error) { logger.error('AusruestungsanfrageController.deleteItem error', { error }); res.status(500).json({ success: false, message: 'Artikel konnte nicht gelöscht werden' }); } } async getCategories(_req: Request, res: Response): Promise { try { const categories = await ausruestungsanfrageService.getCategories(); res.status(200).json({ success: true, data: categories }); } catch (error) { logger.error('AusruestungsanfrageController.getCategories error', { error }); res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' }); } } // ------------------------------------------------------------------------- // Artikel Eigenschaften (characteristics) // ------------------------------------------------------------------------- async getArtikelEigenschaften(req: Request, res: Response): Promise { try { const artikelId = Number(req.params.id); const eigenschaften = await ausruestungsanfrageService.getArtikelEigenschaften(artikelId); res.status(200).json({ success: true, data: eigenschaften }); } catch (error) { logger.error('AusruestungsanfrageController.getArtikelEigenschaften error', { error }); res.status(500).json({ success: false, message: 'Eigenschaften konnten nicht geladen werden' }); } } async upsertArtikelEigenschaft(req: Request, res: Response): Promise { try { const artikelId = Number(req.params.id); const { name, typ, optionen, pflicht, sort_order, eigenschaft_id } = req.body; if (!name || name.trim().length === 0) { res.status(400).json({ success: false, message: 'Name ist erforderlich' }); return; } if (!typ || !['options', 'freitext'].includes(typ)) { res.status(400).json({ success: false, message: 'Typ muss "options" oder "freitext" sein' }); return; } const eigenschaft = await ausruestungsanfrageService.upsertArtikelEigenschaft(artikelId, { id: eigenschaft_id, name: name.trim(), typ, optionen: optionen || undefined, pflicht: pflicht ?? false, sort_order: sort_order ?? 0, }); res.status(eigenschaft_id ? 200 : 201).json({ success: true, data: eigenschaft }); } catch (error) { logger.error('AusruestungsanfrageController.upsertArtikelEigenschaft error', { error }); res.status(500).json({ success: false, message: 'Eigenschaft konnte nicht gespeichert werden' }); } } async deleteArtikelEigenschaft(req: Request, res: Response): Promise { try { const id = Number(req.params.eigenschaftId); await ausruestungsanfrageService.deleteArtikelEigenschaft(id); res.status(200).json({ success: true, message: 'Eigenschaft gelöscht' }); } catch (error) { logger.error('AusruestungsanfrageController.deleteArtikelEigenschaft error', { error }); res.status(500).json({ success: false, message: 'Eigenschaft konnte nicht gelöscht werden' }); } } // ------------------------------------------------------------------------- // Requests // ------------------------------------------------------------------------- async getRequests(req: Request, res: Response): Promise { try { const status = req.query.status as string | undefined; const anfrager_id = req.query.anfrager_id as string | undefined; const requests = await ausruestungsanfrageService.getRequests({ status, anfrager_id }); res.status(200).json({ success: true, data: requests }); } catch (error) { logger.error('AusruestungsanfrageController.getRequests error', { error }); res.status(500).json({ success: false, message: 'Anfragen konnten nicht geladen werden' }); } } async getMyRequests(req: Request, res: Response): Promise { try { const requests = await ausruestungsanfrageService.getMyRequests(req.user!.id); res.status(200).json({ success: true, data: requests }); } catch (error) { logger.error('AusruestungsanfrageController.getMyRequests error', { error }); res.status(500).json({ success: false, message: 'Anfragen konnten nicht geladen werden' }); } } async getRequestById(req: Request, res: Response): Promise { try { const id = Number(req.params.id); const request = await ausruestungsanfrageService.getRequestById(id); if (!request) { res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' }); return; } res.status(200).json({ success: true, data: request }); } catch (error) { logger.error('AusruestungsanfrageController.getRequestById error', { error }); res.status(500).json({ success: false, message: 'Anfrage konnte nicht geladen werden' }); } } async createRequest(req: Request, res: Response): Promise { try { const { items, notizen, bezeichnung, fuer_benutzer_id } = req.body as { items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[]; notizen?: string; bezeichnung?: string; fuer_benutzer_id?: string; }; if (!items || items.length === 0) { res.status(400).json({ success: false, message: 'Mindestens eine Position ist erforderlich' }); return; } for (const item of items) { if (!item.bezeichnung || item.bezeichnung.trim().length === 0) { res.status(400).json({ success: false, message: 'Bezeichnung ist für alle Positionen erforderlich' }); return; } if (!item.menge || item.menge < 1) { res.status(400).json({ success: false, message: 'Menge muss mindestens 1 sein' }); return; } } // Determine anfrager: self or on behalf of another user let anfragerId = req.user!.id; if (fuer_benutzer_id && fuer_benutzer_id !== req.user!.id) { const groups = req.user?.groups ?? []; const canOrderForUser = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'ausruestungsanfrage:order_for_user'); if (!canOrderForUser) { res.status(403).json({ success: false, message: 'Keine Berechtigung für Bestellung im Auftrag' }); return; } anfragerId = fuer_benutzer_id; } const request = await ausruestungsanfrageService.createRequest(anfragerId, items, notizen, bezeichnung); res.status(201).json({ success: true, data: request }); } catch (error) { logger.error('AusruestungsanfrageController.createRequest error', { error }); res.status(500).json({ success: false, message: 'Anfrage konnte nicht erstellt werden' }); } } async updateRequest(req: Request, res: Response): Promise { try { const id = Number(req.params.id); const { bezeichnung, notizen, items } = req.body as { bezeichnung?: string; notizen?: string; items?: { artikel_id?: number; bezeichnung: string; menge: number; notizen?: string; eigenschaften?: { eigenschaft_id: number; wert: string }[] }[]; }; // Validate items if provided if (items) { if (items.length === 0) { res.status(400).json({ success: false, message: 'Mindestens eine Position ist erforderlich' }); return; } for (const item of items) { if (!item.bezeichnung || item.bezeichnung.trim().length === 0) { res.status(400).json({ success: false, message: 'Bezeichnung ist für alle Positionen erforderlich' }); return; } if (!item.menge || item.menge < 1) { res.status(400).json({ success: false, message: 'Menge muss mindestens 1 sein' }); return; } } } const existing = await ausruestungsanfrageService.getRequestById(id); if (!existing) { res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' }); return; } // Check permission: owner + status=offen, OR ausruestungsanfrage:edit const groups = req.user?.groups ?? []; const canEditAny = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'ausruestungsanfrage:edit'); const isOwner = existing.anfrage.anfrager_id === req.user!.id; if (!canEditAny && !(isOwner && existing.anfrage.status === 'offen')) { res.status(403).json({ success: false, message: 'Keine Berechtigung zum Bearbeiten dieser Anfrage' }); return; } const updated = await ausruestungsanfrageService.updateRequest(id, { bezeichnung, notizen, items }); res.status(200).json({ success: true, data: updated }); } catch (error) { logger.error('AusruestungsanfrageController.updateRequest error', { error }); res.status(500).json({ success: false, message: 'Anfrage konnte nicht aktualisiert werden' }); } } async updateRequestStatus(req: Request, res: Response): Promise { try { const id = Number(req.params.id); const { status, admin_notizen } = req.body as { status?: string; admin_notizen?: string; }; if (!status) { res.status(400).json({ success: false, message: 'Status ist erforderlich' }); return; } const validStatuses = ['offen', 'genehmigt', 'abgelehnt', 'bestellt', 'erledigt']; if (!validStatuses.includes(status)) { res.status(400).json({ success: false, message: `Ungültiger Status. Erlaubt: ${validStatuses.join(', ')}` }); return; } // Fetch request to get anfrager_id for notification const existing = await ausruestungsanfrageService.getRequestById(id); if (!existing) { res.status(404).json({ success: false, message: 'Anfrage nicht gefunden' }); return; } const updated = await ausruestungsanfrageService.updateRequestStatus(id, status, admin_notizen, req.user!.id); // Notify requester on status changes if (['genehmigt', 'abgelehnt', 'bestellt', 'erledigt'].includes(status)) { const orderLabel = existing.anfrage.bestell_jahr && existing.anfrage.bestell_nummer ? `${existing.anfrage.bestell_jahr}/${String(existing.anfrage.bestell_nummer).padStart(3, '0')}` : `#${id}`; await notificationService.createNotification({ user_id: existing.anfrage.anfrager_id, typ: 'ausruestung_anfrage', titel: status === 'genehmigt' ? 'Anfrage genehmigt' : status === 'abgelehnt' ? 'Anfrage abgelehnt' : `Anfrage ${status}`, nachricht: `Deine Ausrüstungsanfrage ${orderLabel} wurde ${status === 'genehmigt' ? 'genehmigt' : status === 'abgelehnt' ? 'abgelehnt' : status}.`, schwere: status === 'abgelehnt' ? 'warnung' : 'info', link: '/ausruestungsanfrage', quell_id: String(id), quell_typ: 'ausruestung_anfrage', }); } res.status(200).json({ success: true, data: updated }); } catch (error) { logger.error('AusruestungsanfrageController.updateRequestStatus error', { error }); res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' }); } } async deleteRequest(req: Request, res: Response): Promise { try { const id = Number(req.params.id); await ausruestungsanfrageService.deleteRequest(id); res.status(200).json({ success: true, message: 'Anfrage gelöscht' }); } catch (error) { logger.error('AusruestungsanfrageController.deleteRequest error', { error }); res.status(500).json({ success: false, message: 'Anfrage konnte nicht gelöscht werden' }); } } // ------------------------------------------------------------------------- // Overview // ------------------------------------------------------------------------- async getOverview(_req: Request, res: Response): Promise { try { const overview = await ausruestungsanfrageService.getOverview(); res.status(200).json({ success: true, data: overview }); } catch (error) { logger.error('AusruestungsanfrageController.getOverview error', { error }); res.status(500).json({ success: false, message: 'Übersicht konnte nicht geladen werden' }); } } // ------------------------------------------------------------------------- // Linking // ------------------------------------------------------------------------- async linkToOrder(req: Request, res: Response): Promise { try { const anfrageId = Number(req.params.id); const { bestellung_id } = req.body as { bestellung_id?: number }; if (!bestellung_id) { res.status(400).json({ success: false, message: 'bestellung_id ist erforderlich' }); return; } await ausruestungsanfrageService.linkToOrder(anfrageId, bestellung_id); res.status(200).json({ success: true, message: 'Verknüpfung erstellt' }); } catch (error) { logger.error('AusruestungsanfrageController.linkToOrder error', { error }); res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht erstellt werden' }); } } async unlinkFromOrder(req: Request, res: Response): Promise { try { const anfrageId = Number(req.params.id); const bestellungId = Number(req.params.bestellungId); await ausruestungsanfrageService.unlinkFromOrder(anfrageId, bestellungId); res.status(200).json({ success: true, message: 'Verknüpfung entfernt' }); } catch (error) { logger.error('AusruestungsanfrageController.unlinkFromOrder error', { error }); res.status(500).json({ success: false, message: 'Verknüpfung konnte nicht entfernt werden' }); } } } export default new AusruestungsanfrageController();