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
+43
View File
@@ -0,0 +1,43 @@
/**
* Laufende Nummern je Wehr und Jahr.
*
* Prüfprotokolle, Einsätze und Übungen tragen dieselbe Art von Nummer:
* `JJJJ-NNNN`, fortlaufend innerhalb eines Jahres, je Wehr getrennt. Sie steht
* auf Papier und wird am Telefon genannt — deshalb kurz, sprechbar und ohne
* Bindestrichwüste.
*
* Die Nummern werden nie wiederverwendet. Vergeben wird die höchste vorhandene
* plus eins, nicht die Anzahl der Datensätze; sonst bekäme ein im
* Aufbewahrungslauf entfernter Vorgang seine Nummer an einen neuen vererbt.
*
* Reine Zeichenkettenarbeit, kein Datenbankzugriff: wer die Nummer vergibt,
* muss dabei selbst für eine Sperre sorgen (siehe `abschliessen` im
* Prüfungs-Router und `anlegen` im Einsatz-Router).
*/
/** Vier Stellen reichen für 9999 Vorgänge im Jahr — mehr hat keine Wehr. */
const STELLEN = 4;
/** Bildet "2026-0042". */
export function nummerBilden(jahr: number, laufend: number): string {
return `${jahr}-${String(laufend).padStart(STELLEN, "0")}`;
}
/** Zerlegt eine Nummer wieder. Gibt null zurück, wenn sie nicht passt. */
export function nummerLesen(
nummer: string | null | undefined,
): { jahr: number; laufend: number } | null {
if (!nummer) return null;
const treffer = /^(\d{4})-(\d{1,6})$/.exec(nummer.trim());
if (!treffer) return null;
return { jahr: Number(treffer[1]), laufend: Number(treffer[2]) };
}
/**
* Die nächste Nummer nach der bisher höchsten.
*
* @param hoechste die bisher höchste Nummer des Jahres, oder null
*/
export function naechsteNummer(jahr: number, hoechste: string | null | undefined): string {
return nummerBilden(jahr, (nummerLesen(hoechste)?.laufend ?? 0) + 1);
}