/* ===================================================================== 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 = `

Leitstelle

Notrufe erzeugen offene Einsätze. Wer hinfährt, entscheidet die Leitstelle — und die angefragte Kraft selbst.

${d.rechte.leitstelle && !d.istLeitstelle ? `` : ""} ${d.rechte.erstellen ? `` : ""}
Offen
${d.zaehler.offen}
noch niemand gebunden
Laufend
${d.zaehler.laufend}
Kräfte im Einsatz
Kräfte im Dienst
${d.kraefte.length}
${d.kraefte.filter((k) => !k.gebundenAn).length} ungebunden
Leitstelle
${sicher(d.leitstelleName || "unbesetzt")}
${d.zaehler.heute} Einsätze heute beendet
${z.ordner === "archiv" ? "Abgeschlossene Einsätze" : "Einsätze"} ${d.rechte.zuweisen && z.ordner === "aktuell" ? "Kraft von rechts auf einen Einsatz ziehen" : d.einsaetze.length + " Einträge"}
${d.einsaetze.length ? d.einsaetze.map(einsatzKarte).join("") : `
${z.ordner === "archiv" ? "Das Archiv ist leer." : "Keine offenen Einsätze."}
`}
Kräfte im Dienst ${d.rechte.zuweisen ? "ziehbar" : "Übersicht"}
${d.kraefte.length ? d.kraefte.map((k) => kraftZeile(k, d)).join("") : `
Niemand im Dienst.
`}
`; verdrahteUebersicht(behaelter); } function einsatzKarte(e) { const abgeschlossen = e.status === "abgeschlossen"; return `
${sicher(e.nummer)} ${sicher(e.status)} ${e.ichDabei ? `du bist dabei` : ""} ${abgeschlossen ? zeit(e.geschlossen_am) : "offen seit " + minuten(e.offenSeit)}
${sicher(e.titel)}
${sicher(e.kategorieName)} · ${sicher(prioText[e.prioritaet] || "")} ${e.ort ? " · " + sicher(e.ort) : ""} ${e.quelle === "notruf" ? ` · Notruf` : ""}
${e.kraefte} Kraft/Kräfte gebunden · angelegt von ${sicher(e.erstelltVon)}
${e.pos_x ? `` : ""} ${!abgeschlossen ? (e.ichDabei ? `` : ``) : ""}
`; } 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 `
${sicher(k.personalnummer)}
${sicher(k.name)}
${sicher(k.rolleName)} · ${sicher(funktion)}
${k.leitstelle ? `Leitstelle` : ""} ${k.gebundenAn ? ` ${sicher(k.gebundenAn)}` : `frei`}
`; } 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: `

Du übernimmst die Disposition. Damit fragst du Kräfte für Einsätze an — verpflichten kannst du niemanden.

`, 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: `

${sicher(kraft.name)} (${sicher(kraft.personalnummer)}) soll ${sicher(einsatz.nummer)} übernehmen:

${sicher(einsatz.titel)}${einsatz.ort ? " — " + sicher(einsatz.ort) : ""}

${kraft.gebundenAn ? `
⚠
Diese Kraft ist bereits an ${sicher(kraft.gebundenAn)} gebunden.
` : ""}
ℹ
Das ist eine Anfrage, keine Zuweisung. Die Kraft bekommt den Einsatz mit Wegpunkt angezeigt und entscheidet selbst, ob sie übernimmt.
`, 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 = `

${sicher(e.nummer)} · ${sicher(e.titel)}

${sicher(e.kategorieName)} · ${sicher(prioText[e.prioritaet] || "")} ${e.ort ? " · " + sicher(e.ort) : ""}

Einsatz ${sicher(e.status)}
Angelegt${zeit(e.erstelltAm)}
Von${sicher(e.erstelltVon)}
Quelle${sicher(e.quelle)}
${e.geschlossenAm ? `
Abgeschlossen${zeit(e.geschlossenAm)}
Durch${sicher(e.geschlossenVon)}
` : ""}
${e.beschreibung ? `
${sicher(e.beschreibung).replace(/\n/g, "
")}
` : ""}
${e.position ? `` : ""} ${e.status !== "abgeschlossen" ? (d.ichDabei ? `` : ``) : ""} ${e.status !== "abgeschlossen" && d.rechte.zuweisen ? `` : ""} ${e.status !== "abgeschlossen" && d.rechte.schliessen ? `` : ""}
Beteiligte Kräfte ${aktiveKraefte.length} gebunden
${d.kraefte.length ? d.kraefte.map((k) => `
${sicher(k.personalnummer || "—")}
${sicher(k.name)}
${sicher(k.rolleName || "")} · seit ${zeit(k.seit)}
${k.aktiv ? "im Einsatz" : "abgerückt"}
`).join("") : `
Noch niemand übernommen.
`}
Einsatzverlauf
${d.notizen.length ? d.notizen.map((n) => `
${sicher(n.text)}
${sicher(n.autor)} · ${zeit(n.am)}
`).join("") : `
Noch keine Einträge im Verlauf.
`}
Dokumentation zu diesem Einsatz Das System legt keine an
${d.behandlungen.length ? d.behandlungen.map((b) => `
${sicher(b.kennung)}
${sicher(b.patientName)}
${sicher(b.diagnose || "Noch keine Diagnose")}
${sicher(b.status)}
`).join("") : `
Noch keine Behandlung dokumentiert.
`}
`; 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: `
`, 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: `

Aktuell: ${sicher(prioText[e.prioritaet])}

`, 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: `

${sicher(e.nummer)} — ${sicher(e.titel)}

Alle noch gebundenen Kräfte werden abgemeldet.

${dokumentiert === 0 ? `
📄
Zu diesem Einsatz ist noch keine Behandlung dokumentiert. Das System legt keine an — du bekommst nur eine Erinnerung. Abschließen ist trotzdem möglich.
` : ""}
`, 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: `
Der Einsatz erscheint für alle als offen. Alarmiert wird niemand gezielt.
`, 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 = `

Leitstelle

Konnte nicht geladen werden: ${sicher(f.message)}

`; } } 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 []; } }); })();