312 lines
12 KiB
TypeScript
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();
|