Atemschutzueberwachung nach FwDV 7 ergaenzen
Phase 5: das sicherheitsrelevanteste Modul des Projekts. Ueber jeder Ansicht steht dauerhaft, dass die analoge Atemschutzueberwachung fuehrend bleibt — abschaltbar nur zusammen mit der Fundstelle einer Freigabe des Kommandanten. Es gibt keinen Timer. In der Datenbank stehen Zeitstempel; wie lange ein Trupp unter Atemschutz ist, rechnet jede Ansicht bei jedem Bild neu aus. Damit ist die Anforderung "uebersteht Neuladen und Verbindungsabbruch" keine Funktion, die gelingen muss, sondern eine Eigenschaft, die sich nicht verlieren laesst. Server und Browser rechnen mit denselben Funktionen. Der Rueckzugsdruck folgt dem doppelten Anmarschverbrauch: was der Weg hinein gekostet hat, kostet der Weg zurueck noch einmal. Bis der Trupp das Einsatzziel meldet, greift die Zwei-Drittel-Regel — sie liegt hoeher und ist damit die vorsichtigere Annahme. Das Protokoll ist doppelt gesichert: entzogene Rechte und ein Trigger auf druckmessung und atemschutz_ereignis. Ein Fehler wird durch einen neuen Eintrag richtiggestellt, nicht durch Aendern. Verteilt wird ueber Server-Sent Events statt Socket.IO — es geht nur in eine Richtung, und so bleibt das Deployment bei "next start". Behoben: ein abgerissener Ereignisstrom meldete sich nicht. Die Anzeige haette weiter "Verbunden" behauptet, waehrend der Draht tot war. Jetzt gilt Stille von acht Sekunden als Abriss. Dazu zwei Altlasten: der Navigationseintrag "Protokoll" fuehrte ins Leere, und die Startseite stand noch auf Phase 1. 68 neue Tests (51 Unit, 17 gegen eine echte Datenbank, 8 E2E je Browser). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b2b31ed00a
commit
4e2daaafd4
@@ -0,0 +1,195 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { g26FristVorschlag, tauglichkeit } from "@/lib/atemschutz/tauglichkeit";
|
||||
|
||||
/**
|
||||
* Wer darf unter Atemschutz?
|
||||
*
|
||||
* Die Frage entscheidet der Einheitsführer, nicht diese Funktion. Was sie
|
||||
* leisten muss: keinen der drei Nachweise unterschlagen und im Klartext sagen,
|
||||
* welcher fehlt. Eine Ampel, die rot ist, ohne zu sagen warum, hilft am
|
||||
* Fahrzeug niemandem.
|
||||
*/
|
||||
|
||||
const HEUTE = new Date("2026-06-15T08:00:00.000Z");
|
||||
|
||||
function datum(text: string): Date {
|
||||
return new Date(`${text}T00:00:00.000Z`);
|
||||
}
|
||||
|
||||
/** Der Regelfall: alles vorhanden und gültig. */
|
||||
function vollstaendig() {
|
||||
return {
|
||||
untersuchungen: [{ gueltigBis: datum("2028-03-01"), ergebnis: "TAUGLICH" as const }],
|
||||
uebungen: [
|
||||
{ datum: datum("2026-02-10"), art: "UEBUNGSSTRECKE" as const },
|
||||
{ datum: datum("2026-04-20"), art: "EINSATZUEBUNG" as const },
|
||||
],
|
||||
einsaetzeUnterPa: [] as Date[],
|
||||
istAtemschutzgeraetetraeger: true,
|
||||
jetzt: HEUTE,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Tauglichkeit nach FwDV 7", () => {
|
||||
it("erklärt jemanden mit allen drei Nachweisen für einsetzbar", () => {
|
||||
const ergebnis = tauglichkeit(vollstaendig());
|
||||
|
||||
expect(ergebnis.stufe).toBe("tauglich");
|
||||
expect(ergebnis.gruende).toEqual([]);
|
||||
expect(ergebnis.g26GueltigBis).toEqual(datum("2028-03-01"));
|
||||
});
|
||||
|
||||
it("nennt eine fehlende G26 beim Namen", () => {
|
||||
const ergebnis = tauglichkeit({ ...vollstaendig(), untersuchungen: [] });
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
expect(ergebnis.gruende).toContain("Keine G26.3-Untersuchung hinterlegt");
|
||||
});
|
||||
|
||||
it("erkennt eine abgelaufene G26", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
untersuchungen: [{ gueltigBis: datum("2026-01-31"), ergebnis: "TAUGLICH" }],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
expect(ergebnis.gruende).toContain("G26.3 ist abgelaufen");
|
||||
});
|
||||
|
||||
it("warnt drei Monate vor Ablauf, ohne die Person schon auszuschließen", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
untersuchungen: [{ gueltigBis: datum("2026-08-01"), ergebnis: "TAUGLICH" }],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("laeuft_ab");
|
||||
expect(ergebnis.gruende.join(" ")).toMatch(/läuft in \d+ Tagen ab/);
|
||||
});
|
||||
|
||||
it("nimmt die neueste Untersuchung, nicht die erstbeste", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
untersuchungen: [
|
||||
{ gueltigBis: datum("2024-05-01"), ergebnis: "TAUGLICH" },
|
||||
{ gueltigBis: datum("2029-05-01"), ergebnis: "TAUGLICH" },
|
||||
{ gueltigBis: datum("2026-05-01"), ergebnis: "TAUGLICH" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("tauglich");
|
||||
expect(ergebnis.g26GueltigBis).toEqual(datum("2029-05-01"));
|
||||
});
|
||||
|
||||
it("lässt eine befristete Tauglichkeit gelten", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
untersuchungen: [{ gueltigBis: datum("2027-01-01"), ergebnis: "BEFRISTET_TAUGLICH" }],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("tauglich");
|
||||
});
|
||||
|
||||
it("zählt eine Nachuntersuchung nicht als Nachweis", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
untersuchungen: [{ gueltigBis: datum("2027-01-01"), ergebnis: "NACHUNTERSUCHUNG" }],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
});
|
||||
|
||||
it("sagt es deutlich, wenn der Arzt „nicht tauglich“ geschrieben hat", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
untersuchungen: [{ gueltigBis: datum("2028-01-01"), ergebnis: "NICHT_TAUGLICH" }],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
expect(ergebnis.gruende.join(" ")).toContain("nicht tauglich");
|
||||
});
|
||||
|
||||
it("verlangt die Belastungsübung im laufenden Jahr", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
uebungen: [
|
||||
// Letztes Jahr reicht nicht.
|
||||
{ datum: datum("2025-11-02"), art: "UEBUNGSSTRECKE" },
|
||||
{ datum: datum("2026-04-20"), art: "EINSATZUEBUNG" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
expect(ergebnis.gruende).toContain("Belastungsübung in diesem Jahr fehlt");
|
||||
});
|
||||
|
||||
it("lässt einen echten Einsatz unter Atemschutz für die Einsatzübung gelten", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
uebungen: [{ datum: datum("2026-02-10"), art: "UEBUNGSSTRECKE" }],
|
||||
einsaetzeUnterPa: [datum("2026-05-03")],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("tauglich");
|
||||
expect(ergebnis.einsatzuebungAm).toEqual(datum("2026-05-03"));
|
||||
});
|
||||
|
||||
it("lässt einen Einsatz aus dem Vorjahr nicht gelten", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
uebungen: [{ datum: datum("2026-02-10"), art: "UEBUNGSSTRECKE" }],
|
||||
einsaetzeUnterPa: [datum("2025-12-30")],
|
||||
});
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
expect(ergebnis.gruende).toContain("Einsatzübung unter Atemschutz in diesem Jahr fehlt");
|
||||
});
|
||||
|
||||
it("zählt eine Unterweisung nicht als Belastungsübung", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
...vollstaendig(),
|
||||
uebungen: [
|
||||
{ datum: datum("2026-03-01"), art: "UNTERWEISUNG" },
|
||||
{ datum: datum("2026-04-20"), art: "EINSATZUEBUNG" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(ergebnis.gruende).toContain("Belastungsübung in diesem Jahr fehlt");
|
||||
});
|
||||
|
||||
it("verlangt die Qualifikation als Geräteträger", () => {
|
||||
const ergebnis = tauglichkeit({ ...vollstaendig(), istAtemschutzgeraetetraeger: false });
|
||||
|
||||
expect(ergebnis.stufe).toBe("nicht_tauglich");
|
||||
expect(ergebnis.gruende).toContain("Keine Qualifikation als Atemschutzgeräteträger");
|
||||
});
|
||||
|
||||
it("nennt alle fehlenden Nachweise, nicht nur den ersten", () => {
|
||||
const ergebnis = tauglichkeit({
|
||||
untersuchungen: [],
|
||||
uebungen: [],
|
||||
einsaetzeUnterPa: [],
|
||||
istAtemschutzgeraetetraeger: false,
|
||||
jetzt: HEUTE,
|
||||
});
|
||||
|
||||
// Wer nur „nicht tauglich" liest, weiß nicht, was zu tun ist.
|
||||
expect(ergebnis.gruende).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Fristvorschlag für die G26.3", () => {
|
||||
it("schlägt drei Jahre vor, solange jemand unter fünfzig ist", () => {
|
||||
const frist = g26FristVorschlag(datum("1990-04-01"), datum("2026-06-15"));
|
||||
expect(frist).toEqual(datum("2029-06-15"));
|
||||
});
|
||||
|
||||
it("schlägt ein Jahr vor, sobald jemand fünfzig ist", () => {
|
||||
const frist = g26FristVorschlag(datum("1972-01-10"), datum("2026-06-15"));
|
||||
expect(frist).toEqual(datum("2027-06-15"));
|
||||
});
|
||||
|
||||
it("nimmt ohne Geburtsdatum den längeren Zeitraum — überschreibbar bleibt er ohnehin", () => {
|
||||
const frist = g26FristVorschlag(null, datum("2026-06-15"));
|
||||
expect(frist).toEqual(datum("2029-06-15"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user