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>
This commit is contained in:
Florian Wilde
2026-09-03 13:50:58 +02:00
co-authored by Claude Opus 5
parent bc62a9c1e8
commit 91954c402b
100 changed files with 9667 additions and 38 deletions
+127
View File
@@ -0,0 +1,127 @@
import { describe, expect, it } from "vitest";
import {
EINHEITEN,
staerkeAlsText,
staerkeBerechnen,
staerkeHinweise,
taktischeEinheit,
type Funktion,
} from "@/lib/einsatz/staerke";
/**
* Stärke nach FwDV 3.
*
* Die Zahlen stehen später im Einsatzbericht und in der Meldung an die
* Gemeinde. Wer die Zuordnung ändert, ändert damit die Statistik der ganzen
* Wehr — deshalb steht sie hier ausführlich unter Test.
*/
/** Die Besetzung einer Gruppe nach FwDV 3. */
const GRUPPE: Funktion[] = [
"GRUPPENFUEHRER",
"MASCHINIST",
"MELDER",
"TRUPPFUEHRER",
"TRUPPMANN",
"TRUPPFUEHRER",
"TRUPPMANN",
"TRUPPFUEHRER",
"TRUPPMANN",
];
/** Die Besetzung einer Staffel. */
const STAFFEL: Funktion[] = [
"GRUPPENFUEHRER",
"MASCHINIST",
"TRUPPFUEHRER",
"TRUPPMANN",
"TRUPPFUEHRER",
"TRUPPMANN",
];
describe("staerkeBerechnen", () => {
it("zählt eine Gruppe als 1/3/5/9", () => {
const staerke = staerkeBerechnen(GRUPPE);
expect(staerke).toEqual({ fuehrer: 1, unterfuehrer: 3, mannschaft: 5, gesamt: 9 });
expect(staerkeAlsText(staerke)).toBe("1/3/5/9");
});
it("zählt eine Staffel als 1/2/3/6", () => {
expect(staerkeAlsText(staerkeBerechnen(STAFFEL))).toBe("1/2/3/6");
});
it("zählt einen Trupp als 0/1/1/2", () => {
expect(staerkeAlsText(staerkeBerechnen(["TRUPPFUEHRER", "TRUPPMANN"]))).toBe("0/1/1/2");
});
it("zählt jede Führungsfunktion als Führer", () => {
const staerke = staerkeBerechnen([
"EINSATZLEITER",
"ZUGFUEHRER",
"GRUPPENFUEHRER",
"STAFFELFUEHRER",
]);
expect(staerke.fuehrer).toBe(4);
expect(staerke.mannschaft).toBe(0);
});
it("zählt den Maschinisten zur Mannschaft", () => {
// Regional unterschiedlich — dieser Test hält die getroffene Festlegung
// fest, damit eine Änderung auffällt statt durchzurutschen.
const staerke = staerkeBerechnen(["MASCHINIST"]);
expect(staerke.mannschaft).toBe(1);
expect(staerke.unterfuehrer).toBe(0);
});
it("liefert für niemanden lauter Nullen", () => {
expect(staerkeAlsText(staerkeBerechnen([]))).toBe("0/0/0/0");
});
});
describe("taktischeEinheit", () => {
it("erkennt die Einheiten der FwDV 3", () => {
expect(taktischeEinheit(staerkeBerechnen(GRUPPE)).einheit).toBe("Gruppe");
expect(taktischeEinheit(staerkeBerechnen(STAFFEL)).einheit).toBe("Staffel");
expect(taktischeEinheit(staerkeBerechnen(["TRUPPFUEHRER", "TRUPPMANN"])).einheit).toBe("Trupp");
});
it("nennt die überzähligen Kräfte", () => {
const zehn = [...GRUPPE, "TRUPPMANN" as Funktion];
const ergebnis = taktischeEinheit(staerkeBerechnen(zehn));
expect(ergebnis.einheit).toBe("Gruppe");
expect(ergebnis.ueberzaehlig).toBe(1);
});
it("gibt bei einer einzelnen Kraft keine Einheit zurück", () => {
const ergebnis = taktischeEinheit(staerkeBerechnen(["GRUPPENFUEHRER"]));
expect(ergebnis.einheit).toBeNull();
expect(ergebnis.ueberzaehlig).toBe(1);
});
it("kennt die Sollstärken", () => {
expect(EINHEITEN.map((einheit) => einheit.staerke)).toEqual([2, 6, 9, 18]);
});
});
describe("staerkeHinweise", () => {
it("meldet, wenn niemand erfasst ist", () => {
expect(staerkeHinweise(staerkeBerechnen([]))[0]).toContain("noch niemand erfasst");
});
it("meldet eine fehlende Führungskraft", () => {
const hinweise = staerkeHinweise(staerkeBerechnen(["TRUPPFUEHRER", "TRUPPMANN"]));
expect(hinweise.some((hinweis) => hinweis.includes("Führungskraft"))).toBe(true);
});
it("meldet den Alleingang", () => {
const hinweise = staerkeHinweise(staerkeBerechnen(["GRUPPENFUEHRER"]));
expect(hinweise.some((hinweis) => hinweis.includes("Alleine"))).toBe(true);
});
it("schweigt bei einer vollständigen Gruppe", () => {
expect(staerkeHinweise(staerkeBerechnen(GRUPPE))).toHaveLength(0);
});
});