- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
642 lines
26 KiB
JavaScript
642 lines
26 KiB
JavaScript
/* =====================================================================
|
||
aether-medical / Ansicht: Leitstelle und Einsatzverwaltung
|
||
|
||
Links die Einsätze, rechts die Kräfte. Eine Kraft lässt sich auf
|
||
einen Einsatz ziehen — das schickt eine ANFRAGE, keine Zuweisung.
|
||
Ob sie den Einsatz übernimmt, entscheidet sie selbst.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
(() => {
|
||
const { sicher, zeit, minuten, aufruf, meldung, fenster, bestaetigen, anClient } = App;
|
||
|
||
const z = {
|
||
ordner: "aktuell", // aktuell | archiv
|
||
daten: null,
|
||
offen: null,
|
||
};
|
||
|
||
const prioText = { 1: "Priorität 1", 2: "Priorität 2", 3: "Priorität 3", 4: "Priorität 4" };
|
||
|
||
/* ---- Leitstelle --------------------------------------------------------- */
|
||
|
||
function uebersicht(behaelter) {
|
||
const d = z.daten;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Leitstelle</h2>
|
||
<p>Notrufe erzeugen offene Einsätze. Wer hinfährt, entscheidet die
|
||
Leitstelle — und die angefragte Kraft selbst.</p>
|
||
</div>
|
||
<div class="ls-kopf-aktionen">
|
||
${d.rechte.leitstelle && !d.istLeitstelle
|
||
? `<button class="knopf leise" data-uebernehmen="1">Leitstelle übernehmen</button>` : ""}
|
||
${d.rechte.erstellen
|
||
? `<button class="knopf" data-neu="1">+ Einsatz anlegen</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="raster k4">
|
||
<div class="kennzahl ${d.zaehler.offen > 0 ? "warn" : ""}">
|
||
<div class="kz-titel">Offen</div>
|
||
<div class="kz-wert">${d.zaehler.offen}</div>
|
||
<div class="kz-zusatz">noch niemand gebunden</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Laufend</div>
|
||
<div class="kz-wert">${d.zaehler.laufend}</div>
|
||
<div class="kz-zusatz">Kräfte im Einsatz</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Kräfte im Dienst</div>
|
||
<div class="kz-wert">${d.kraefte.length}</div>
|
||
<div class="kz-zusatz">${d.kraefte.filter((k) => !k.gebundenAn).length} ungebunden</div>
|
||
</div>
|
||
<div class="kennzahl ${d.leitstelleName ? "" : "warn"}">
|
||
<div class="kz-titel">Leitstelle</div>
|
||
<div class="kz-wert" style="font-size:16px">
|
||
${sicher(d.leitstelleName || "unbesetzt")}</div>
|
||
<div class="kz-zusatz">${d.zaehler.heute} Einsätze heute beendet</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reiter">
|
||
<button class="${z.ordner === "aktuell" ? "aktiv" : ""}" data-reiter="aktuell">
|
||
Aktuelle Einsätze</button>
|
||
<button class="${z.ordner === "archiv" ? "aktiv" : ""}" data-reiter="archiv">
|
||
Archiv</button>
|
||
</div>
|
||
|
||
<div class="ls-raster">
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">${z.ordner === "archiv"
|
||
? "Abgeschlossene Einsätze" : "Einsätze"}</span>
|
||
<span class="feld-hinweis">${d.rechte.zuweisen && z.ordner === "aktuell"
|
||
? "Kraft von rechts auf einen Einsatz ziehen"
|
||
: d.einsaetze.length + " Einträge"}</span>
|
||
</div>
|
||
<div class="ls-liste">
|
||
${d.einsaetze.length ? d.einsaetze.map(einsatzKarte).join("")
|
||
: `<div class="liste-leer">${z.ordner === "archiv"
|
||
? "Das Archiv ist leer." : "Keine offenen Einsätze."}</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Kräfte im Dienst</span>
|
||
<span class="feld-hinweis">${d.rechte.zuweisen ? "ziehbar" : "Übersicht"}</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.kraefte.length ? d.kraefte.map((k) => kraftZeile(k, d)).join("")
|
||
: `<div class="liste-leer">Niemand im Dienst.</div>`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteUebersicht(behaelter);
|
||
}
|
||
|
||
function einsatzKarte(e) {
|
||
const abgeschlossen = e.status === "abgeschlossen";
|
||
|
||
return `<div class="ls-einsatz prio-${sicher(e.prioritaet)} ${e.ichDabei ? "meiner" : ""}"
|
||
data-einsatz="${e.id}" data-ziel="1">
|
||
<div class="ls-e-kopf">
|
||
<span class="ls-e-nummer">${sicher(e.nummer)}</span>
|
||
<span class="marker ${abgeschlossen ? "neutral"
|
||
: e.status === "offen" ? "warn" : "info"}">${sicher(e.status)}</span>
|
||
${e.ichDabei ? `<span class="marker gut">du bist dabei</span>` : ""}
|
||
<span class="db-zeit">${abgeschlossen
|
||
? zeit(e.geschlossen_am) : "offen seit " + minuten(e.offenSeit)}</span>
|
||
</div>
|
||
|
||
<div class="ls-e-titel">${sicher(e.titel)}</div>
|
||
<div class="ls-e-unter">
|
||
<span class="ls-e-kategorie" ${e.farbe ? `style="color:${sicher(e.farbe)}"` : ""}>
|
||
${sicher(e.kategorieName)}</span>
|
||
· ${sicher(prioText[e.prioritaet] || "")}
|
||
${e.ort ? " · " + sicher(e.ort) : ""}
|
||
${e.quelle === "notruf" ? ` · <span class="marker kritisch">Notruf</span>` : ""}
|
||
</div>
|
||
|
||
<div class="ls-e-fuss">
|
||
<span class="feld-hinweis">${e.kraefte} Kraft/Kräfte gebunden
|
||
· angelegt von ${sicher(e.erstelltVon)}</span>
|
||
<div class="ls-e-aktionen">
|
||
${e.pos_x ? `<button class="knopf leise klein" data-wegpunkt="${e.id}"
|
||
title="Wegpunkt setzen">◎</button>` : ""}
|
||
${!abgeschlossen ? (e.ichDabei
|
||
? `<button class="knopf leise klein" data-verlassen="${e.id}">Abrücken</button>`
|
||
: `<button class="knopf klein" data-beitreten="${e.id}">Übernehmen</button>`) : ""}
|
||
<button class="knopf leise klein" data-oeffnen="${e.id}">Details</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function kraftZeile(k, d) {
|
||
const farbe = (d.status.find((s) => s.id === k.status) || {}).farbe || "var(--flaeche-3)";
|
||
const funktion = (d.funktionen.find((f) => f.id === k.funktion) || {}).name || k.funktion;
|
||
|
||
return `<div class="liste-zeile ls-kraft" data-kraft="${k.quelle}"
|
||
${d.rechte.zuweisen && z.ordner === "aktuell" ? 'draggable="true"' : ""}>
|
||
<span class="db-dienstpunkt" style="background:${sicher(farbe)}"
|
||
title="${sicher(k.status)}"></span>
|
||
<span class="db-nummer">${sicher(k.personalnummer)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(k.name)}</div>
|
||
<div class="lz-unter">${sicher(k.rolleName)} · ${sicher(funktion)}</div>
|
||
</div>
|
||
${k.leitstelle ? `<span class="marker info">Leitstelle</span>` : ""}
|
||
${k.gebundenAn
|
||
? `<span class="marker warn" title="Gebunden an einen Einsatz">
|
||
${sicher(k.gebundenAn)}</span>`
|
||
: `<span class="marker gut">frei</span>`}
|
||
</div>`;
|
||
}
|
||
|
||
function verdrahteUebersicht(behaelter) {
|
||
const d = z.daten;
|
||
|
||
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
z.ordner = el.dataset.reiter;
|
||
rendern(behaelter);
|
||
});
|
||
});
|
||
|
||
const neu = behaelter.querySelector("[data-neu]");
|
||
if (neu) neu.addEventListener("click", () => erstellenFenster(behaelter));
|
||
|
||
const uebernehmen = behaelter.querySelector("[data-uebernehmen]");
|
||
if (uebernehmen) uebernehmen.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Leitstelle übernehmen",
|
||
inhalt: `<p>Du übernimmst die Disposition. Damit fragst du Kräfte für
|
||
Einsätze an — verpflichten kannst du niemanden.</p>`,
|
||
bestaetigenText: "Übernehmen",
|
||
bestaetigt: (_, zu) => {
|
||
zu();
|
||
anClient("leitstelle", {});
|
||
setTimeout(() => rendern(behaelter), 400);
|
||
},
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-oeffnen]").forEach((el) => {
|
||
el.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
oeffne(behaelter, +el.dataset.oeffnen);
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-beitreten]").forEach((el) => {
|
||
el.addEventListener("click", async (e) => {
|
||
e.stopPropagation();
|
||
try {
|
||
await aufruf("einsatz", "beitreten", { id: +el.dataset.beitreten });
|
||
meldung("Einsatz übernommen.", "gut");
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-verlassen]").forEach((el) => {
|
||
el.addEventListener("click", async (e) => {
|
||
e.stopPropagation();
|
||
try {
|
||
await aufruf("einsatz", "verlassen", { id: +el.dataset.verlassen });
|
||
meldung("Vom Einsatz abgerückt.", "gut");
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-wegpunkt]").forEach((el) => {
|
||
el.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
const einsatz = d.einsaetze.find((x) => x.id === +el.dataset.wegpunkt);
|
||
if (einsatz && einsatz.pos_x) {
|
||
anClient("wegpunkt", { x: einsatz.pos_x, y: einsatz.pos_y });
|
||
meldung("Wegpunkt gesetzt.");
|
||
}
|
||
});
|
||
});
|
||
|
||
if (d.rechte.zuweisen && z.ordner === "aktuell") verdrahteDisposition(behaelter);
|
||
}
|
||
|
||
/* ---- Disposition per Ziehen ---------------------------------------------- */
|
||
|
||
function verdrahteDisposition(behaelter) {
|
||
let gezogeneQuelle = null;
|
||
|
||
behaelter.querySelectorAll("[data-kraft]").forEach((el) => {
|
||
el.addEventListener("dragstart", (e) => {
|
||
gezogeneQuelle = +el.dataset.kraft;
|
||
el.classList.add("zieht");
|
||
// Ohne Nutzdaten lässt Firefox das Ziehen nicht zu
|
||
e.dataTransfer.setData("text/plain", String(gezogeneQuelle));
|
||
e.dataTransfer.effectAllowed = "link";
|
||
});
|
||
|
||
el.addEventListener("dragend", () => {
|
||
gezogeneQuelle = null;
|
||
el.classList.remove("zieht");
|
||
behaelter.querySelectorAll("[data-ziel]")
|
||
.forEach((ziel) => ziel.classList.remove("uebergabe"));
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-ziel]").forEach((el) => {
|
||
el.addEventListener("dragover", (e) => {
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = "link";
|
||
el.classList.add("uebergabe");
|
||
});
|
||
|
||
el.addEventListener("dragleave", () => el.classList.remove("uebergabe"));
|
||
|
||
el.addEventListener("drop", (e) => {
|
||
e.preventDefault();
|
||
el.classList.remove("uebergabe");
|
||
const quelle = gezogeneQuelle || +e.dataTransfer.getData("text/plain");
|
||
if (quelle) anfrageFenster(behaelter, +el.dataset.einsatz, quelle);
|
||
});
|
||
});
|
||
}
|
||
|
||
function anfrageFenster(behaelter, einsatzId, zielQuelle) {
|
||
const einsatz = z.daten.einsaetze.find((e) => e.id === einsatzId);
|
||
const kraft = z.daten.kraefte.find((k) => k.quelle === zielQuelle);
|
||
if (!einsatz || !kraft) return;
|
||
|
||
bestaetigen({
|
||
titel: "Kraft anfragen",
|
||
inhalt: `
|
||
<p><b>${sicher(kraft.name)}</b> (${sicher(kraft.personalnummer)})
|
||
soll <b>${sicher(einsatz.nummer)}</b> übernehmen:</p>
|
||
<p>${sicher(einsatz.titel)}${einsatz.ort ? " — " + sicher(einsatz.ort) : ""}</p>
|
||
${kraft.gebundenAn ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">⚠</span>
|
||
<div>Diese Kraft ist bereits an <b>${sicher(kraft.gebundenAn)}</b> gebunden.</div>
|
||
</div>` : ""}
|
||
<div class="grundsatz">
|
||
<span>ℹ</span>
|
||
<div><b>Das ist eine Anfrage, keine Zuweisung.</b> Die Kraft bekommt den
|
||
Einsatz mit Wegpunkt angezeigt und entscheidet selbst, ob sie übernimmt.</div>
|
||
</div>`,
|
||
bestaetigenText: "Anfragen",
|
||
bestaetigt: async (_, zu) => {
|
||
try {
|
||
const antwort = await aufruf("einsatz", "anfragen", {
|
||
id: einsatzId, quelle: zielQuelle,
|
||
});
|
||
zu();
|
||
meldung("Anfrage an " + antwort.name + " gesendet.", "gut");
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ---- Einzelansicht --------------------------------------------------------- */
|
||
|
||
function einzelAnsicht(behaelter, d) {
|
||
const e = d.einsatz;
|
||
const aktiveKraefte = d.kraefte.filter((k) => k.aktiv);
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>${sicher(e.nummer)} · ${sicher(e.titel)}</h2>
|
||
<p>${sicher(e.kategorieName)} · ${sicher(prioText[e.prioritaet] || "")}
|
||
${e.ort ? " · " + sicher(e.ort) : ""}</p>
|
||
</div>
|
||
<button class="knopf leise" data-zurueck="1">← Leitstelle</button>
|
||
</div>
|
||
|
||
<div class="db-spalten">
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Einsatz</span>
|
||
<span class="marker ${e.status === "offen" ? "warn"
|
||
: e.status === "laufend" ? "info" : "neutral"}">${sicher(e.status)}</span>
|
||
</div>
|
||
<div class="werte-liste">
|
||
<div><span>Angelegt</span><b>${zeit(e.erstelltAm)}</b></div>
|
||
<div><span>Von</span><b>${sicher(e.erstelltVon)}</b></div>
|
||
<div><span>Quelle</span><b>${sicher(e.quelle)}</b></div>
|
||
${e.geschlossenAm ? `
|
||
<div><span>Abgeschlossen</span><b>${zeit(e.geschlossenAm)}</b></div>
|
||
<div><span>Durch</span><b>${sicher(e.geschlossenVon)}</b></div>` : ""}
|
||
</div>
|
||
${e.beschreibung
|
||
? `<div class="karte-fuss">${sicher(e.beschreibung).replace(/\n/g, "<br>")}</div>` : ""}
|
||
|
||
<div class="ls-e-aktionen" style="margin-top:14px">
|
||
${e.position ? `<button class="knopf leise klein" data-wegpunkt="1">
|
||
◎ Wegpunkt</button>` : ""}
|
||
${e.status !== "abgeschlossen" ? (d.ichDabei
|
||
? `<button class="knopf leise klein" data-verlassen="1">Abrücken</button>`
|
||
: `<button class="knopf klein" data-beitreten="1">Übernehmen</button>`) : ""}
|
||
${e.status !== "abgeschlossen" && d.rechte.zuweisen
|
||
? `<button class="knopf leise klein" data-prio="1">Priorität</button>` : ""}
|
||
${e.status !== "abgeschlossen" && d.rechte.schliessen
|
||
? `<button class="knopf gefahr klein" data-schliessen="1">Abschließen</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Beteiligte Kräfte</span>
|
||
<span class="feld-hinweis">${aktiveKraefte.length} gebunden</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.kraefte.length ? d.kraefte.map((k) => `
|
||
<div class="liste-zeile">
|
||
<span class="db-nummer">${sicher(k.personalnummer || "—")}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(k.name)}</div>
|
||
<div class="lz-unter">${sicher(k.rolleName || "")} · seit ${zeit(k.seit)}</div>
|
||
</div>
|
||
<span class="marker ${k.aktiv ? "gut" : "neutral"}">
|
||
${k.aktiv ? "im Einsatz" : "abgerückt"}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch niemand übernommen.</div>`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte" style="margin-top:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Einsatzverlauf</span>
|
||
<button class="knopf klein" data-notiz="1">+ Notiz</button>
|
||
</div>
|
||
<div class="liste">
|
||
${d.notizen.length ? d.notizen.map((n) => `
|
||
<div class="liste-zeile">
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(n.text)}</div>
|
||
<div class="lz-unter">${sicher(n.autor)} · ${zeit(n.am)}</div>
|
||
</div>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch keine Einträge im Verlauf.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte" style="margin-top:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Dokumentation zu diesem Einsatz</span>
|
||
<span class="feld-hinweis">Das System legt keine an</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.behandlungen.length ? d.behandlungen.map((b) => `
|
||
<div class="liste-zeile">
|
||
<span class="db-nummer">${sicher(b.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.patientName)}</div>
|
||
<div class="lz-unter">${sicher(b.diagnose || "Noch keine Diagnose")}</div>
|
||
</div>
|
||
<span class="marker ${b.status === "laufend" ? "warn" : "gut"}">
|
||
${sicher(b.status)}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch keine Behandlung dokumentiert.</div>`}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteEinzel(behaelter, d);
|
||
}
|
||
|
||
function verdrahteEinzel(behaelter, d) {
|
||
const e = d.einsatz;
|
||
|
||
behaelter.querySelector("[data-zurueck]").addEventListener("click", () => {
|
||
z.offen = null;
|
||
rendern(behaelter);
|
||
});
|
||
|
||
const wegpunkt = behaelter.querySelector("[data-wegpunkt]");
|
||
if (wegpunkt) wegpunkt.addEventListener("click", () => {
|
||
anClient("wegpunkt", { x: e.position.x, y: e.position.y });
|
||
meldung("Wegpunkt gesetzt.");
|
||
});
|
||
|
||
const beitreten = behaelter.querySelector("[data-beitreten]");
|
||
if (beitreten) beitreten.addEventListener("click", async () => {
|
||
try {
|
||
await aufruf("einsatz", "beitreten", { id: e.id });
|
||
meldung("Einsatz übernommen.", "gut");
|
||
oeffne(behaelter, e.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
|
||
const verlassen = behaelter.querySelector("[data-verlassen]");
|
||
if (verlassen) verlassen.addEventListener("click", async () => {
|
||
try {
|
||
await aufruf("einsatz", "verlassen", { id: e.id });
|
||
meldung("Abgerückt.", "gut");
|
||
oeffne(behaelter, e.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
|
||
behaelter.querySelector("[data-notiz]").addEventListener("click", () => {
|
||
fenster({
|
||
titel: "Notiz zum Einsatzverlauf",
|
||
inhalt: `<div class="feld"><label>Was ist passiert?</label>
|
||
<textarea name="text" rows="4"
|
||
placeholder="Lagemeldung, Beobachtung, Übergabe…"></textarea></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: "Speichern", klick: async (el, zu) => {
|
||
try {
|
||
await aufruf("einsatz", "notiz", {
|
||
id: e.id, text: el.querySelector("[name=text]").value.trim(),
|
||
});
|
||
zu();
|
||
oeffne(behaelter, e.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
} },
|
||
],
|
||
});
|
||
});
|
||
|
||
const prio = behaelter.querySelector("[data-prio]");
|
||
if (prio) prio.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Priorität ändern",
|
||
inhalt: `
|
||
<p>Aktuell: <b>${sicher(prioText[e.prioritaet])}</b></p>
|
||
<div class="feld"><label>Neue Priorität</label>
|
||
<select name="prioritaet">
|
||
<option value="1" ${e.prioritaet === 1 ? "selected" : ""}>1 — Sofort</option>
|
||
<option value="2" ${e.prioritaet === 2 ? "selected" : ""}>2 — Dringend</option>
|
||
<option value="3" ${e.prioritaet === 3 ? "selected" : ""}>3 — Regulär</option>
|
||
<option value="4" ${e.prioritaet === 4 ? "selected" : ""}>4 — Nachrangig</option>
|
||
</select></div>
|
||
<div class="feld"><label>Begründung (Pflicht)</label>
|
||
<input name="grund" maxlength="255"></div>`,
|
||
bestaetigenText: "Setzen",
|
||
bestaetigt: async (el, zu) => {
|
||
const grund = el.querySelector("[name=grund]").value.trim();
|
||
if (grund.length < 3) return meldung("Bitte eine Begründung angeben.", "fehler");
|
||
try {
|
||
await aufruf("einsatz", "prioritaet", {
|
||
id: e.id,
|
||
prioritaet: +el.querySelector("[name=prioritaet]").value,
|
||
grund,
|
||
});
|
||
zu();
|
||
meldung("Priorität geändert.", "gut");
|
||
oeffne(behaelter, e.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
|
||
const schliessen = behaelter.querySelector("[data-schliessen]");
|
||
if (schliessen) schliessen.addEventListener("click", () => {
|
||
const dokumentiert = d.behandlungen.length;
|
||
|
||
bestaetigen({
|
||
titel: "Einsatz abschließen",
|
||
gefahr: true,
|
||
inhalt: `
|
||
<p><b>${sicher(e.nummer)}</b> — ${sicher(e.titel)}</p>
|
||
<p>Alle noch gebundenen Kräfte werden abgemeldet.</p>
|
||
${dokumentiert === 0 ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">📄</span>
|
||
<div><b>Zu diesem Einsatz ist noch keine Behandlung dokumentiert.</b>
|
||
Das System legt keine an — du bekommst nur eine Erinnerung.
|
||
Abschließen ist trotzdem möglich.</div>
|
||
</div>` : ""}
|
||
<div class="feld"><label>Abschlussvermerk (Pflicht)</label>
|
||
<textarea name="vermerk" rows="4"
|
||
placeholder="Was wurde vorgefunden, was wurde getan, wie endete der Einsatz?"></textarea></div>`,
|
||
bestaetigenText: "Abschließen",
|
||
bestaetigt: async (el, zu) => {
|
||
const vermerk = el.querySelector("[name=vermerk]").value.trim();
|
||
if (vermerk.length < 5) return meldung("Bitte einen Vermerk angeben.", "fehler");
|
||
try {
|
||
await aufruf("einsatz", "schliessen", { id: e.id, vermerk });
|
||
zu();
|
||
meldung("Einsatz abgeschlossen.", "gut");
|
||
z.offen = null;
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Einsatz anlegen --------------------------------------------------------- */
|
||
|
||
function erstellenFenster(behaelter) {
|
||
const d = z.daten;
|
||
|
||
fenster({
|
||
titel: "Einsatz anlegen",
|
||
inhalt: `
|
||
<div class="feld"><label>Titel</label>
|
||
<input name="titel" maxlength="160" placeholder="Kurz und eindeutig"></div>
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Kategorie</label>
|
||
<select name="kategorie">
|
||
${d.kategorien.map((k) =>
|
||
`<option value="${sicher(k.id)}">${sicher(k.name)}</option>`).join("")}
|
||
</select></div>
|
||
<div class="feld"><label>Priorität</label>
|
||
<select name="prioritaet">
|
||
<option value="1">1 — Sofort</option>
|
||
<option value="2">2 — Dringend</option>
|
||
<option value="3" selected>3 — Regulär</option>
|
||
<option value="4">4 — Nachrangig</option>
|
||
</select></div>
|
||
</div>
|
||
<div class="feld"><label>Ort</label>
|
||
<input name="ort" maxlength="160" placeholder="Straße, Gebäude, Bezirk…"></div>
|
||
<div class="feld"><label>Beschreibung</label>
|
||
<textarea name="beschreibung" rows="5"
|
||
placeholder="Was wurde gemeldet?"></textarea>
|
||
<div class="feld-hinweis">Der Einsatz erscheint für alle als <b>offen</b>.
|
||
Alarmiert wird niemand gezielt.</div></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: "Anlegen", klick: async (el, zu) => {
|
||
try {
|
||
await aufruf("einsatz", "erstellen", {
|
||
titel: el.querySelector("[name=titel]").value.trim(),
|
||
kategorie: el.querySelector("[name=kategorie]").value,
|
||
prioritaet: +el.querySelector("[name=prioritaet]").value,
|
||
ort: el.querySelector("[name=ort]").value.trim(),
|
||
beschreibung: el.querySelector("[name=beschreibung]").value.trim(),
|
||
});
|
||
zu();
|
||
meldung("Einsatz angelegt.", "gut");
|
||
z.ordner = "aktuell";
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
} },
|
||
],
|
||
});
|
||
}
|
||
|
||
/* ---- Laden --------------------------------------------------------------------- */
|
||
|
||
async function oeffne(behaelter, einsatzId) {
|
||
try {
|
||
const d = await aufruf("einsatz", "oeffnen", { id: einsatzId });
|
||
z.offen = einsatzId;
|
||
einzelAnsicht(behaelter, d);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}
|
||
|
||
async function rendern(behaelter) {
|
||
try {
|
||
z.daten = await aufruf("einsatz", "uebersicht", { ordner: z.ordner });
|
||
|
||
// Sprungziel aus einem Hinweis, z. B. „Neuer Einsatz"
|
||
const ziel = App.zustand.modulZiel;
|
||
if (ziel && ziel.einsatz) {
|
||
App.zustand.modulZiel = null;
|
||
return oeffne(behaelter, ziel.einsatz);
|
||
}
|
||
|
||
if (z.offen) await oeffne(behaelter, z.offen);
|
||
else uebersicht(behaelter);
|
||
} catch (f) {
|
||
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Leitstelle</h2>
|
||
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
App.registriereAnsicht("einsatz", {
|
||
rendern,
|
||
aktualisieren() {
|
||
if (App.zustand.aktivesModul === "einsatz") rendern(App.$("inhalt"));
|
||
},
|
||
});
|
||
|
||
App.registriereSuche(async (begriff) => {
|
||
try {
|
||
const d = await aufruf("einsatz", "uebersicht", { ordner: "aktuell" });
|
||
const klein = begriff.toLowerCase();
|
||
return (d.einsaetze || [])
|
||
.filter((e) => (e.nummer + " " + e.titel + " " + (e.ort || ""))
|
||
.toLowerCase().includes(klein))
|
||
.slice(0, 5)
|
||
.map((e) => ({
|
||
bereich: "Einsatz", titel: e.nummer + " · " + e.titel,
|
||
unter: e.kategorieName + (e.ort ? " · " + e.ort : ""),
|
||
}));
|
||
} catch (e) { return []; }
|
||
});
|
||
})();
|