227 lines
9.5 KiB
TypeScript
227 lines
9.5 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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<void> {
|
|
try {
|
|
const { userId } = req.params as Record<string, string>;
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
try {
|
|
const { id } = req.params as Record<string, string>;
|
|
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();
|