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
+77
View File
@@ -118,6 +118,26 @@ const AUFNAHMEN: Aufnahme[] = [
beschreibung: "Abgeschlossene Prüfprotokolle",
angemeldet: true,
},
// Phase 3 — Einsätze und Übungen
{
name: "19-einsaetze",
pfad: "/einsaetze",
beschreibung: "Einsatzliste mit Schnellerfassung",
angemeldet: true,
},
{
name: "20-statistik",
pfad: "/einsaetze/statistik",
beschreibung: "Einsatzstatistik mit Hilfsfristen und Beteiligung",
angemeldet: true,
},
{
name: "21-uebungen",
pfad: "/uebungen",
beschreibung: "Übungen mit Thema und Teilnehmerzahl",
angemeldet: true,
},
];
/**
@@ -163,6 +183,18 @@ const DYNAMISCHE_AUFNAHMEN: Array<{
// Nur fertige Protokolle: die tragen eine Nummer, Entwürfe nicht.
textMuster: /^\d{4}-\d{4}$/,
},
{
name: "22-einsatz",
beschreibung: "Einsatzdetail mit Zeitleiste, Stärke und Verlauf",
von: "/einsaetze",
linkMuster: /^\/einsaetze\/[0-9a-f-]{36}$/,
},
{
name: "23-uebung",
beschreibung: "Übung mit Anwesenheitsliste",
von: "/uebungen",
linkMuster: /^\/uebungen\/[0-9a-f-]{36}$/,
},
];
/**
@@ -201,6 +233,49 @@ async function anmelden(seite: Page): Promise<void> {
await seite.waitForLoadState("networkidle");
}
/**
* Das Einsatzportal lässt sich nicht über eine feste Adresse aufrufen — der
* Link entsteht erst, wenn ihn jemand erzeugt. Also erzeugt ihn dieses Skript
* selbst, im ersten Einsatz der Liste, und fotografiert das Ergebnis.
*/
async function portalAufnehmen(seite: Page, darstellung: string): Promise<void> {
try {
await seite.goto(`${BASIS}/einsaetze`, { waitUntil: "networkidle" });
await fertigGeladen(seite);
const ziele = await seite
.locator("a[href]")
.evaluateAll((elemente) =>
elemente.map((element) => element.getAttribute("href") ?? "").filter(Boolean),
);
const einsatz = ziele.find((href) => /^\/einsaetze\/[0-9a-f-]{36}$/.test(href));
if (!einsatz) {
console.warn("[screenshots] Kein Einsatz für das Portal gefunden — übersprungen.");
return;
}
await seite.goto(`${BASIS}${einsatz}`, { waitUntil: "networkidle" });
await fertigGeladen(seite);
await seite.getByLabel("Für wen ist der Link?").fill("FF Nachbarhausen");
await seite.getByRole("button", { name: "Erzeugen" }).click();
const linkfeld = seite.locator("input[readonly]").first();
await linkfeld.waitFor({ timeout: 15_000 });
const adresse = await linkfeld.inputValue();
await seite.goto(adresse, { waitUntil: "networkidle" });
await fertigGeladen(seite);
const datei = path.join(ZIEL, `24-portal-${darstellung}.png`);
await seite.screenshot({ path: datei, fullPage: true });
console.info(`[screenshots] Einsatzportal für Fremdwehren (${darstellung}) -> ${datei}`);
} catch (fehler) {
console.warn("[screenshots] Portalaufnahme fehlgeschlagen:", fehler);
}
}
async function main(): Promise<void> {
await mkdir(ZIEL, { recursive: true });
@@ -266,6 +341,8 @@ async function main(): Promise<void> {
console.info(`[screenshots] ${aufnahme.beschreibung} (${darstellung}) -> ${datei}`);
}
await portalAufnehmen(seite, darstellung);
await kontext.close();
}