/* ===================================================================== aether-medical / Ansicht: Operationsverwaltung OP-Plan, OP-Säle und Operationsbericht. Der geplante Zeitpunkt ist eine Erinnerung — beginnen und beenden trägt das Team von Hand ein. Überschneidungen im Saal werden angezeigt, aber nicht verhindert. ===================================================================== */ "use strict"; (() => { const { sicher, zeit, minuten, aufruf, meldung, fenster, bestaetigen } = App; const z = { ordner: "plan", // plan | laufend | archiv daten: null, offen: null, }; const statusArt = { geplant: "info", laufend: "warn", abgeschlossen: "gut", abgesagt: "neutral", }; /* ---- Übersicht ----------------------------------------------------------- */ function uebersicht(behaelter) { const d = z.daten; behaelter.innerHTML = `

Operationen

Der OP-Plan erinnert an Termine. Begonnen und beendet wird eine Operation ausschließlich durch Eintrag des OP-Teams.

${d.rechte.planen ? `` : ""}
Geplant
${d.zaehler.geplant}
im OP-Plan
Termin erreicht
${d.zaehler.faellig}
wartet auf Beginn
Laufend
${d.zaehler.laufend}
Eingriffe im Gange
Heute beendet
${d.zaehler.heute}
abgeschlossen
OP-Säle Belegung ergibt sich aus laufenden Eingriffen
${d.saele.length ? d.saele.map(saalKachel).join("") : `
Keine OP-Säle angelegt.
`}
${z.ordner === "archiv" ? "Abgeschlossene und abgesagte Operationen" : "Eingriffe"} ${d.liste.length} Einträge
${d.liste.length ? d.liste.map(zeile).join("") : `
Nichts geplant.
`}
`; verdrahteUebersicht(behaelter); } function saalKachel(s) { const belegt = !!s.laufend; const gesperrt = s.status === "gesperrt"; return `
${sicher(s.kennung)} ${belegt ? "belegt" : gesperrt ? "gesperrt" : "frei"}
${belegt ? `
${sicher(s.laufend.kennung)} · ${sicher(s.laufend.art)}
${sicher(s.laufend.patientName)} · läuft seit ${minuten(s.laufend.dauer)}
` : `
${s.geplant} geplante Eingriffe ${s.hinweis ? " · " + sicher(s.hinweis) : ""}
`}
`; } function zeile(o) { const laufend = o.status === "laufend"; const faellig = o.status === "geplant" && o.restMinuten !== null && o.restMinuten <= 0; return `
${sicher(o.kennung)}
${sicher(o.art)} ${sicher(o.patientName)}
${o.saalName ? "Saal " + sicher(o.saalName) : "kein Saal"} · ${o.teamGroesse} im Team · geplant von ${sicher(o.erstelltVon)}
${o.hinweis ? `⚠` : ""} ${o.allergien ? `A` : ""} ${o.blutgruppe ? `${sicher(o.blutgruppe)}` : ""} ${o.hatKomplikationen ? `Komplikationen` : ""} ${o.ichDabei ? `du bist im Team` : ""} ${faellig ? `Termin erreicht` : ""} ${sicher(o.status)} ${laufend ? "läuft seit " + minuten(o.dauerMinuten) : o.status === "geplant" ? (o.geplant_fuer ? (o.restMinuten > 0 ? "in " + minuten(o.restMinuten) : "seit " + minuten(-o.restMinuten) + " fällig") : "ohne Termin") : zeit(o.ende)}
`; } function verdrahteUebersicht(behaelter) { behaelter.querySelectorAll("[data-reiter]").forEach((el) => { el.addEventListener("click", () => { z.ordner = el.dataset.reiter; rendern(behaelter); }); }); behaelter.querySelectorAll("[data-oeffne]").forEach((el) => { el.addEventListener("click", () => oeffne(behaelter, +el.dataset.oeffne)); }); const neu = behaelter.querySelector("[data-neu]"); if (neu) neu.addEventListener("click", () => planungsFenster(behaelter)); } /* ---- Einzelansicht -------------------------------------------------------- */ function einzelAnsicht(behaelter, d) { const o = d.operation, s = d.stamm; const geplant = o.status === "geplant"; const laufend = o.status === "laufend"; const beendet = o.status === "abgeschlossen" || o.status === "abgesagt"; const schreibbar = !beendet && d.rechte.dokumentieren; behaelter.innerHTML = `

${sicher(o.kennung)} · ${sicher(o.art)}

${sicher(o.patientName)} · geb. ${sicher(o.geburt)} ${o.saalName ? " · Saal " + sicher(o.saalName) : ""}

${s.hinweis ? `
⚠
Warnhinweis — ${sicher(s.hinweis)}
` : ""} ${s.allergien ? `
💊
Allergien — ${sicher(s.allergien)}
` : ""} ${s.implantate ? `
🔩
Implantate — ${sicher(s.implantate)}
` : ""} ${d.ueberschneidungen.length ? `
🕐
Zeitgleich im selben Saal: ${d.ueberschneidungen.map((u) => `${sicher(u.kennung)} (${sicher(u.patientName)})`).join(", ")}
Das System verhindert nichts — vielleicht ist der andere Eingriff früher fertig. Die Entscheidung liegt bei dir.
` : ""}
Status${sicher(o.status)}
Geplant für${o.geplantFuer ? zeit(o.geplantFuer) : "ohne Termin"}
Beginn${o.beginn ? zeit(o.beginn) : "noch nicht begonnen"}
Ende${o.ende ? zeit(o.ende) : "—"}
Geplant von${sicher(o.erstelltVon)}
Blutgruppe ${sicher(s.blutgruppe || "nicht bestimmt")}
${geplant && d.rechte.dokumentieren ? `` : ""} ${laufend && d.rechte.dokumentieren ? `` : ""} ${geplant && d.rechte.planen ? `` : ""} ${!beendet && d.rechte.dokumentieren ? `` : ""} ${!beendet && d.rechte.planen ? `` : ""}
OP-Team ${!beendet && d.rechte.planen ? `` : ""}
${d.team.length ? d.team.map((t) => `
${sicher(t.funktionName)}
${sicher(t.name)}
${sicher(t.personalnummer || "—")} · ${sicher(t.rolleName || "")}
${!beendet && d.rechte.planen ? `` : ""}
`).join("") : `
Noch niemand eingeteilt.
`}
Relevante Vorgeschichte
Vorerkrankungen ${sicher(s.vorerkrankungen || "keine erfasst")}
Dauermedikation ${sicher(s.dauermedikation || "keine erfasst")}
Implantate ${sicher(s.implantate || "keine erfasst")}
${o.aufnahmeGrund ? `
Aufnahmegrund${sicher(o.aufnahmeGrund)}
` : ""}
Fehlende Angaben heißen „nicht erhoben". Das System schließt daraus nichts.
${bogenFeld("verlauf", "Operationsverlauf", o.verlauf, schreibbar, "Zugang, Vorgehen, Befund, Versorgung…")} ${bogenFeld("komplikationen", "Komplikationen", o.komplikationen, schreibbar, "Nur eintragen, wenn welche auftraten.")} ${bogenFeld("nachsorge", "Nachsorge", o.nachsorge, schreibbar, "Drainagen, Verbandwechsel, Kontrolle, Belastung…")}
${schreibbar ? `
` : ""} `; verdrahteEinzel(behaelter, d); } /** Ein Feld des Operationsberichts */ function bogenFeld(name, titel, wert, schreibbar, hinweis) { if (!schreibbar) { return `
${wert ? sicher(wert).replace(/\n/g, "
") : "Nicht dokumentiert."}
`; } return `
${sicher(hinweis || "")}
`; } function verdrahteEinzel(behaelter, d) { const o = d.operation; behaelter.querySelector("[data-zurueck]").addEventListener("click", () => { z.offen = null; rendern(behaelter); }); behaelter.querySelector("[data-akte]").addEventListener("click", () => { if (!App.zustand.module.some((m) => m.id === "mdt")) { return meldung("Das MDT steht dir nicht zur Verfügung.", "fehler"); } App.oeffneModul("mdt", { patient: o.patient }); }); /** Liest die Felder des Berichts aus */ const bericht = () => ({ verlauf: (behaelter.querySelector("[name=verlauf]") || {}).value, komplikationen: (behaelter.querySelector("[name=komplikationen]") || {}).value, nachsorge: (behaelter.querySelector("[name=nachsorge]") || {}).value, }); const speichern = behaelter.querySelector("[data-speichern]"); if (speichern) speichern.addEventListener("click", async () => { try { await aufruf("operation", "dokumentieren", Object.assign({ id: o.id }, bericht())); meldung("OP-Bericht gespeichert.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } }); const beginnen = behaelter.querySelector("[data-beginnen]"); if (beginnen) beginnen.addEventListener("click", () => { bestaetigen({ titel: "Operation beginnen", inhalt: `

${sicher(o.kennung)} — ${sicher(o.art)} bei ${sicher(o.patientName)}.

Der Beginn wird mit der aktuellen Uhrzeit vermerkt und der Saal gilt als belegt. ${o.geplantFuer ? "Der geplante Termin war " + zeit(o.geplantFuer) + "." : ""}

${d.team.length === 0 ? `
👥
Es ist noch niemand im OP-Team. Du wirst mit dem Beginn als Beteiligter eingetragen.
` : ""}`, bestaetigenText: "Beginn eintragen", bestaetigt: async (_, zu) => { try { await aufruf("operation", "beginnen", { id: o.id }); zu(); meldung("Beginn eingetragen.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } }, }); }); const beenden = behaelter.querySelector("[data-beenden]"); if (beenden) beenden.addEventListener("click", () => { const aktuell = bericht(); bestaetigen({ titel: "Operation beenden", inhalt: `

${sicher(o.kennung)} wird abgeschlossen. Der Bericht ist danach schreibgeschützt.

${!aktuell.verlauf && !o.verlauf ? `
📄
Der Operationsverlauf ist leer. Ohne Verlauf lässt sich nicht abschließen — bitte zuerst dokumentieren.
` : ""}

Was gerade im Bogen steht, wird mit übernommen.

`, bestaetigenText: "Beenden", gefahr: true, bestaetigt: async (_, zu) => { try { await aufruf("operation", "beenden", Object.assign({ id: o.id }, aktuell)); zu(); meldung("Operation abgeschlossen.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } }, }); }); const umplanen = behaelter.querySelector("[data-umplanen]"); if (umplanen) umplanen.addEventListener("click", () => umplanungsFenster(behaelter, d)); const absagen = behaelter.querySelector("[data-absagen]"); if (absagen) absagen.addEventListener("click", () => { bestaetigen({ titel: "Operation absagen", gefahr: true, inhalt: `

${sicher(o.kennung)} — ${sicher(o.art)}

Die Operation wird als abgesagt abgelegt. Die Begründung wandert in den Verlauf und bleibt dauerhaft lesbar.

`, bestaetigenText: "Absagen", bestaetigt: async (el, zu) => { const grund = el.querySelector("[name=grund]").value.trim(); if (grund.length < 5) return meldung("Bitte eine Begründung angeben.", "fehler"); try { await aufruf("operation", "absagen", { id: o.id, grund }); zu(); meldung("Operation abgesagt.", "gut"); z.offen = null; rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, }); }); const team = behaelter.querySelector("[data-team]"); if (team) team.addEventListener("click", async () => { try { const antwort = await aufruf("operation", "teamWechsel", { id: o.id, beitreten: !d.ichDabei, funktion: "assistenz", }); meldung(antwort.dabei ? "Du bist im OP-Team." : "Team verlassen.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } }); const teamZu = behaelter.querySelector("[data-teamzu]"); if (teamZu) teamZu.addEventListener("click", () => teamFenster(behaelter, d)); behaelter.querySelectorAll("[data-teamweg]").forEach((el) => { el.addEventListener("click", async () => { try { await aufruf("operation", "teamEntfernen", { id: o.id, charakterId: +el.dataset.teamweg, }); meldung("Aus dem Team genommen.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } }); }); } /* ---- Team ergänzen --------------------------------------------------------- */ async function teamFenster(behaelter, d) { const o = d.operation; let auswahl; try { auswahl = await aufruf("operation", "kollegen", {}); } catch (f) { return meldung(f.message, "fehler"); } const imTeam = new Set(d.team.map((t) => t.character)); const frei = (auswahl.kollegen || []).filter((k) => !imTeam.has(k.charakterId)); let gewaehlt = null; fenster({ titel: "OP-Team ergänzen", inhalt: `
${frei.length ? frei.map((k) => `
${sicher(k.personalnummer)}
${sicher(k.name)}
${sicher(k.rolleName)}
`).join("") : `
Niemand weiter im Dienst.
`}
Die Einteilung ist eine Zuordnung im Bericht — teilnehmen muss der Kollege trotzdem selbst.
`, knoepfe: [ { text: "Abbrechen", art: "leise", klick: (_, zu) => zu() }, { text: "Einteilen", klick: async (el, zu) => { if (!gewaehlt) return meldung("Bitte einen Kollegen wählen.", "fehler"); try { await aufruf("operation", "teamSetzen", { id: o.id, charakterId: gewaehlt, funktion: el.querySelector("[name=funktion]").value, }); zu(); meldung("Ins Team eingeteilt.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } } }, ], nachOeffnen: (hg) => { hg.querySelectorAll("[data-kollege]").forEach((el) => { el.addEventListener("click", () => { gewaehlt = +el.dataset.kollege; hg.querySelectorAll("[data-kollege]") .forEach((k) => k.classList.remove("gewaehlt")); el.classList.add("gewaehlt"); }); }); }, }); } /* ---- Planen und Umplanen ---------------------------------------------------- */ function planungsFenster(behaelter) { let gewaehlt = null; let saele = []; bestaetigen({ titel: "Operation planen", inhalt: `
Mindestens zwei Zeichen eingeben.
0 = ohne Termin
ℹ
Der Termin ist eine Erinnerung. Zum geplanten Zeitpunkt passiert nichts von selbst — das System meldet sich, beginnen musst du.
`, bestaetigenText: "Planen", bestaetigt: async (el, zu) => { if (!gewaehlt) return meldung("Bitte zuerst einen Patienten wählen.", "fehler"); const saalFeld = el.querySelector("[name=saal]"); try { const antwort = await aufruf("operation", "planen", { patient: gewaehlt.id, aufnahme: gewaehlt.aufnahme || null, art: el.querySelector("[name=art]").value.trim(), saal: saalFeld.value ? +saalFeld.value : null, vorlaufMinuten: +el.querySelector("[name=vorlauf]").value, selbstOperateur: el.querySelector("[name=operateur]").checked, }); zu(); meldung("Operation " + antwort.kennung + " geplant.", "gut"); oeffne(behaelter, antwort.id); } catch (f) { meldung(f.message, "fehler"); } }, nachOeffnen: (hg) => { const suchfeld = hg.querySelector("[name=suche]"); const liste = hg.querySelector("[data-treffer]"); const formular = hg.querySelector("[data-formular]"); let warten = null; suchfeld.addEventListener("input", () => { clearTimeout(warten); warten = setTimeout(async () => { const begriff = suchfeld.value.trim(); if (begriff.length < 2) { liste.innerHTML = `
Mindestens zwei Zeichen eingeben.
`; return; } try { const antwort = await aufruf("operation", "patientSuche", { begriff }); const treffer = antwort.treffer || []; saele = antwort.saele || []; liste.innerHTML = treffer.length ? treffer.map((t) => `
${sicher(t.firstname)} ${sicher(t.lastname)}
geb. ${sicher(t.dob)} ${t.blutgruppe ? " · " + sicher(t.blutgruppe) : ""}
${t.hinweis ? `⚠` : ""} ${t.allergien ? `A` : ""} ${t.aufnahme ? `im Haus` : ""}
`).join("") : `
Keine Person gefunden.
`; liste.querySelectorAll("[data-person]").forEach((el) => { el.addEventListener("click", () => { gewaehlt = treffer.find((t) => t.id === +el.dataset.person); liste.querySelectorAll("[data-person]") .forEach((zeile) => zeile.classList.remove("gewaehlt")); el.classList.add("gewaehlt"); formular.style.display = ""; hg.querySelector("[name=saal]").innerHTML = `` + saele.map((s) => ``).join(""); }); }); } catch (f) { meldung(f.message, "fehler"); } }, 260); }); }, }); } function umplanungsFenster(behaelter, d) { const o = d.operation; bestaetigen({ titel: "Operation umplanen", inhalt: `

${sicher(o.kennung)} — ${sicher(o.art)}

0 = Termin entfernen
`, bestaetigenText: "Umplanen", bestaetigt: async (el, zu) => { const grund = el.querySelector("[name=grund]").value.trim(); if (grund.length < 3) return meldung("Bitte eine Begründung angeben.", "fehler"); const saalFeld = el.querySelector("[name=saal]"); try { await aufruf("operation", "umplanen", { id: o.id, saal: saalFeld.value ? +saalFeld.value : null, vorlaufMinuten: +el.querySelector("[name=vorlauf]").value, grund, }); zu(); meldung("Umgeplant.", "gut"); oeffne(behaelter, o.id); } catch (f) { meldung(f.message, "fehler"); } }, }); } /* ---- Laden ------------------------------------------------------------------- */ async function oeffne(behaelter, operationId) { try { const d = await aufruf("operation", "oeffnen", { id: operationId }); z.offen = operationId; einzelAnsicht(behaelter, d); } catch (f) { meldung(f.message, "fehler"); } } async function rendern(behaelter) { try { z.daten = await aufruf("operation", "uebersicht", { ordner: z.ordner }); // Sprungziel aus einem Hinweis, z. B. „Operation beginnt in 10 Minuten" const ziel = App.zustand.modulZiel; if (ziel && ziel.operation) { App.zustand.modulZiel = null; return oeffne(behaelter, ziel.operation); } if (z.offen) await oeffne(behaelter, z.offen); else uebersicht(behaelter); } catch (f) { behaelter.innerHTML = `

Operationen

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

`; } } App.registriereAnsicht("operation", { rendern, aktualisieren() { if (!z.offen && App.zustand.aktivesModul === "operation") rendern(App.$("inhalt")); }, }); App.registriereSuche(async (begriff) => { try { const d = await aufruf("operation", "uebersicht", { ordner: "plan" }); const klein = begriff.toLowerCase(); return (d.liste || []) .filter((o) => (o.kennung + " " + o.art + " " + o.patientName) .toLowerCase().includes(klein)) .slice(0, 5) .map((o) => ({ bereich: "Operation", titel: o.kennung + " · " + o.art, unter: o.patientName + " · " + o.status, })); } catch (e) { return []; } }); })();