/* =====================================================================
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
? `Leitstelle übernehmen ` : ""}
${d.rechte.erstellen
? `+ Einsatz anlegen ` : ""}
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
Aktuelle Einsätze
Archiv
${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
? `Abrücken `
: `Übernehmen `) : ""}
Details
`;
}
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) : ""}
← Leitstelle
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 ? `
◎ Wegpunkt ` : ""}
${e.status !== "abgeschlossen" ? (d.ichDabei
? `Abrücken `
: `Übernehmen `) : ""}
${e.status !== "abgeschlossen" && d.rechte.zuweisen
? `Priorität ` : ""}
${e.status !== "abgeschlossen" && d.rechte.schliessen
? `Abschließen ` : ""}
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
+ Notiz
${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: `Was ist passiert?
`,
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])}
Neue Priorität
1 — Sofort
2 — Dringend
3 — Regulär
4 — Nachrangig
Begründung (Pflicht)
`,
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.
` : ""}
Abschlussvermerk (Pflicht)
`,
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: `
Titel
Kategorie
${d.kategorien.map((k) =>
`${sicher(k.name)} `).join("")}
Priorität
1 — Sofort
2 — Dringend
3 — Regulär
4 — Nachrangig
Ort
Beschreibung
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 []; }
});
})();