import { Request, Response } from 'express'; import atemschutzService from '../services/atemschutz.service'; import notificationService from '../services/notification.service'; import { CreateAtemschutzSchema, UpdateAtemschutzSchema } from '../models/atemschutz.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); } // ── Helper ──────────────────────────────────────────────────────────────────── function getUserId(req: Request): string { return req.user!.id; } // ── Controller ──────────────────────────────────────────────────────────────── class AtemschutzController { async list(req: Request, res: Response): Promise { try { const userGroups: string[] = (req.user as any)?.groups ?? []; const userId = getUserId(req); const records = await atemschutzService.getAll(userGroups, userId); res.status(200).json({ success: true, data: records }); } catch (error) { logger.error('Atemschutz list error', { error }); res.status(500).json({ success: false, message: 'Atemschutzträger konnten nicht geladen werden' }); } } async getOne(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Atemschutz-ID' }); return; } const record = await atemschutzService.getById(id); if (!record) { res.status(404).json({ success: false, message: 'Atemschutzträger nicht gefunden' }); return; } res.status(200).json({ success: true, data: record }); } catch (error) { logger.error('Atemschutz getOne error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Atemschutzträger konnte nicht geladen werden' }); } } async getStats(req: Request, res: Response): Promise { try { const userGroups: string[] = (req.user as any)?.groups ?? []; const userId = getUserId(req); const stats = await atemschutzService.getStats(userGroups, userId); res.status(200).json({ success: true, data: stats }); } catch (error) { logger.error('Atemschutz getStats error', { error }); res.status(500).json({ success: false, message: 'Atemschutz-Statistiken konnten nicht geladen werden' }); } } async create(req: Request, res: Response): Promise { try { const parsed = CreateAtemschutzSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, message: 'Validierungsfehler', errors: parsed.error.flatten().fieldErrors, }); return; } const record = await atemschutzService.create(parsed.data, getUserId(req)); res.status(201).json({ success: true, data: record }); } catch (error: any) { if (error?.message?.includes('bereits ein Atemschutz-Eintrag')) { res.status(409).json({ success: false, message: error.message }); return; } logger.error('Atemschutz create error', { error }); res.status(500).json({ success: false, message: 'Atemschutzträger konnte nicht erstellt werden' }); } } async update(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Atemschutz-ID' }); return; } const parsed = UpdateAtemschutzSchema.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 record = await atemschutzService.update(id, parsed.data, getUserId(req)); if (!record) { res.status(404).json({ success: false, message: 'Atemschutzträger nicht gefunden' }); return; } res.status(200).json({ success: true, data: record }); } 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('Atemschutz update error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Atemschutzträger konnte nicht aktualisiert werden' }); } } async getByUserId(req: Request, res: Response): Promise { try { const { userId } = req.params as Record; if (!isValidUUID(userId)) { res.status(400).json({ success: false, message: 'Ungültige Benutzer-ID' }); return; } const callerId = getUserId(req); const callerGroups: string[] = (req.user as any)?.groups ?? []; const privileged = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator']; const isPrivileged = callerGroups.some((g) => privileged.includes(g)); if (userId !== callerId && !isPrivileged) { res.status(403).json({ success: false, message: 'Keine Berechtigung' }); return; } const record = await atemschutzService.getByUserId(userId); res.status(200).json({ success: true, data: record ?? null }); } catch (error) { logger.error('Atemschutz getByUserId error', { error, userId: req.params.userId }); res.status(500).json({ success: false, message: 'Atemschutzstatus konnte nicht geladen werden' }); } } async getMyStatus(req: Request, res: Response): Promise { try { const userId = getUserId(req); const record = await atemschutzService.getByUserId(userId); if (!record) { // User has no atemschutz entry — not an error, just no data res.status(200).json({ success: true, data: null }); return; } res.status(200).json({ success: true, data: record }); } catch (error) { logger.error('Atemschutz getMyStatus error', { error }); res.status(500).json({ success: false, message: 'Persönlicher Atemschutz-Status konnte nicht geladen werden' }); } } async getExpiring(_req: Request, res: Response): Promise { try { const expiring = await atemschutzService.getExpiringCertifications(30); // Side-effect: create notifications for expiring certifications (dedup via DB constraint) for (const item of expiring) { if (item.untersuchung_status !== 'ok') { await notificationService.createNotification({ user_id: item.user_id, typ: 'atemschutz_expiry', titel: item.untersuchung_status === 'abgelaufen' ? 'G26 Untersuchung abgelaufen' : 'G26 Untersuchung läuft bald ab', nachricht: `Ihre G26 Untersuchung ${item.untersuchung_status === 'abgelaufen' ? 'ist abgelaufen' : 'läuft bald ab'}.`, schwere: item.untersuchung_status === 'abgelaufen' ? 'fehler' : 'warnung', quell_typ: 'atemschutz_untersuchung', quell_id: item.id, link: '/atemschutz', }); } if (item.leistungstest_status !== 'ok') { await notificationService.createNotification({ user_id: item.user_id, typ: 'atemschutz_expiry', titel: item.leistungstest_status === 'abgelaufen' ? 'Leistungstest abgelaufen' : 'Leistungstest läuft bald ab', nachricht: `Ihr Leistungstest ${item.leistungstest_status === 'abgelaufen' ? 'ist abgelaufen' : 'läuft bald ab'}.`, schwere: item.leistungstest_status === 'abgelaufen' ? 'fehler' : 'warnung', quell_typ: 'atemschutz_leistungstest', quell_id: item.id, link: '/atemschutz', }); } } res.status(200).json({ success: true, data: expiring }); } catch (error) { logger.error('Atemschutz getExpiring error', { error }); res.status(500).json({ success: false, message: 'Ablaufende Zertifizierungen konnten nicht geladen werden' }); } } async delete(req: Request, res: Response): Promise { try { const { id } = req.params as Record; if (!isValidUUID(id)) { res.status(400).json({ success: false, message: 'Ungültige Atemschutz-ID' }); return; } const deleted = await atemschutzService.delete(id, getUserId(req)); if (!deleted) { res.status(404).json({ success: false, message: 'Atemschutzträger nicht gefunden' }); return; } res.status(200).json({ success: true, message: 'Atemschutzträger gelöscht' }); } catch (error) { logger.error('Atemschutz delete error', { error, id: req.params.id }); res.status(500).json({ success: false, message: 'Atemschutzträger konnte nicht gelöscht werden' }); } } } export default new AtemschutzController();