- 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>
736 lines
30 KiB
JavaScript
736 lines
30 KiB
JavaScript
/* =====================================================================
|
||
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 = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Operationen</h2>
|
||
<p>Der OP-Plan erinnert an Termine. Begonnen und beendet wird eine
|
||
Operation ausschließlich durch Eintrag des OP-Teams.</p>
|
||
</div>
|
||
${d.rechte.planen
|
||
? `<button class="knopf" data-neu="1">+ Operation planen</button>` : ""}
|
||
</div>
|
||
|
||
<div class="raster k4">
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Geplant</div>
|
||
<div class="kz-wert">${d.zaehler.geplant}</div>
|
||
<div class="kz-zusatz">im OP-Plan</div>
|
||
</div>
|
||
<div class="kennzahl ${d.zaehler.faellig > 0 ? "hoch" : ""}">
|
||
<div class="kz-titel">Termin erreicht</div>
|
||
<div class="kz-wert">${d.zaehler.faellig}</div>
|
||
<div class="kz-zusatz">wartet auf Beginn</div>
|
||
</div>
|
||
<div class="kennzahl ${d.zaehler.laufend > 0 ? "warn" : ""}">
|
||
<div class="kz-titel">Laufend</div>
|
||
<div class="kz-wert">${d.zaehler.laufend}</div>
|
||
<div class="kz-zusatz">Eingriffe im Gange</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Heute beendet</div>
|
||
<div class="kz-wert">${d.zaehler.heute}</div>
|
||
<div class="kz-zusatz">abgeschlossen</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte" style="margin-bottom:16px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">OP-Säle</span>
|
||
<span class="feld-hinweis">Belegung ergibt sich aus laufenden Eingriffen</span>
|
||
</div>
|
||
<div class="op-saele">
|
||
${d.saele.length ? d.saele.map(saalKachel).join("")
|
||
: `<div class="liste-leer">Keine OP-Säle angelegt.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reiter">
|
||
<button class="${z.ordner === "plan" ? "aktiv" : ""}" data-reiter="plan">
|
||
OP-Plan<span class="mdt-zahl">${d.zaehler.geplant + d.zaehler.laufend}</span></button>
|
||
<button class="${z.ordner === "laufend" ? "aktiv" : ""}" data-reiter="laufend">
|
||
Laufend<span class="mdt-zahl">${d.zaehler.laufend}</span></button>
|
||
<button class="${z.ordner === "archiv" ? "aktiv" : ""}" data-reiter="archiv">
|
||
Archiv</button>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">${z.ordner === "archiv"
|
||
? "Abgeschlossene und abgesagte Operationen" : "Eingriffe"}</span>
|
||
<span class="feld-hinweis">${d.liste.length} Einträge</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.liste.length ? d.liste.map(zeile).join("")
|
||
: `<div class="liste-leer">Nichts geplant.</div>`}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteUebersicht(behaelter);
|
||
}
|
||
|
||
function saalKachel(s) {
|
||
const belegt = !!s.laufend;
|
||
const gesperrt = s.status === "gesperrt";
|
||
|
||
return `<div class="op-saal ${belegt ? "belegt" : gesperrt ? "gesperrt" : "frei"}">
|
||
<div class="op-saal-kopf">
|
||
<span class="op-saal-name">${sicher(s.kennung)}</span>
|
||
<span class="marker ${belegt ? "warn" : gesperrt ? "kritisch" : "gut"}">
|
||
${belegt ? "belegt" : gesperrt ? "gesperrt" : "frei"}</span>
|
||
</div>
|
||
${belegt ? `
|
||
<div class="op-saal-inhalt">
|
||
<b>${sicher(s.laufend.kennung)}</b> · ${sicher(s.laufend.art)}
|
||
<div class="feld-hinweis">${sicher(s.laufend.patientName)}
|
||
· läuft seit ${minuten(s.laufend.dauer)}</div>
|
||
</div>`
|
||
: `<div class="op-saal-inhalt">
|
||
<div class="feld-hinweis">${s.geplant} geplante Eingriffe
|
||
${s.hinweis ? " · " + sicher(s.hinweis) : ""}</div>
|
||
</div>`}
|
||
</div>`;
|
||
}
|
||
|
||
function zeile(o) {
|
||
const laufend = o.status === "laufend";
|
||
const faellig = o.status === "geplant" && o.restMinuten !== null && o.restMinuten <= 0;
|
||
|
||
return `<div class="liste-zeile" data-oeffne="${o.id}">
|
||
<span class="db-nummer">${sicher(o.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(o.art)}
|
||
<span class="pat-geburt">${sicher(o.patientName)}</span></div>
|
||
<div class="lz-unter">
|
||
${o.saalName ? "Saal " + sicher(o.saalName) : "kein Saal"}
|
||
· ${o.teamGroesse} im Team
|
||
· geplant von ${sicher(o.erstelltVon)}
|
||
</div>
|
||
</div>
|
||
|
||
${o.hinweis ? `<span class="marker kritisch" title="${sicher(o.hinweis)}">⚠</span>` : ""}
|
||
${o.allergien ? `<span class="marker warn" title="Allergien: ${sicher(o.allergien)}">A</span>` : ""}
|
||
${o.blutgruppe ? `<span class="marker neutral">${sicher(o.blutgruppe)}</span>` : ""}
|
||
${o.hatKomplikationen ? `<span class="marker kritisch">Komplikationen</span>` : ""}
|
||
${o.ichDabei ? `<span class="marker gut">du bist im Team</span>` : ""}
|
||
${faellig ? `<span class="marker warn">Termin erreicht</span>` : ""}
|
||
|
||
<span class="marker ${statusArt[o.status] || "neutral"}">${sicher(o.status)}</span>
|
||
<span class="db-zeit">${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)}</span>
|
||
</div>`;
|
||
}
|
||
|
||
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 = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>${sicher(o.kennung)} · ${sicher(o.art)}</h2>
|
||
<p>${sicher(o.patientName)} · geb. ${sicher(o.geburt)}
|
||
${o.saalName ? " · Saal " + sicher(o.saalName) : ""}</p>
|
||
</div>
|
||
<button class="knopf leise" data-zurueck="1">← OP-Plan</button>
|
||
</div>
|
||
|
||
${s.hinweis ? `
|
||
<div class="warnung kritisch">
|
||
<span class="w-symbol">⚠</span>
|
||
<div><b>Warnhinweis</b> — ${sicher(s.hinweis)}</div>
|
||
</div>` : ""}
|
||
|
||
${s.allergien ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">💊</span>
|
||
<div><b>Allergien</b> — ${sicher(s.allergien)}</div>
|
||
</div>` : ""}
|
||
|
||
${s.implantate ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">🔩</span>
|
||
<div><b>Implantate</b> — ${sicher(s.implantate)}</div>
|
||
</div>` : ""}
|
||
|
||
${d.ueberschneidungen.length ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">🕐</span>
|
||
<div><b>Zeitgleich im selben Saal:</b>
|
||
${d.ueberschneidungen.map((u) =>
|
||
`${sicher(u.kennung)} (${sicher(u.patientName)})`).join(", ")}
|
||
<br><span class="feld-hinweis">Das System verhindert nichts — vielleicht
|
||
ist der andere Eingriff früher fertig. Die Entscheidung liegt bei dir.</span>
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
<div class="beh-kopf">
|
||
<div class="werte-liste">
|
||
<div><span>Status</span><b>${sicher(o.status)}</b></div>
|
||
<div><span>Geplant für</span><b>${o.geplantFuer
|
||
? zeit(o.geplantFuer) : "ohne Termin"}</b></div>
|
||
<div><span>Beginn</span><b>${o.beginn ? zeit(o.beginn) : "noch nicht begonnen"}</b></div>
|
||
<div><span>Ende</span><b>${o.ende ? zeit(o.ende) : "—"}</b></div>
|
||
<div><span>Geplant von</span><b>${sicher(o.erstelltVon)}</b></div>
|
||
<div class="${s.blutgruppe ? "" : "leer"}"><span>Blutgruppe</span>
|
||
<b>${sicher(s.blutgruppe || "nicht bestimmt")}</b></div>
|
||
</div>
|
||
<div class="beh-kopf-aktionen">
|
||
<button class="knopf leise klein" data-akte="1">Patientenakte</button>
|
||
${geplant && d.rechte.dokumentieren
|
||
? `<button class="knopf klein" data-beginnen="1">Operation beginnen</button>` : ""}
|
||
${laufend && d.rechte.dokumentieren
|
||
? `<button class="knopf klein" data-beenden="1">Operation beenden</button>` : ""}
|
||
${geplant && d.rechte.planen
|
||
? `<button class="knopf leise klein" data-umplanen="1">Umplanen</button>` : ""}
|
||
${!beendet && d.rechte.dokumentieren
|
||
? `<button class="knopf leise klein" data-team="1">
|
||
${d.ichDabei ? "Team verlassen" : "Ins Team eintreten"}</button>` : ""}
|
||
${!beendet && d.rechte.planen
|
||
? `<button class="knopf gefahr klein" data-absagen="1">Absagen</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="db-spalten" style="margin-bottom:14px">
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">OP-Team</span>
|
||
${!beendet && d.rechte.planen
|
||
? `<button class="knopf klein" data-teamzu="1">+ Mitglied</button>` : ""}
|
||
</div>
|
||
<div class="liste">
|
||
${d.team.length ? d.team.map((t) => `
|
||
<div class="liste-zeile">
|
||
<span class="marker ${t.funktion === "operateur" ? "gut" : "neutral"}">
|
||
${sicher(t.funktionName)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(t.name)}</div>
|
||
<div class="lz-unter">${sicher(t.personalnummer || "—")}
|
||
· ${sicher(t.rolleName || "")}</div>
|
||
</div>
|
||
${!beendet && d.rechte.planen
|
||
? `<button class="knopf leise klein" data-teamweg="${t.character}"
|
||
title="Aus dem Team nehmen">✕</button>` : ""}
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch niemand eingeteilt.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf"><span class="karte-titel">Relevante Vorgeschichte</span></div>
|
||
<div class="werte-liste">
|
||
<div class="${s.vorerkrankungen ? "" : "leer"}"><span>Vorerkrankungen</span>
|
||
<b>${sicher(s.vorerkrankungen || "keine erfasst")}</b></div>
|
||
<div class="${s.dauermedikation ? "" : "leer"}"><span>Dauermedikation</span>
|
||
<b>${sicher(s.dauermedikation || "keine erfasst")}</b></div>
|
||
<div class="${s.implantate ? "" : "leer"}"><span>Implantate</span>
|
||
<b>${sicher(s.implantate || "keine erfasst")}</b></div>
|
||
${o.aufnahmeGrund
|
||
? `<div><span>Aufnahmegrund</span><b>${sicher(o.aufnahmeGrund)}</b></div>` : ""}
|
||
</div>
|
||
<div class="karte-fuss">
|
||
Fehlende Angaben heißen „nicht erhoben". Das System schließt daraus nichts.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="beh-bogen">
|
||
${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…")}
|
||
</div>
|
||
|
||
${schreibbar ? `
|
||
<div style="margin-top:14px">
|
||
<button class="knopf" data-speichern="1">OP-Bericht speichern</button>
|
||
</div>` : ""}
|
||
`;
|
||
|
||
verdrahteEinzel(behaelter, d);
|
||
}
|
||
|
||
/** Ein Feld des Operationsberichts */
|
||
function bogenFeld(name, titel, wert, schreibbar, hinweis) {
|
||
if (!schreibbar) {
|
||
return `<div class="beh-feld gesperrt">
|
||
<label>${sicher(titel)}</label>
|
||
<div class="beh-text ${wert ? "" : "leer"}">${wert
|
||
? sicher(wert).replace(/\n/g, "<br>") : "Nicht dokumentiert."}</div>
|
||
</div>`;
|
||
}
|
||
return `<div class="beh-feld">
|
||
<label>${sicher(titel)}</label>
|
||
<textarea name="${sicher(name)}" rows="6"
|
||
placeholder="${sicher(hinweis || "")}">${sicher(wert || "")}</textarea>
|
||
<div class="feld-hinweis">${sicher(hinweis || "")}</div>
|
||
</div>`;
|
||
}
|
||
|
||
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: `<p><b>${sicher(o.kennung)}</b> — ${sicher(o.art)}
|
||
bei ${sicher(o.patientName)}.</p>
|
||
<p>Der Beginn wird mit der aktuellen Uhrzeit vermerkt und der Saal gilt
|
||
als belegt. ${o.geplantFuer
|
||
? "Der geplante Termin war " + zeit(o.geplantFuer) + "." : ""}</p>
|
||
${d.team.length === 0 ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">👥</span>
|
||
<div><b>Es ist noch niemand im OP-Team.</b> Du wirst mit dem Beginn
|
||
als Beteiligter eingetragen.</div>
|
||
</div>` : ""}`,
|
||
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: `<p><b>${sicher(o.kennung)}</b> wird abgeschlossen. Der Bericht
|
||
ist danach schreibgeschützt.</p>
|
||
${!aktuell.verlauf && !o.verlauf ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">📄</span>
|
||
<div><b>Der Operationsverlauf ist leer.</b> Ohne Verlauf lässt sich
|
||
nicht abschließen — bitte zuerst dokumentieren.</div>
|
||
</div>` : ""}
|
||
<p class="feld-hinweis">Was gerade im Bogen steht, wird mit übernommen.</p>`,
|
||
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: `<p><b>${sicher(o.kennung)}</b> — ${sicher(o.art)}</p>
|
||
<p>Die Operation wird als abgesagt abgelegt. Die Begründung wandert
|
||
in den Verlauf und bleibt dauerhaft lesbar.</p>
|
||
<div class="feld"><label>Begründung (Pflicht)</label>
|
||
<textarea name="grund" rows="3"
|
||
placeholder="z. B. Patient nicht nüchtern, Zustand verschlechtert"></textarea></div>`,
|
||
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: `
|
||
<div class="feld"><label>Kollege im Dienst</label>
|
||
<div class="liste pat-trefferliste">
|
||
${frei.length ? frei.map((k) => `
|
||
<div class="liste-zeile" data-kollege="${k.charakterId}">
|
||
<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)}</div>
|
||
</div>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Niemand weiter im Dienst.</div>`}
|
||
</div></div>
|
||
<div class="feld"><label>Funktion im Team</label>
|
||
<select name="funktion">
|
||
${d.funktionen.map((f) =>
|
||
`<option value="${sicher(f.id)}">${sicher(f.name)}</option>`).join("")}
|
||
</select>
|
||
<div class="feld-hinweis">Die Einteilung ist eine Zuordnung im Bericht —
|
||
teilnehmen muss der Kollege trotzdem selbst.</div></div>`,
|
||
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: `
|
||
<div class="feld"><label>Patient suchen</label>
|
||
<input name="suche" placeholder="Name oder Geburtsdatum…"></div>
|
||
<div class="liste pat-trefferliste" data-treffer="1">
|
||
<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>
|
||
</div>
|
||
<div data-formular="1" style="display:none">
|
||
<div class="feld"><label>Art des Eingriffs</label>
|
||
<input name="art" maxlength="160"
|
||
placeholder="z. B. Laparotomie, Wundversorgung Oberschenkel"></div>
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>OP-Saal</label>
|
||
<select name="saal"><option value="">— später festlegen —</option></select></div>
|
||
<div class="feld"><label>Beginn in … Minuten</label>
|
||
<input name="vorlauf" type="number" min="0" max="20160" value="30">
|
||
<div class="feld-hinweis">0 = ohne Termin</div></div>
|
||
</div>
|
||
<div class="feld">
|
||
<label><input type="checkbox" name="operateur" class="haken" checked>
|
||
Ich bin der Operateur</label>
|
||
</div>
|
||
<div class="grundsatz">
|
||
<span>ℹ</span>
|
||
<div>Der Termin ist eine <b>Erinnerung</b>. Zum geplanten Zeitpunkt passiert
|
||
nichts von selbst — das System meldet sich, beginnen musst du.</div>
|
||
</div>
|
||
</div>`,
|
||
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 = `<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>`;
|
||
return;
|
||
}
|
||
try {
|
||
const antwort = await aufruf("operation", "patientSuche", { begriff });
|
||
const treffer = antwort.treffer || [];
|
||
saele = antwort.saele || [];
|
||
|
||
liste.innerHTML = treffer.length ? treffer.map((t) => `
|
||
<div class="liste-zeile" data-person="${t.id}">
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(t.firstname)} ${sicher(t.lastname)}</div>
|
||
<div class="lz-unter">geb. ${sicher(t.dob)}
|
||
${t.blutgruppe ? " · " + sicher(t.blutgruppe) : ""}</div>
|
||
</div>
|
||
${t.hinweis ? `<span class="marker kritisch">⚠</span>` : ""}
|
||
${t.allergien ? `<span class="marker warn">A</span>` : ""}
|
||
${t.aufnahme ? `<span class="marker info">im Haus</span>` : ""}
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Person gefunden.</div>`;
|
||
|
||
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 =
|
||
`<option value="">— später festlegen —</option>` +
|
||
saele.map((s) =>
|
||
`<option value="${s.id}" ${s.status === "gesperrt" ? "disabled" : ""}>
|
||
${sicher(s.kennung)}${s.belegt > 0 ? " (gerade belegt)" : ""}
|
||
${s.status === "gesperrt" ? " — gesperrt" : ""}</option>`).join("");
|
||
});
|
||
});
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}, 260);
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
function umplanungsFenster(behaelter, d) {
|
||
const o = d.operation;
|
||
|
||
bestaetigen({
|
||
titel: "Operation umplanen",
|
||
inhalt: `
|
||
<p><b>${sicher(o.kennung)}</b> — ${sicher(o.art)}</p>
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>OP-Saal</label>
|
||
<select name="saal">
|
||
<option value="">— kein Saal —</option>
|
||
${d.saele.map((s) =>
|
||
`<option value="${s.id}" ${s.id === o.saal ? "selected" : ""}
|
||
${s.status === "gesperrt" ? "disabled" : ""}>
|
||
${sicher(s.kennung)}${s.status === "gesperrt" ? " — gesperrt" : ""}
|
||
</option>`).join("")}
|
||
</select></div>
|
||
<div class="feld"><label>Neuer Beginn in … Minuten</label>
|
||
<input name="vorlauf" type="number" min="0" max="20160" value="30">
|
||
<div class="feld-hinweis">0 = Termin entfernen</div></div>
|
||
</div>
|
||
<div class="feld"><label>Begründung (Pflicht)</label>
|
||
<input name="grund" maxlength="255"
|
||
placeholder="Warum wird verschoben?"></div>`,
|
||
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 = `<div class="seiten-kopf"><div><h2>Operationen</h2>
|
||
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
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 []; }
|
||
});
|
||
})();
|