/* =====================================================================
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
? `
+ Operation 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.
`}
OP-Plan${d.zaehler.geplant + d.zaehler.laufend}
Laufend${d.zaehler.laufend}
Archiv
${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) : ""}
← OP-Plan
${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")}
Patientenakte
${geplant && d.rechte.dokumentieren
? `Operation beginnen ` : ""}
${laufend && d.rechte.dokumentieren
? `Operation beenden ` : ""}
${geplant && d.rechte.planen
? `Umplanen ` : ""}
${!beendet && d.rechte.dokumentieren
? `
${d.ichDabei ? "Team verlassen" : "Ins Team eintreten"} ` : ""}
${!beendet && d.rechte.planen
? `Absagen ` : ""}
OP-Team
${!beendet && d.rechte.planen
? `+ Mitglied ` : ""}
${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 ? `
OP-Bericht speichern
` : ""}
`;
verdrahteEinzel(behaelter, d);
}
/** Ein Feld des Operationsberichts */
function bogenFeld(name, titel, wert, schreibbar, hinweis) {
if (!schreibbar) {
return `
${sicher(titel)}
${wert
? sicher(wert).replace(/\n/g, " ") : "Nicht dokumentiert."}
`;
}
return `
${sicher(titel)}
${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.
Begründung (Pflicht)
`,
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: `
Kollege im Dienst
${frei.length ? frei.map((k) => `
${sicher(k.personalnummer)}
${sicher(k.name)}
${sicher(k.rolleName)}
`).join("")
: `
Niemand weiter im Dienst.
`}
Funktion im Team
${d.funktionen.map((f) =>
`${sicher(f.name)} `).join("")}
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: `
Patient suchen
Mindestens zwei Zeichen eingeben.
`,
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 =
`— später festlegen — ` +
saele.map((s) =>
`
${sicher(s.kennung)}${s.belegt > 0 ? " (gerade belegt)" : ""}
${s.status === "gesperrt" ? " — gesperrt" : ""} `).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)}
OP-Saal
— kein Saal —
${d.saele.map((s) =>
`
${sicher(s.kennung)}${s.status === "gesperrt" ? " — gesperrt" : ""}
`).join("")}
Begründung (Pflicht)
`,
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 []; }
});
})();