import pool from '../config/database'; import logger from '../utils/logger'; import { AtemschutzTraeger, AtemschutzUebersicht, AtemschutzStats, CreateAtemschutzData, UpdateAtemschutzData, } from '../models/atemschutz.model'; const ATEMSCHUTZ_PRIVILEGED = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator']; class AtemschutzService { // ========================================================================= // ÜBERSICHT (ALL RECORDS) // ========================================================================= async getAll(userGroups: string[], userId: string): Promise { const isPrivileged = userGroups.some(g => ATEMSCHUTZ_PRIVILEGED.includes(g)); try { let result; if (isPrivileged) { result = await pool.query(` SELECT * FROM atemschutz_uebersicht WHERE mitglied_status IS NULL OR mitglied_status IN ('aktiv', 'anwärter') ORDER BY user_family_name, user_given_name `); } else { result = await pool.query(` SELECT * FROM atemschutz_uebersicht WHERE user_id = $1 `, [userId]); } return result.rows.map((row) => ({ ...row, untersuchung_tage_rest: row.untersuchung_tage_rest != null ? parseInt(row.untersuchung_tage_rest, 10) : null, leistungstest_tage_rest: row.leistungstest_tage_rest != null ? parseInt(row.leistungstest_tage_rest, 10) : null, })) as AtemschutzUebersicht[]; } catch (error) { logger.error('AtemschutzService.getAll fehlgeschlagen', { error }); throw new Error('Atemschutzträger konnten nicht geladen werden'); } } // ========================================================================= // EINZELNER DATENSATZ (BY ID) // ========================================================================= async getById(id: string): Promise { try { const result = await pool.query( `SELECT * FROM atemschutz_uebersicht WHERE id = $1`, [id] ); if (result.rows.length === 0) return null; const row = result.rows[0]; return { ...row, untersuchung_tage_rest: row.untersuchung_tage_rest != null ? parseInt(row.untersuchung_tage_rest, 10) : null, leistungstest_tage_rest: row.leistungstest_tage_rest != null ? parseInt(row.leistungstest_tage_rest, 10) : null, } as AtemschutzUebersicht; } catch (error) { logger.error('AtemschutzService.getById fehlgeschlagen', { error, id }); throw new Error('Atemschutzträger konnte nicht geladen werden'); } } // ========================================================================= // EINZELNER DATENSATZ (BY USER_ID) // ========================================================================= async getByUserId(userId: string): Promise { try { const result = await pool.query( `SELECT * FROM atemschutz_uebersicht WHERE user_id = $1`, [userId] ); if (result.rows.length === 0) return null; const row = result.rows[0]; return { ...row, untersuchung_tage_rest: row.untersuchung_tage_rest != null ? parseInt(row.untersuchung_tage_rest, 10) : null, leistungstest_tage_rest: row.leistungstest_tage_rest != null ? parseInt(row.leistungstest_tage_rest, 10) : null, } as AtemschutzUebersicht; } catch (error) { logger.error('AtemschutzService.getByUserId fehlgeschlagen', { error, userId }); throw new Error('Atemschutzträger konnte nicht geladen werden'); } } // ========================================================================= // CREATE // ========================================================================= async create(data: CreateAtemschutzData, createdBy: string): Promise { try { const result = await pool.query( `INSERT INTO atemschutz_traeger ( id, user_id, atemschutz_lehrgang, lehrgang_datum, untersuchung_datum, untersuchung_gueltig_bis, untersuchung_ergebnis, leistungstest_datum, leistungstest_gueltig_bis, leistungstest_bestanden, bemerkung ) VALUES (uuid_generate_v4(),$1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`, [ data.user_id, data.atemschutz_lehrgang ?? false, data.lehrgang_datum ?? null, data.untersuchung_datum ?? null, data.untersuchung_gueltig_bis ?? null, data.untersuchung_ergebnis ?? null, data.leistungstest_datum ?? null, data.leistungstest_gueltig_bis ?? null, data.leistungstest_bestanden ?? null, data.bemerkung ?? null, ] ); const record = result.rows[0] as AtemschutzTraeger; logger.info('Atemschutzträger erstellt', { id: record.id, userId: data.user_id, by: createdBy }); return record; } catch (error: any) { if (error?.code === '23505') { // unique constraint on user_id throw new Error('Für diesen Benutzer existiert bereits ein Atemschutz-Eintrag'); } logger.error('AtemschutzService.create fehlgeschlagen', { error, createdBy }); throw new Error('Atemschutzträger konnte nicht erstellt werden'); } } // ========================================================================= // UPDATE // ========================================================================= async update(id: string, data: UpdateAtemschutzData, updatedBy: string): Promise { try { const fields: string[] = []; const values: unknown[] = []; let p = 1; const addField = (col: string, value: unknown) => { fields.push(`${col} = $${p++}`); values.push(value); }; if (data.atemschutz_lehrgang !== undefined) addField('atemschutz_lehrgang', data.atemschutz_lehrgang); if (data.lehrgang_datum !== undefined) addField('lehrgang_datum', data.lehrgang_datum); if (data.untersuchung_datum !== undefined) addField('untersuchung_datum', data.untersuchung_datum); if (data.untersuchung_gueltig_bis !== undefined) addField('untersuchung_gueltig_bis', data.untersuchung_gueltig_bis); if (data.untersuchung_ergebnis !== undefined) addField('untersuchung_ergebnis', data.untersuchung_ergebnis); if (data.leistungstest_datum !== undefined) addField('leistungstest_datum', data.leistungstest_datum); if (data.leistungstest_gueltig_bis !== undefined) addField('leistungstest_gueltig_bis', data.leistungstest_gueltig_bis); if (data.leistungstest_bestanden !== undefined) addField('leistungstest_bestanden', data.leistungstest_bestanden); if (data.bemerkung !== undefined) addField('bemerkung', data.bemerkung); if (fields.length === 0) { throw new Error('No fields to update'); } // always bump updated_at fields.push(`updated_at = NOW()`); values.push(id); const result = await pool.query( `UPDATE atemschutz_traeger SET ${fields.join(', ')} WHERE id = $${p} RETURNING *`, values ); if (result.rows.length === 0) { return null; } const record = result.rows[0] as AtemschutzTraeger; logger.info('Atemschutzträger aktualisiert', { id, by: updatedBy }); return record; } catch (error) { logger.error('AtemschutzService.update fehlgeschlagen', { error, id, updatedBy }); throw error; } } // ========================================================================= // DELETE (hard delete — qualification record) // ========================================================================= async delete(id: string, deletedBy: string): Promise { try { const result = await pool.query( `DELETE FROM atemschutz_traeger WHERE id = $1 RETURNING id`, [id] ); if (result.rows.length === 0) { return false; } logger.info('Atemschutzträger gelöscht', { id, by: deletedBy }); return true; } catch (error) { logger.error('AtemschutzService.delete fehlgeschlagen', { error, id }); throw error; } } // ========================================================================= // DASHBOARD KPI / STATISTIKEN // ========================================================================= async getStats(userGroups: string[], _userId: string): Promise { const isPrivileged = userGroups.some(g => ATEMSCHUTZ_PRIVILEGED.includes(g)); if (!isPrivileged) { return { total: 0, mitLehrgang: 0, untersuchungGueltig: 0, untersuchungAbgelaufen: 0, untersuchungBaldFaellig: 0, leistungstestGueltig: 0, leistungstestAbgelaufen: 0, leistungstestBaldFaellig: 0, einsatzbereit: 0, }; } try { const result = await pool.query(` SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE atemschutz_lehrgang = TRUE) AS mit_lehrgang, COUNT(*) FILTER (WHERE untersuchung_gueltig = TRUE) AS untersuchung_gueltig, COUNT(*) FILTER ( WHERE untersuchung_gueltig_bis IS NOT NULL AND untersuchung_gueltig_bis < CURRENT_DATE ) AS untersuchung_abgelaufen, COUNT(*) FILTER ( WHERE untersuchung_gueltig_bis IS NOT NULL AND untersuchung_gueltig_bis >= CURRENT_DATE AND untersuchung_gueltig_bis <= CURRENT_DATE + INTERVAL '90 days' ) AS untersuchung_bald_faellig, COUNT(*) FILTER (WHERE leistungstest_gueltig = TRUE) AS leistungstest_gueltig, COUNT(*) FILTER ( WHERE leistungstest_gueltig_bis IS NOT NULL AND leistungstest_gueltig_bis < CURRENT_DATE ) AS leistungstest_abgelaufen, COUNT(*) FILTER ( WHERE leistungstest_gueltig_bis IS NOT NULL AND leistungstest_gueltig_bis >= CURRENT_DATE AND leistungstest_gueltig_bis <= CURRENT_DATE + INTERVAL '30 days' ) AS leistungstest_bald_faellig, COUNT(*) FILTER (WHERE einsatzbereit = TRUE) AS einsatzbereit FROM atemschutz_uebersicht WHERE mitglied_status IS NULL OR mitglied_status IN ('aktiv', 'anwärter') `); const row = result.rows[0] ?? {}; return { total: parseInt(row.total ?? '0', 10), mitLehrgang: parseInt(row.mit_lehrgang ?? '0', 10), untersuchungGueltig: parseInt(row.untersuchung_gueltig ?? '0', 10), untersuchungAbgelaufen: parseInt(row.untersuchung_abgelaufen ?? '0', 10), untersuchungBaldFaellig: parseInt(row.untersuchung_bald_faellig ?? '0', 10), leistungstestGueltig: parseInt(row.leistungstest_gueltig ?? '0', 10), leistungstestAbgelaufen: parseInt(row.leistungstest_abgelaufen ?? '0', 10), leistungstestBaldFaellig: parseInt(row.leistungstest_bald_faellig ?? '0', 10), einsatzbereit: parseInt(row.einsatzbereit ?? '0', 10), }; } catch (error) { logger.error('AtemschutzService.getStats fehlgeschlagen', { error }); throw new Error('Atemschutz-Statistiken konnten nicht geladen werden'); } } } export default new AtemschutzService();