Files
FireAdministration/src/lib/einsatz/staerke.ts
T
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

157 lines
4.5 KiB
TypeScript

/**
* Einsatzstärke nach FwDV 3.
*
* Die Stärke ist die Kurzform dafür, wer ausgerückt ist. Geschrieben wird sie
* als `Führer / Unterführer / Mannschaft / Gesamt`, gesprochen "eins, drei,
* fünf, neun".
*
* Die taktischen Einheiten der FwDV 3:
*
* | Einheit | Stärke | wer |
* |---|---|---|
* | Trupp | 0/1/1/2 | Truppführer und Truppmann |
* | Staffel | 1/1/4/6 | Gruppenführer, Maschinist, zwei Trupps |
* | Gruppe | 1/3/5/9 | Gruppenführer, Maschinist, Melder, drei Trupps |
* | Zug | 1/5/12/18 | Zugführer, Melder, Gruppe und Staffel |
*
* **Achtung, regional unterschiedlich.** Manche Wehren zählen den Maschinisten
* zu den Unterführern, andere zur Mannschaft; verbreitet ist auch die
* dreistellige Schreibweise ohne Unterführer (Gruppe = 1/8/9). Die Zuordnung
* steht deshalb an einer Stelle und ist bewusst leicht zu ändern.
*/
export type Funktion =
| "EINSATZLEITER"
| "ZUGFUEHRER"
| "GRUPPENFUEHRER"
| "STAFFELFUEHRER"
| "TRUPPFUEHRER"
| "MASCHINIST"
| "MELDER"
| "TRUPPMANN"
| "ATEMSCHUTZTRAEGER"
| "SICHERHEITSTRUPP"
| "SANITAETER"
| "SONSTIGES";
export const FUNKTION_TEXT: Record<Funktion, string> = {
EINSATZLEITER: "Einsatzleiter",
ZUGFUEHRER: "Zugführer",
GRUPPENFUEHRER: "Gruppenführer",
STAFFELFUEHRER: "Staffelführer",
TRUPPFUEHRER: "Truppführer",
MASCHINIST: "Maschinist",
MELDER: "Melder",
TRUPPMANN: "Truppmann",
ATEMSCHUTZTRAEGER: "Atemschutzgeräteträger",
SICHERHEITSTRUPP: "Sicherheitstrupp",
SANITAETER: "Sanitäter",
SONSTIGES: "sonstige Funktion",
};
/** Wer als Führer zählt: wer eine taktische Einheit führt. */
const FUEHRER: Funktion[] = [
"EINSATZLEITER",
"ZUGFUEHRER",
"GRUPPENFUEHRER",
"STAFFELFUEHRER",
];
/** Wer als Unterführer zählt: wer innerhalb der Einheit einen Trupp führt. */
const UNTERFUEHRER: Funktion[] = ["TRUPPFUEHRER"];
export interface Staerke {
fuehrer: number;
unterfuehrer: number;
mannschaft: number;
gesamt: number;
}
export const LEERE_STAERKE: Staerke = {
fuehrer: 0,
unterfuehrer: 0,
mannschaft: 0,
gesamt: 0,
};
/** Zählt die Funktionen zur Stärke zusammen. */
export function staerkeBerechnen(funktionen: Funktion[]): Staerke {
const staerke = { ...LEERE_STAERKE };
for (const funktion of funktionen) {
if (FUEHRER.includes(funktion)) staerke.fuehrer += 1;
else if (UNTERFUEHRER.includes(funktion)) staerke.unterfuehrer += 1;
else staerke.mannschaft += 1;
staerke.gesamt += 1;
}
return staerke;
}
/** Die übliche Schreibweise: "1/3/5/9". */
export function staerkeAlsText(staerke: Staerke): string {
return `${staerke.fuehrer}/${staerke.unterfuehrer}/${staerke.mannschaft}/${staerke.gesamt}`;
}
/** Die taktischen Einheiten der FwDV 3, von klein nach groß. */
export const EINHEITEN = [
{ name: "Trupp", staerke: 2 },
{ name: "Staffel", staerke: 6 },
{ name: "Gruppe", staerke: 9 },
{ name: "Zug", staerke: 18 },
] as const;
/**
* Welcher taktischen Einheit die Mannschaft entspricht.
*
* Gibt die größte Einheit zurück, die vollständig besetzt ist — und dazu, wie
* viele darüber hinaus mitgefahren sind. Eine Wehr rückt selten in exakter
* Sollstärke aus; die Angabe soll trotzdem etwas sagen.
*/
export function taktischeEinheit(staerke: Staerke): {
einheit: string | null;
ueberzaehlig: number;
} {
if (staerke.gesamt === 0) return { einheit: null, ueberzaehlig: 0 };
let gefunden: (typeof EINHEITEN)[number] | null = null;
for (const einheit of EINHEITEN) {
if (staerke.gesamt >= einheit.staerke) gefunden = einheit;
}
if (!gefunden) {
return { einheit: null, ueberzaehlig: staerke.gesamt };
}
return { einheit: gefunden.name, ueberzaehlig: staerke.gesamt - gefunden.staerke };
}
/**
* Fehlt eine Führungskraft?
*
* Ohne Führer keine taktische Einheit — das ist keine Formsache, sondern der
* Unterschied zwischen einer Einheit und einer Ansammlung von Leuten. Die
* Oberfläche weist darauf hin; verhindert wird das Speichern nicht, denn beim
* Nachtragen eines Einsatzes von vor drei Wochen weiß man es manchmal nicht
* mehr genau.
*/
export function staerkeHinweise(staerke: Staerke): string[] {
const hinweise: string[] = [];
if (staerke.gesamt === 0) {
hinweise.push("Zu diesem Einsatz ist noch niemand erfasst.");
return hinweise;
}
if (staerke.fuehrer === 0) {
hinweise.push("Es ist keine Führungskraft erfasst — wer hat den Einsatz geführt?");
}
if (staerke.gesamt < 2) {
hinweise.push("Ein Trupp besteht aus zwei Kräften. Alleine geht niemand vor.");
}
return hinweise;
}