Files
dashboard/backend/src/controllers/incident.controller.ts
Matthias Hochmeister 2bb22850f4 rights system
2026-03-23 10:07:53 +01:00

312 lines
12 KiB
TypeScript

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<void> {
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<void> {
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<void> {
try {
const { id } = req.params as Record<string, string>;
// 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<void> {
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<void> {
try {
if (!req.user) {
res.status(401).json({ success: false, message: 'Nicht authentifiziert' });
return;
}
const { id } = req.params as Record<string, string>;
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<void> {
try {
if (!req.user) {
res.status(401).json({ success: false, message: 'Nicht authentifiziert' });
return;
}
const { id } = req.params as Record<string, string>;
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<void> {
try {
const { id } = req.params as Record<string, string>;
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<void> {
try {
const { id, userId } = req.params as Record<string, string>;
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<void> {
try {
const { id } = req.params as Record<string, string>;
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<void> {
try {
const { id, fahrzeugId } = req.params as Record<string, string>;
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<void> {
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();