import { Request, Response } from 'express'; import incidentService from '../services/incident.service'; import logger from '../utils/logger'; import { AppError } from '../middleware/error.middleware'; import { AppRole } from '../middleware/rbac.middleware'; import { permissionService } from '../services/permission.service'; import { CreateEinsatzSchema, UpdateEinsatzSchema, AssignPersonnelSchema, AssignVehicleSchema, IncidentFiltersSchema, } from '../models/incident.model'; // Extend Request type to carry the resolved role (set by requirePermission) interface AuthenticatedRequest extends Request { userRole?: AppRole; } class IncidentController { // ------------------------------------------------------------------------- // GET /api/incidents // ------------------------------------------------------------------------- async listIncidents(req: AuthenticatedRequest, res: Response): Promise { try { const parseResult = IncidentFiltersSchema.safeParse(req.query); if (!parseResult.success) { res.status(400).json({ success: false, message: 'Ungültige Filter-Parameter', errors: parseResult.error.issues, }); return; } const { items, total } = await incidentService.getAllIncidents(parseResult.data); res.status(200).json({ success: true, data: { items, total, limit: parseResult.data.limit, offset: parseResult.data.offset, }, }); } catch (error) { logger.error('List incidents error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden der Einsätze' }); } } // ------------------------------------------------------------------------- // GET /api/incidents/stats // ------------------------------------------------------------------------- async getStats(req: Request, res: Response): Promise { try { const year = req.query.year ? parseInt(req.query.year as string, 10) : undefined; if (year !== undefined && (isNaN(year) || year < 2000 || year > 2100)) { res.status(400).json({ success: false, message: 'Ungültiges Jahr' }); return; } const stats = await incidentService.getIncidentStats(year); res.status(200).json({ success: true, data: stats }); } catch (error) { logger.error('Get incident stats error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Laden der Statistik' }); } } // ------------------------------------------------------------------------- // GET /api/incidents/:id // ------------------------------------------------------------------------- async getIncident(req: AuthenticatedRequest, res: Response): Promise { try { const { id } = req.params as Record; // UUID validation if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) { res.status(400).json({ success: false, message: 'Ungültige Einsatz-ID' }); return; } const incident = await incidentService.getIncidentById(id); if (!incident) { throw new AppError('Einsatz nicht gefunden', 404); } // Role-based redaction: check einsaetze:view_reports permission const groups: string[] = req.user?.groups ?? []; const canReadBerichtText = groups.includes('dashboard_admin') || permissionService.hasPermission(groups, 'einsaetze:view_reports'); const responseData = { ...incident, bericht_text: canReadBerichtText ? incident.bericht_text : undefined, }; res.status(200).json({ success: true, data: responseData }); } catch (error) { if (error instanceof AppError) { res.status(error.statusCode).json({ success: false, message: error.message }); return; } logger.error('Get incident error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Laden des Einsatzes' }); } } // ------------------------------------------------------------------------- // POST /api/incidents // ------------------------------------------------------------------------- async createIncident(req: AuthenticatedRequest, res: Response): Promise { try { if (!req.user) { res.status(401).json({ success: false, message: 'Nicht authentifiziert' }); return; } const parseResult = CreateEinsatzSchema.safeParse(req.body); if (!parseResult.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parseResult.error.issues, }); return; } const einsatz = await incidentService.createIncident(parseResult.data, req.user.id); logger.info('Incident created via API', { einsatzId: einsatz.id, einsatz_nr: einsatz.einsatz_nr, createdBy: req.user.id, }); res.status(201).json({ success: true, data: einsatz }); } catch (error) { logger.error('Create incident error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Erstellen des Einsatzes' }); } } // ------------------------------------------------------------------------- // PATCH /api/incidents/:id // ------------------------------------------------------------------------- async updateIncident(req: AuthenticatedRequest, res: Response): Promise { try { if (!req.user) { res.status(401).json({ success: false, message: 'Nicht authentifiziert' }); return; } const { id } = req.params as Record; const parseResult = UpdateEinsatzSchema.safeParse(req.body); if (!parseResult.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parseResult.error.issues, }); return; } const einsatz = await incidentService.updateIncident(id, parseResult.data, req.user.id); res.status(200).json({ success: true, data: einsatz }); } catch (error) { if (error instanceof Error && error.message === 'Incident not found') { res.status(404).json({ success: false, message: 'Einsatz nicht gefunden' }); return; } logger.error('Update incident error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Einsatzes' }); } } // ------------------------------------------------------------------------- // DELETE /api/incidents/:id (soft delete) // ------------------------------------------------------------------------- async deleteIncident(req: AuthenticatedRequest, res: Response): Promise { try { if (!req.user) { res.status(401).json({ success: false, message: 'Nicht authentifiziert' }); return; } const { id } = req.params as Record; await incidentService.deleteIncident(id, req.user.id); res.status(200).json({ success: true, message: 'Einsatz archiviert' }); } catch (error) { if (error instanceof Error && error.message.includes('not found')) { res.status(404).json({ success: false, message: 'Einsatz nicht gefunden' }); return; } logger.error('Delete incident error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Archivieren des Einsatzes' }); } } // ------------------------------------------------------------------------- // POST /api/incidents/:id/personnel // ------------------------------------------------------------------------- async assignPersonnel(req: AuthenticatedRequest, res: Response): Promise { try { const { id } = req.params as Record; const parseResult = AssignPersonnelSchema.safeParse(req.body); if (!parseResult.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parseResult.error.issues, }); return; } await incidentService.assignPersonnel(id, parseResult.data); res.status(200).json({ success: true, message: 'Person zugewiesen' }); } catch (error) { logger.error('Assign personnel error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Zuweisen der Person' }); } } // ------------------------------------------------------------------------- // DELETE /api/incidents/:id/personnel/:userId // ------------------------------------------------------------------------- async removePersonnel(req: AuthenticatedRequest, res: Response): Promise { try { const { id, userId } = req.params as Record; await incidentService.removePersonnel(id, userId); res.status(200).json({ success: true, message: 'Person entfernt' }); } catch (error) { if (error instanceof Error && error.message.includes('not found')) { res.status(404).json({ success: false, message: 'Zuweisung nicht gefunden' }); return; } logger.error('Remove personnel error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Entfernen der Person' }); } } // ------------------------------------------------------------------------- // POST /api/incidents/:id/vehicles // ------------------------------------------------------------------------- async assignVehicle(req: AuthenticatedRequest, res: Response): Promise { try { const { id } = req.params as Record; const parseResult = AssignVehicleSchema.safeParse(req.body); if (!parseResult.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parseResult.error.issues, }); return; } await incidentService.assignVehicle(id, parseResult.data); res.status(200).json({ success: true, message: 'Fahrzeug zugewiesen' }); } catch (error) { logger.error('Assign vehicle error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Zuweisen des Fahrzeugs' }); } } // ------------------------------------------------------------------------- // DELETE /api/incidents/:id/vehicles/:fahrzeugId // ------------------------------------------------------------------------- async removeVehicle(req: AuthenticatedRequest, res: Response): Promise { try { const { id, fahrzeugId } = req.params as Record; await incidentService.removeVehicle(id, fahrzeugId); res.status(200).json({ success: true, message: 'Fahrzeug entfernt' }); } catch (error) { if (error instanceof Error && error.message.includes('not found')) { res.status(404).json({ success: false, message: 'Zuweisung nicht gefunden' }); return; } logger.error('Remove vehicle error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Fehler beim Entfernen des Fahrzeugs' }); } } // ------------------------------------------------------------------------- // POST /api/incidents/refresh-stats (admin utility) // ------------------------------------------------------------------------- async refreshStats(_req: Request, res: Response): Promise { try { await incidentService.refreshStatistikView(); res.status(200).json({ success: true, message: 'Statistik-View aktualisiert' }); } catch (error) { logger.error('Refresh stats view error', { error }); res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren der Statistik' }); } } } export default new IncidentController();