Files
Florian WildeandClaude Opus 5 91954c402b Phase 3: Einsatz- und Uebungserfassung
Schnellerfassung im Alarmfall, sechs Zeitstempel mit Ausrueckzeit, Anfahrt
und Hilfsfrist, Staerke nach FwDV 3, Personalvorschlag mit
Qualifikationsstand, Einsatzbericht als PDF, Jahresexport fuer die Gemeinde,
Statistik und Uebungen mit Anwesenheitsliste.

Dazu das Einsatzportal fuer Fremdwehren: ein signierter Link ohne Anmeldung,
zeitlich und mengenbegrenzt, der Stichwort, Ort, Zeiten und Lagemeldungen
zeigt und Meldungen mit Foto annimmt. Es sind die ersten oeffentlich
erreichbaren Endpunkte in FLORIAN.

Acht neue Tabellen, alle mit organisationId und Row Level Security. Ein
freigegebener Einsatzbericht ist unveraenderlich; ein Datenbanktrigger setzt
das durch. Koordinaten liegen doppelt — als Dezimalgrad und als
PostGIS-Punkt, zusammengehalten von einem Trigger.

57 neue Tests (32 Unit, 14 gegen eine echte Datenbank, 11 E2E).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 13:50:58 +02:00

139 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
alsDauer,
anfahrtszeit,
ausrueckzeit,
einsatzdauer,
hilfsfrist,
kennzahlen,
naechsterStempel,
zeitHinweise,
} from "@/lib/einsatz/zeiten";
import { nummerBilden, nummerLesen, naechsteNummer } from "@/lib/nummernkreis";
/**
* Einsatzzeiten und Nummernkreis.
*
* Die Hilfsfrist ist die Zahl, über die eine Wehr mit ihrer Gemeinde
* diskutiert. Wenn sie falsch gerechnet wird, merkt es niemand — bis es
* darauf ankommt.
*/
function uhr(stunde: number, minute: number): Date {
return new Date(Date.UTC(2026, 5, 15, stunde, minute, 0));
}
const EINSATZ = {
alarmiertUm: uhr(3, 42),
ausgeruecktUm: uhr(3, 47),
eingetroffenUm: uhr(3, 51),
einsatzendeUm: uhr(6, 5),
eingeruecktUm: uhr(6, 30),
einsatzbereitUm: uhr(7, 0),
};
describe("Kennzahlen", () => {
it("rechnet Ausrückzeit, Anfahrt und Hilfsfrist", () => {
expect(ausrueckzeit(EINSATZ)).toBe(5);
expect(anfahrtszeit(EINSATZ)).toBe(4);
expect(hilfsfrist(EINSATZ)).toBe(9);
});
it("rechnet die Einsatzdauer bis zum Einrücken", () => {
expect(einsatzdauer(EINSATZ)).toBe(168);
});
it("nimmt für die Dauer ersatzweise das Einsatzende", () => {
expect(einsatzdauer({ ...EINSATZ, eingeruecktUm: null })).toBe(143);
});
it("liefert null, wo ein Stempel fehlt", () => {
expect(hilfsfrist({ alarmiertUm: uhr(3, 42) })).toBeNull();
expect(ausrueckzeit({})).toBeNull();
});
it("liefert alle vier Zahlen auf einmal", () => {
expect(kennzahlen(EINSATZ)).toEqual({
ausrueckzeit: 5,
anfahrtszeit: 4,
hilfsfrist: 9,
dauer: 168,
});
});
it("gibt keine negative Zeit zurück, wenn Stempel verdreht sind", () => {
// Die Reihenfolge stimmt nicht — dann lieber keine Zahl als eine falsche.
expect(ausrueckzeit({ alarmiertUm: uhr(4, 0), ausgeruecktUm: uhr(3, 50) })).toBeNull();
});
});
describe("zeitHinweise", () => {
it("schweigt bei einem stimmigen Ablauf", () => {
expect(zeitHinweise(EINSATZ)).toHaveLength(0);
});
it("meldet eine verdrehte Reihenfolge", () => {
const hinweise = zeitHinweise({ ...EINSATZ, eingetroffenUm: uhr(3, 40) });
expect(hinweise.length).toBeGreaterThan(0);
expect(hinweise[0]).toContain("eingetroffen");
});
it("meldet eine unplausibel lange Anfahrt", () => {
const hinweise = zeitHinweise({
alarmiertUm: uhr(3, 0),
eingetroffenUm: uhr(5, 0),
});
expect(hinweise.some((hinweis) => hinweis.includes("120 Minuten"))).toBe(true);
});
});
describe("naechsterStempel", () => {
it("nennt den ersten offenen Stempel", () => {
expect(naechsterStempel({})).toBe("alarmiertUm");
expect(naechsterStempel({ alarmiertUm: uhr(3, 42) })).toBe("ausgeruecktUm");
});
it("gibt null zurück, wenn alles gesetzt ist", () => {
expect(naechsterStempel(EINSATZ)).toBeNull();
});
});
describe("alsDauer", () => {
it("schreibt Minuten unter einer Stunde als Minuten", () => {
expect(alsDauer(9)).toBe("9 min");
expect(alsDauer(59)).toBe("59 min");
});
it("schreibt längere Zeiten als Stunden", () => {
expect(alsDauer(60)).toBe("1:00 h");
expect(alsDauer(168)).toBe("2:48 h");
});
it("kommt mit fehlenden Werten zurecht", () => {
expect(alsDauer(null)).toBe("—");
expect(alsDauer(undefined)).toBe("—");
});
});
describe("Nummernkreis", () => {
it("bildet und liest Nummern", () => {
expect(nummerBilden(2026, 7)).toBe("2026-0007");
expect(nummerLesen("2026-0007")).toEqual({ jahr: 2026, laufend: 7 });
expect(nummerLesen("Unsinn")).toBeNull();
});
it("zählt von der bisher höchsten Nummer weiter", () => {
expect(naechsteNummer(2026, "2026-0041")).toBe("2026-0042");
});
it("beginnt im leeren Jahr bei eins", () => {
expect(naechsteNummer(2027, null)).toBe("2027-0001");
});
it("übernimmt keine Nummer aus einem anderen Jahr", () => {
// Der Aufrufer sucht die höchste Nummer des Jahres; bekommt er keine,
// beginnt das neue Jahr wieder bei eins.
expect(naechsteNummer(2027, undefined)).toBe("2027-0001");
});
});