import { G26Ergebnis, UebungsartAtemschutz } from "@prisma/client"; /** * Wer darf unter Atemschutz? * * FwDV 7 verlangt dreierlei, und alle drei müssen gleichzeitig gelten: * * 1. **Gültige G26.3.** Die Untersuchung beim Arbeitsmediziner. Ohne sie * geht gar nichts, auch nicht „einmalig ausnahmsweise". * 2. **Belastungsübung im laufenden Jahr.** Übungsstrecke oder eine * gleichwertige Belastung. * 3. **Einsatzübung unter Atemschutz im laufenden Jahr.** Ein Einsatz unter * PA zählt dafür ebenso — wer im Ernstfall drin war, hat mehr geleistet * als in jeder Übung. * * Dazu die Qualifikation als Atemschutzgeräteträger, die aus Phase 1 kommt. * * Diese Datei rechnet nur. Sie entscheidet nicht, ob jemand eingesetzt wird — * das tut der Einheitsführer, und das Ergebnis hier ist der Hinweis darauf, * was ihm dabei bekannt sein sollte. */ /** Wie lange vor Ablauf gewarnt wird. Drei Monate reichen für einen Arzttermin. */ export const VORWARNUNG_TAGE = 90; export type Tauglichkeitsstufe = "tauglich" | "laeuft_ab" | "nicht_tauglich"; export const STUFE_TEXT: Record = { tauglich: "einsetzbar", laeuft_ab: "einsetzbar, läuft bald ab", nicht_tauglich: "nicht einsetzbar", }; export interface Tauglichkeit { stufe: Tauglichkeitsstufe; /** Klartext, was fehlt — die Farbe allein sagt niemandem, was zu tun ist. */ gruende: string[]; g26GueltigBis: Date | null; belastungsuebungAm: Date | null; einsatzuebungAm: Date | null; } function tageBis(ziel: Date, jetzt: Date): number { return Math.floor((ziel.getTime() - jetzt.getTime()) / 86_400_000); } /** Fällt ein Datum in dasselbe Kalenderjahr wie der Stichtag? */ function imSelbenJahr(datum: Date, jetzt: Date): boolean { return datum.getUTCFullYear() === jetzt.getUTCFullYear(); } /** * Bewertet eine Person. * * Bewusst ohne Datenbankzugriff: die Aufrufer holen die Daten so, wie es an * ihrer Stelle günstig ist — einzeln für eine Person, gebündelt für die * Übersicht der ganzen Mannschaft. */ export function tauglichkeit(angaben: { /** Untersuchungen der Person, beliebige Reihenfolge. */ untersuchungen: Array<{ gueltigBis: Date; ergebnis: G26Ergebnis }>; /** Übungen und Unterweisungen, beliebige Reihenfolge. */ uebungen: Array<{ datum: Date; art: UebungsartAtemschutz }>; /** Einsätze unter Atemschutz im laufenden Jahr — sie ersetzen die Einsatzübung. */ einsaetzeUnterPa: Date[]; /** Ist die Person überhaupt als Atemschutzgeräteträger qualifiziert? */ istAtemschutzgeraetetraeger: boolean; jetzt: Date; }): Tauglichkeit { const { untersuchungen, uebungen, einsaetzeUnterPa, jetzt } = angaben; const gruende: string[] = []; // --- G26.3 --------------------------------------------------------------- const brauchbar = untersuchungen.filter( (untersuchung) => untersuchung.ergebnis === G26Ergebnis.TAUGLICH || untersuchung.ergebnis === G26Ergebnis.BEFRISTET_TAUGLICH, ); const neueste = brauchbar.reduce( (bisher, untersuchung) => !bisher || untersuchung.gueltigBis.getTime() > bisher.getTime() ? untersuchung.gueltigBis : bisher, null, ); const abgelehnt = untersuchungen.some( (untersuchung) => untersuchung.ergebnis === G26Ergebnis.NICHT_TAUGLICH, ); let g26Laeuft = false; if (!neueste) { gruende.push( abgelehnt ? "G26.3 ergab „nicht tauglich“" : "Keine G26.3-Untersuchung hinterlegt", ); } else if (tageBis(neueste, jetzt) < 0) { gruende.push("G26.3 ist abgelaufen"); } else if (tageBis(neueste, jetzt) <= VORWARNUNG_TAGE) { g26Laeuft = true; } // --- Belastungsübung ----------------------------------------------------- const belastung = uebungen .filter( (uebung) => uebung.art === UebungsartAtemschutz.UEBUNGSSTRECKE && imSelbenJahr(uebung.datum, jetzt), ) .sort((a, b) => b.datum.getTime() - a.datum.getTime())[0]; if (!belastung) gruende.push("Belastungsübung in diesem Jahr fehlt"); // --- Einsatzübung oder Einsatz unter PA ---------------------------------- const einsatzuebung = uebungen .filter( (uebung) => uebung.art === UebungsartAtemschutz.EINSATZUEBUNG && imSelbenJahr(uebung.datum, jetzt), ) .sort((a, b) => b.datum.getTime() - a.datum.getTime())[0]; const echterEinsatz = einsaetzeUnterPa .filter((datum) => imSelbenJahr(datum, jetzt)) .sort((a, b) => b.getTime() - a.getTime())[0]; if (!einsatzuebung && !echterEinsatz) { gruende.push("Einsatzübung unter Atemschutz in diesem Jahr fehlt"); } // --- Qualifikation ------------------------------------------------------- if (!angaben.istAtemschutzgeraetetraeger) { gruende.push("Keine Qualifikation als Atemschutzgeräteträger"); } const stufe: Tauglichkeitsstufe = gruende.length > 0 ? "nicht_tauglich" : g26Laeuft ? "laeuft_ab" : "tauglich"; if (g26Laeuft && neueste) { gruende.push(`G26.3 läuft in ${tageBis(neueste, jetzt)} Tagen ab`); } return { stufe, gruende, g26GueltigBis: neueste, belastungsuebungAm: belastung?.datum ?? null, einsatzuebungAm: einsatzuebung?.datum ?? echterEinsatz ?? null, }; } /** * Frist einer G26.3. * * Die Regel richtet sich nach dem Alter: bis 50 Jahre drei Jahre, danach ein * Jahr. Der Arzt kann kürzer befristen — deshalb ist das hier nur der * Vorschlag für das Eingabefeld, überschreibbar wie das Ablaufdatum einer * Qualifikation aus Phase 1. */ export function g26FristVorschlag(geburtsdatum: Date | null, untersuchtAm: Date): Date { const vorschlag = new Date(untersuchtAm); const alter = geburtsdatum ? Math.floor((untersuchtAm.getTime() - geburtsdatum.getTime()) / (365.25 * 86_400_000)) : 0; vorschlag.setUTCFullYear(vorschlag.getUTCFullYear() + (alter >= 50 ? 1 : 3)); return vorschlag; } export const G26_ERGEBNIS_TEXT: Record = { TAUGLICH: "tauglich", BEFRISTET_TAUGLICH: "befristet tauglich", NACHUNTERSUCHUNG: "Nachuntersuchung nötig", NICHT_TAUGLICH: "nicht tauglich", }; export const UEBUNGSART_TEXT: Record = { UEBUNGSSTRECKE: "Belastungsübung (Übungsstrecke)", EINSATZUEBUNG: "Einsatzübung unter Atemschutz", UNTERWEISUNG: "Unterweisung", };