289 lines
12 KiB
TypeScript
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();
|