import { Request, Response } from 'express'; import { z } from 'zod'; import equipmentService from '../services/equipment.service'; import { AusruestungStatus } from '../models/equipment.model'; import logger from '../utils/logger'; // ── UUID validation ─────────────────────────────────────────────────────────── function isValidUUID(s: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s); } // ── Zod Validation Schemas ──────────────────────────────────────────────────── const AusruestungStatusEnum = z.enum([ AusruestungStatus.Einsatzbereit, AusruestungStatus.Beschaedigt, AusruestungStatus.InWartung, AusruestungStatus.AusserDienst, ]); const isoDate = z.string().regex( /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/, 'Erwartet ISO-Datum im Format YYYY-MM-DD' ); const uuidString = z.string().regex( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, 'Ungültige UUID' ); const CreateAusruestungSchema = z.object({ bezeichnung: z.string().min(1).max(200), kategorie_id: uuidString, seriennummer: z.string().max(100).optional(), inventarnummer: z.string().max(50).optional(), hersteller: z.string().max(150).optional(), baujahr: z.number().int().min(1950).max(2100).optional(), status: AusruestungStatusEnum.optional(), status_bemerkung: z.string().max(2000).optional(), ist_wichtig: z.boolean().optional(), fahrzeug_id: uuidString.optional(), standort: z.string().min(1).max(150).optional(), pruef_intervall_monate: z.number().int().min(1).optional(), letzte_pruefung_am: isoDate.optional(), naechste_pruefung_am: isoDate.optional(), bemerkung: z.string().max(2000).optional(), }); const UpdateAusruestungSchema = z.object({ bezeichnung: z.string().min(1).max(200).optional(), kategorie_id: uuidString.optional(), seriennummer: z.string().max(100).nullable().optional(), inventarnummer: z.string().max(50).nullable().optional(), hersteller: z.string().max(150).nullable().optional(), baujahr: z.number().int().min(1950).max(2100).nullable().optional(), status: AusruestungStatusEnum.optional(), status_bemerkung: z.string().max(2000).nullable().optional(), ist_wichtig: z.boolean().optional(), fahrzeug_id: uuidString.nullable().optional(), standort: z.string().min(1).max(150).optional(), pruef_intervall_monate: z.number().int().min(1).nullable().optional(), letzte_pruefung_am: isoDate.nullable().optional(), naechste_pruefung_am: isoDate.nullable().optional(), bemerkung: z.string().max(2000).nullable().optional(), }); const UpdateStatusSchema = z.object({ status: AusruestungStatusEnum, bemerkung: z.string().max(2000).optional().default(''), }); const CreateWartungslogSchema = z.object({ datum: isoDate, art: z.enum(['Prüfung', 'Reparatur', 'Sonstiges']), beschreibung: z.string().min(1).max(2000), ergebnis: z.enum(['bestanden', 'bestanden_mit_maengeln', 'nicht_bestanden']).optional(), kosten: z.number().min(0).optional(), pruefende_stelle: z.string().max(150).optional(), dokument_url: z.string().url().max(500).refine( (url) => /^https?:\/\//i.test(url), 'Nur http/https URLs erlaubt' ).optional(), }); // ── Helper ──────────────────────────────────────────────────────────────────── function getUserId(req: Request): string { return req.user!.id; } // ── Controller ──────────────────────────────────────────────────────────────── class EquipmentController { async listEquipment(_req: Request, res: Response): Promise { try { const equipment = await equipmentService.getAllEquipment(); res.status(200).json({ success: true, data: equipment }); } catch (error) { logger.error('listEquipment error', { error }); res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht geladen werden' }); } } async getEquipment(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' }); return; } const equipment = await equipmentService.getEquipmentById(id); if (!equipment) { res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' }); return; } res.status(200).json({ success: true, data: equipment }); } catch (error) { logger.error('getEquipment error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht geladen werden' }); } } async getByVehicle(req: Request, res: Response): Promise { try { const { fahrzeugId } = req.params as Record; if (!isValidUUID(fahrzeugId)) { res.status(400).json({ success: false, message: 'Ungültige Fahrzeug-ID' }); return; } const equipment = await equipmentService.getEquipmentByVehicle(fahrzeugId); res.status(200).json({ success: true, data: equipment }); } catch (error) { logger.error('getByVehicle error', { error, fahrzeugId: req.params.fahrzeugId }); res.status(500).json({ success: false, message: 'Ausrüstung für Fahrzeug konnte nicht geladen werden' }); } } async getCategories(_req: Request, res: Response): Promise { try { const categories = await equipmentService.getCategories(); res.status(200).json({ success: true, data: categories }); } catch (error) { logger.error('getCategories error', { error }); res.status(500).json({ success: false, message: 'Kategorien konnten nicht geladen werden' }); } } async getStats(_req: Request, res: Response): Promise { try { const stats = await equipmentService.getEquipmentStats(); res.status(200).json({ success: true, data: stats }); } catch (error) { logger.error('getStats error', { error }); res.status(500).json({ success: false, message: 'Statistiken konnten nicht geladen werden' }); } } async getAlerts(req: Request, res: Response): Promise { try { const raw = parseInt((req.query.daysAhead as string) || '30', 10); if (isNaN(raw) || raw < 0) { res.status(400).json({ success: false, message: 'Ungültiger daysAhead-Wert' }); return; } const daysAhead = Math.min(raw, 365); const alerts = await equipmentService.getUpcomingInspections(daysAhead); res.status(200).json({ success: true, data: alerts }); } catch (error) { logger.error('getAlerts error', { error }); res.status(500).json({ success: false, message: 'Prüfungshinweise konnten nicht geladen werden' }); } } async getVehicleWarnings(_req: Request, res: Response): Promise { try { const warnings = await equipmentService.getVehicleWarnings(); res.status(200).json({ success: true, data: warnings }); } catch (error) { logger.error('getVehicleWarnings error', { error }); res.status(500).json({ success: false, message: 'Fahrzeug-Warnungen konnten nicht geladen werden' }); } } async createEquipment(req: Request, res: Response): Promise { try { const parsed = CreateAusruestungSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const equipment = await equipmentService.createEquipment(parsed.data, getUserId(req)); res.status(201).json({ success: true, data: equipment }); } catch (error) { logger.error('createEquipment error', { error }); res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht erstellt werden' }); } } async updateEquipment(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' }); return; } const parsed = UpdateAusruestungSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } if (Object.keys(parsed.data).length === 0) { res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' }); return; } const equipment = await equipmentService.updateEquipment(id, parsed.data, getUserId(req)); if (!equipment) { res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' }); return; } res.status(200).json({ success: true, data: equipment }); } catch (error: any) { if (error?.message === 'No fields to update') { res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' }); return; } logger.error('updateEquipment error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht aktualisiert werden' }); } } async updateStatus(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' }); return; } const parsed = UpdateStatusSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } await equipmentService.updateStatus( id, parsed.data.status, parsed.data.bemerkung, getUserId(req) ); res.status(200).json({ success: true, message: 'Status aktualisiert' }); } catch (error: any) { if (error?.message === 'Equipment not found') { res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' }); return; } logger.error('updateStatus error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Status konnte nicht aktualisiert werden' }); } } async deleteEquipment(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' }); return; } const deleted = await equipmentService.deleteEquipment(id, getUserId(req)); if (!deleted) { res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' }); return; } res.status(200).json({ success: true, message: 'Ausrüstung gelöscht' }); } catch (error) { logger.error('deleteEquipment error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Ausrüstung konnte nicht gelöscht werden' }); } } async addWartung(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Ausrüstungs-ID' }); return; } const parsed = CreateWartungslogSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const entry = await equipmentService.addWartungslog(id, parsed.data, getUserId(req)); res.status(201).json({ success: true, data: entry }); } catch (error: any) { if (error?.message === 'Equipment not found') { res.status(404).json({ success: false, message: 'Ausrüstung nicht gefunden' }); return; } logger.error('addWartung error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Wartungseintrag konnte nicht gespeichert werden' }); } } } export default new EquipmentController();