Files
dashboard/backend/src/services/atemschutz.service.ts
Matthias Hochmeister 5dfaf7db54 bug fixes
2026-03-03 14:45:46 +01:00

289 lines
12 KiB
TypeScript

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<AtemschutzUebersicht[]> {
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<AtemschutzUebersicht | null> {
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<AtemschutzUebersicht | null> {
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<AtemschutzTraeger> {
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<AtemschutzTraeger | null> {
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<boolean> {
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<AtemschutzStats> {
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();