- 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>
731 lines
31 KiB
JavaScript
731 lines
31 KiB
JavaScript
/* =====================================================================
|
||
aether-medical / Ansicht: Behandlungsverwaltung
|
||
|
||
Ein Dokumentationsbogen, mehr nicht. Kein Feld wird vorausgefüllt,
|
||
keine Diagnose vorgeschlagen, kein Symptom ausgewertet. Was hier
|
||
steht, hat ein Mediziner geschrieben.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
(() => {
|
||
const { sicher, zeit, minuten, aufruf, meldung, fenster, bestaetigen } = App;
|
||
|
||
const z = {
|
||
ordner: "laufend", // laufend | meine | abgeschlossen
|
||
suche: "",
|
||
daten: null,
|
||
offen: null,
|
||
offeneDaten: null,
|
||
};
|
||
|
||
/* ---- Übersicht --------------------------------------------------------- */
|
||
|
||
function uebersicht(behaelter) {
|
||
const d = z.daten;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Behandlungen</h2>
|
||
<p>Das System speichert ausschließlich die Dokumentation. Es behandelt
|
||
nicht, es diagnostiziert nicht und es schlägt nichts vor.</p>
|
||
</div>
|
||
${d.rechte.dokumentieren
|
||
? `<button class="knopf" data-neu="1">+ Behandlung dokumentieren</button>` : ""}
|
||
</div>
|
||
|
||
<div class="raster k3">
|
||
<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">noch nicht abgeschlossen</div>
|
||
</div>
|
||
<div class="kennzahl ${d.zaehler.meine > 0 ? "warn" : ""}">
|
||
<div class="kz-titel">Davon von dir</div>
|
||
<div class="kz-wert">${d.zaehler.meine}</div>
|
||
<div class="kz-zusatz">deine offene Dokumentation</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Heute abgeschlossen</div>
|
||
<div class="kz-wert">${d.zaehler.heute}</div>
|
||
<div class="kz-zusatz">in der ganzen Klinik</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reiter">
|
||
<button class="${z.ordner === "laufend" ? "aktiv" : ""}" data-reiter="laufend">
|
||
Laufend<span class="mdt-zahl">${d.zaehler.laufend}</span></button>
|
||
<button class="${z.ordner === "meine" ? "aktiv" : ""}" data-reiter="meine">
|
||
Meine</button>
|
||
<button class="${z.ordner === "abgeschlossen" ? "aktiv" : ""}" data-reiter="abgeschlossen">
|
||
Abgeschlossen</button>
|
||
</div>
|
||
|
||
<div class="filterleiste">
|
||
<input data-suche="1" placeholder="Kennung, Patient oder Diagnose…"
|
||
value="${sicher(z.suche)}">
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Dokumentationen</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">Keine Dokumentation gefunden.</div>`}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteUebersicht(behaelter);
|
||
}
|
||
|
||
function zeile(b) {
|
||
const laufend = b.status === "laufend";
|
||
|
||
return `<div class="liste-zeile" data-oeffne="${b.id}">
|
||
<span class="db-nummer">${sicher(b.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.patientName)}
|
||
<span class="pat-geburt">geb. ${sicher(b.geburt)}</span></div>
|
||
<div class="lz-unter">${sicher(b.diagnose || b.massnahmen
|
||
|| "Noch nichts eingetragen")}</div>
|
||
</div>
|
||
|
||
${b.hinweis ? `<span class="marker kritisch" title="${sicher(b.hinweis)}">⚠</span>` : ""}
|
||
${b.allergien ? `<span class="marker warn" title="Allergien: ${sicher(b.allergien)}">A</span>` : ""}
|
||
${b.gaben > 0 ? `<span class="marker neutral" title="Medikamentengaben">💊 ${b.gaben}</span>` : ""}
|
||
${b.einsatzNummer ? `<span class="marker info">${sicher(b.einsatzNummer)}</span>` : ""}
|
||
${b.eigene ? `<span class="marker neutral">von dir</span>` : ""}
|
||
|
||
<span class="marker ${laufend ? "warn" : "gut"}">${sicher(b.status)}</span>
|
||
<span class="db-zeit">${laufend
|
||
? "offen seit " + minuten(b.offenSeit) : zeit(b.abgeschlossen_am)}</span>
|
||
</div>`;
|
||
}
|
||
|
||
function verdrahteUebersicht(behaelter) {
|
||
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
z.ordner = el.dataset.reiter;
|
||
rendern(behaelter);
|
||
});
|
||
});
|
||
|
||
const suchfeld = behaelter.querySelector("[data-suche]");
|
||
let warten = null;
|
||
suchfeld.addEventListener("input", () => {
|
||
clearTimeout(warten);
|
||
warten = setTimeout(() => {
|
||
z.suche = suchfeld.value.trim();
|
||
rendern(behaelter);
|
||
}, 280);
|
||
});
|
||
|
||
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", () => anlegenFenster(behaelter));
|
||
}
|
||
|
||
/* ---- Dokumentationsbogen ------------------------------------------------- */
|
||
|
||
function einzelAnsicht(behaelter, d) {
|
||
const b = d.behandlung, s = d.stamm;
|
||
const laufend = b.status === "laufend";
|
||
// Die Diagnose darf nur mit eigenem Recht geschrieben werden
|
||
const diagnoseSperre = !d.rechte.diagnose;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>${sicher(b.kennung)} · ${sicher(b.patientName)}</h2>
|
||
<p>geb. ${sicher(b.geburt)}
|
||
${b.einsatzNummer ? " · Einsatz " + sicher(b.einsatzNummer) : ""}
|
||
${b.aufnahmeGrund ? " · Aufnahme: " + sicher(b.aufnahmeGrund) : ""}</p>
|
||
</div>
|
||
<button class="knopf leise" data-zurueck="1">← Übersicht</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)}
|
||
<br><span class="feld-hinweis">Vor jeder Gabe selbst prüfen. Das System
|
||
gleicht Medikamente nicht gegen Allergien ab.</span></div>
|
||
</div>` : ""}
|
||
|
||
<div class="beh-kopf">
|
||
<div class="werte-liste">
|
||
<div><span>Status</span><b>${sicher(b.status)}</b></div>
|
||
<div><span>Angelegt</span><b>${zeit(b.erstelltAm)}</b></div>
|
||
<div><span>Von</span><b>${sicher(b.erstelltVon)}</b></div>
|
||
<div class="${s.blutgruppe ? "" : "leer"}"><span>Blutgruppe</span>
|
||
<b>${sicher(s.blutgruppe || "nicht bestimmt")}</b></div>
|
||
<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>
|
||
${b.abgeschlossenAm ? `
|
||
<div><span>Abgeschlossen</span><b>${zeit(b.abgeschlossenAm)}</b></div>
|
||
<div><span>Durch</span><b>${sicher(b.abgeschlossenVon)}</b></div>` : ""}
|
||
</div>
|
||
<div class="beh-kopf-aktionen">
|
||
<button class="knopf leise klein" data-akte="1">Patientenakte</button>
|
||
${laufend && d.rechte.dokumentieren
|
||
? `<button class="knopf leise klein" data-baustein="1">🧩 Baustein</button>
|
||
<button class="knopf klein" data-speichern="1">Dokumentation speichern</button>` : ""}
|
||
${laufend && d.rechte.abschliessen
|
||
? `<button class="knopf leise klein" data-abschliessen="1">Abschließen</button>` : ""}
|
||
${!laufend && d.rechte.abschliessen
|
||
? `<button class="knopf leise klein" data-oeffnen="1">Wieder öffnen</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="beh-bogen">
|
||
${feld("symptome", "Symptome / Anamnese", b.symptome, laufend && d.rechte.dokumentieren,
|
||
"Was schildert der Patient?")}
|
||
${feld("untersuchung", "Untersuchung", b.untersuchung, laufend && d.rechte.dokumentieren,
|
||
"Was hast du erhoben? Vitalwerte, Befunde am Patienten…")}
|
||
${feld("diagnose", "Diagnose", b.diagnose,
|
||
laufend && d.rechte.dokumentieren && !diagnoseSperre,
|
||
diagnoseSperre ? "Für die Diagnose fehlt dir die Berechtigung."
|
||
: "Deine Einschätzung. Das System stellt keine Diagnose.")}
|
||
${feld("massnahmen", "Maßnahmen / Behandlung", b.massnahmen,
|
||
laufend && d.rechte.dokumentieren,
|
||
"Verbände, Lagerung, Eingriffe, Reanimation…")}
|
||
${feld("empfehlung", "Empfehlung", b.empfehlung, laufend && d.rechte.dokumentieren,
|
||
"Was empfiehlst du dem Patienten?")}
|
||
${feld("nachsorge", "Nachsorge", b.nachsorge, laufend && d.rechte.dokumentieren,
|
||
"Kontrolltermin, Wundversorgung, Medikamentenplan…")}
|
||
</div>
|
||
|
||
<div class="karte" style="margin-top:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Verabreichte Medikamente</span>
|
||
${laufend && d.rechte.medikament
|
||
? `<button class="knopf klein" data-gabe="1">+ Gabe dokumentieren</button>` : ""}
|
||
</div>
|
||
<div class="liste">
|
||
${d.gaben.length ? d.gaben.map((g) => `
|
||
<div class="liste-zeile">
|
||
<span class="marker ${g.btm ? "kritisch" : "neutral"}">
|
||
${g.btm ? "BTM" : "💊"}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(g.name)}
|
||
<span class="pat-geburt">${g.menge} ${sicher(g.einheit)}</span></div>
|
||
<div class="lz-unter">
|
||
${g.dosierung ? sicher(g.dosierung) + " · " : ""}
|
||
${sicher(g.autor)} · ${zeit(g.gegeben_am)}
|
||
${g.hinweis ? " · " + sicher(g.hinweis) : ""}
|
||
</div>
|
||
</div>
|
||
${laufend && (g.eigene || d.rechte.bestand)
|
||
? `<button class="knopf leise klein" data-zuruecknehmen="${g.id}"
|
||
title="Fehleintrag zurücknehmen">✕</button>` : ""}
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Gabe dokumentiert.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="db-spalten" style="margin-top:14px">
|
||
<div class="karte">
|
||
<div class="karte-kopf"><span class="karte-titel">Laborergebnisse des Patienten</span></div>
|
||
<div class="liste">
|
||
${d.labor.length ? d.labor.map((l) => `
|
||
<div class="liste-zeile">
|
||
<span class="db-nummer">${sicher(l.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(l.art)}</div>
|
||
<div class="lz-unter">${sicher(l.befund || l.ergebnis
|
||
|| "Noch kein Ergebnis")}</div>
|
||
</div>
|
||
<span class="marker ${l.status === "fertig" ? "gut" : "warn"}">
|
||
${sicher(l.status)}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Laboraufträge.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf"><span class="karte-titel">Operationen des Patienten</span></div>
|
||
<div class="liste">
|
||
${d.operationen.length ? d.operationen.map((o) => `
|
||
<div class="liste-zeile">
|
||
<span class="db-nummer">${sicher(o.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(o.art)}</div>
|
||
<div class="lz-unter">${o.beginn ? zeit(o.beginn) : "noch nicht begonnen"}</div>
|
||
</div>
|
||
<span class="marker neutral">${sicher(o.status)}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Operationen.</div>`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteEinzel(behaelter, d);
|
||
}
|
||
|
||
/**
|
||
* Ein Feld des Dokumentationsbogens.
|
||
* @param {string} name Feldname
|
||
* @param {string} titel Beschriftung
|
||
* @param {string} wert Bisheriger Inhalt
|
||
* @param {boolean} schreibbar Darf bearbeitet werden?
|
||
* @param {string} hinweis Text unter dem Feld
|
||
*/
|
||
function feld(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>
|
||
${hinweis ? `<div class="feld-hinweis">${sicher(hinweis)}</div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
return `<div class="beh-feld">
|
||
<label>${sicher(titel)}</label>
|
||
<textarea name="${sicher(name)}" rows="4"
|
||
placeholder="${sicher(hinweis || "")}">${sicher(wert || "")}</textarea>
|
||
${hinweis ? `<div class="feld-hinweis">${sicher(hinweis)}</div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
function verdrahteEinzel(behaelter, d) {
|
||
const b = d.behandlung;
|
||
|
||
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: b.patient });
|
||
});
|
||
|
||
// Baustein einsetzen: füllt nur den Bogen, gespeichert wird danach von Hand
|
||
const baustein = behaelter.querySelector("[data-baustein]");
|
||
if (baustein) baustein.addEventListener("click", () => {
|
||
Bausteine.einsetzen({ modul: "behandlung", behaelter });
|
||
});
|
||
|
||
const speichern = behaelter.querySelector("[data-speichern]");
|
||
if (speichern) speichern.addEventListener("click", async () => {
|
||
const lies = (name) => {
|
||
const el = behaelter.querySelector(`[name=${name}]`);
|
||
return el ? el.value.trim() : undefined;
|
||
};
|
||
try {
|
||
await aufruf("behandlung", "speichern", {
|
||
id: b.id,
|
||
symptome: lies("symptome"),
|
||
untersuchung: lies("untersuchung"),
|
||
diagnose: lies("diagnose"),
|
||
massnahmen: lies("massnahmen"),
|
||
empfehlung: lies("empfehlung"),
|
||
nachsorge: lies("nachsorge"),
|
||
});
|
||
meldung("Dokumentation gespeichert.", "gut");
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
|
||
const abschliessen = behaelter.querySelector("[data-abschliessen]");
|
||
if (abschliessen) abschliessen.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Behandlung abschließen",
|
||
inhalt: `
|
||
<p><b>${sicher(b.kennung)}</b> — ${sicher(b.patientName)}</p>
|
||
<p>Die Dokumentation wird damit schreibgeschützt. Sie lässt sich später
|
||
mit Begründung wieder öffnen.</p>
|
||
${!b.diagnose ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">📄</span>
|
||
<div><b>Es ist keine Diagnose eingetragen.</b> Abschließen ist trotzdem
|
||
möglich — das System trägt keine für dich ein.</div>
|
||
</div>` : ""}
|
||
<p class="feld-hinweis">Nicht gespeicherte Änderungen im Bogen gehen dabei
|
||
verloren. Erst speichern, dann abschließen.</p>
|
||
<div class="feld"><label>Vermerk (freiwillig)</label>
|
||
<input name="vermerk" maxlength="255"></div>`,
|
||
bestaetigenText: "Abschließen",
|
||
bestaetigt: async (el, zu) => {
|
||
try {
|
||
await aufruf("behandlung", "abschliessen", {
|
||
id: b.id, vermerk: el.querySelector("[name=vermerk]").value.trim(),
|
||
});
|
||
zu();
|
||
meldung("Behandlung abgeschlossen.", "gut");
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
|
||
const wiederOeffnen = behaelter.querySelector("[data-oeffnen]");
|
||
if (wiederOeffnen) wiederOeffnen.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Dokumentation wieder öffnen",
|
||
inhalt: `<p>Die Behandlung wird wieder bearbeitbar. Der Vorgang steht
|
||
dauerhaft im Protokoll.</p>
|
||
<div class="feld"><label>Begründung (Pflicht)</label>
|
||
<input name="grund" maxlength="255"
|
||
placeholder="Was muss nachgetragen werden?"></div>`,
|
||
bestaetigenText: "Wieder öffnen",
|
||
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("behandlung", "wiedereroeffnen", { id: b.id, grund });
|
||
zu();
|
||
meldung("Dokumentation wieder geöffnet.", "gut");
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
|
||
const gabe = behaelter.querySelector("[data-gabe]");
|
||
if (gabe) gabe.addEventListener("click", () => gabeFenster(behaelter, d));
|
||
|
||
behaelter.querySelectorAll("[data-zuruecknehmen]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Eintrag zurücknehmen",
|
||
gefahr: true,
|
||
inhalt: `<p>Der Eintrag wird aus der Dokumentation entfernt. Wurde damals
|
||
Bestand gebucht, wird er als Korrektur zurückgebucht.</p>
|
||
<div class="feld"><label>Begründung (Pflicht)</label>
|
||
<input name="grund" maxlength="255"
|
||
placeholder="z. B. falsches Medikament eingetragen"></div>`,
|
||
bestaetigenText: "Zurücknehmen",
|
||
bestaetigt: async (dialog, zu) => {
|
||
const grund = dialog.querySelector("[name=grund]").value.trim();
|
||
if (grund.length < 3) return meldung("Bitte eine Begründung angeben.", "fehler");
|
||
try {
|
||
await aufruf("behandlung", "medikamentZuruecknehmen", {
|
||
gabe: +el.dataset.zuruecknehmen, grund,
|
||
});
|
||
zu();
|
||
meldung("Eintrag zurückgenommen.", "gut");
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Medikamentengabe ---------------------------------------------------- */
|
||
|
||
async function gabeFenster(behaelter, d) {
|
||
const b = d.behandlung;
|
||
|
||
let katalog;
|
||
try {
|
||
katalog = await aufruf("behandlung", "medikamente", {});
|
||
} catch (f) { return meldung(f.message, "fehler"); }
|
||
|
||
let gewaehlt = null;
|
||
// Wird gesetzt, wenn der Bestand nicht reicht und trotzdem dokumentiert wird
|
||
let ohneBestand = false;
|
||
|
||
// Medikamente nach Kategorie gruppieren
|
||
const gruppen = {};
|
||
katalog.medikamente.forEach((m) => {
|
||
(gruppen[m.kategorie] = gruppen[m.kategorie] || []).push(m);
|
||
});
|
||
const kategorieName = {};
|
||
katalog.kategorien.forEach((k) => { kategorieName[k.id] = k.name; });
|
||
|
||
const dialog = bestaetigen({
|
||
titel: "Medikamentengabe dokumentieren",
|
||
inhalt: `
|
||
<p><b>${sicher(b.patientName)}</b> · ${sicher(b.kennung)}</p>
|
||
${d.stamm.allergien ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">⚠</span>
|
||
<div><b>Bekannte Allergien:</b> ${sicher(d.stamm.allergien)}<br>
|
||
<span class="feld-hinweis">Das System prüft nicht gegen diese Liste.
|
||
Der Abgleich ist deine Aufgabe.</span></div>
|
||
</div>` : ""}
|
||
|
||
<div class="feld"><label>Medikament</label>
|
||
<input name="suche" placeholder="Filtern…">
|
||
<div class="beh-medikamente" data-liste="1">
|
||
${Object.keys(gruppen).map((kategorie) => `
|
||
<div class="beh-med-gruppe" data-gruppe="${sicher(kategorie)}">
|
||
<div class="beh-med-titel">${sicher(kategorieName[kategorie] || kategorie)}</div>
|
||
${gruppen[kategorie].map((m) => `
|
||
<button class="beh-med ${m.waehlbar ? "" : "gesperrt"}"
|
||
${m.waehlbar ? "" : "disabled"}
|
||
data-med="${m.id}" data-name="${sicher(m.name.toLowerCase())}"
|
||
data-dosierung="${sicher(m.dosierung_standard || "")}"
|
||
data-bestand="${m.bestand}" data-einheit="${sicher(m.einheit)}">
|
||
<span class="beh-med-name">${sicher(m.name)}
|
||
${m.btm ? `<span class="marker kritisch">BTM</span>` : ""}
|
||
${m.abgelaufen ? `<span class="marker warn"
|
||
title="Verfallsdatum überschritten — nicht gesperrt">abgelaufen</span>` : ""}</span>
|
||
<span class="beh-med-unter">${sicher(m.wirkstoff || "")}
|
||
${m.dosierung_standard ? " · " + sicher(m.dosierung_standard) : ""}</span>
|
||
<span class="beh-med-bestand ${m.bestand <= m.mindestbestand ? "knapp" : ""}">
|
||
${m.bestand} ${sicher(m.einheit)}</span>
|
||
</button>`).join("")}
|
||
</div>`).join("")}
|
||
</div>
|
||
<div class="feld-hinweis">Die Standarddosierung ist ein <b>Vorschlag</b> aus dem
|
||
Katalog. Was du gibst, entscheidest du.</div>
|
||
</div>
|
||
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Menge</label>
|
||
<input name="menge" type="number" min="1" max="999" value="1"></div>
|
||
<div class="feld"><label>Dosierung</label>
|
||
<input name="dosierung" maxlength="64" placeholder="z. B. 1 g i.v."></div>
|
||
</div>
|
||
<div class="feld"><label>Hinweis (freiwillig)</label>
|
||
<input name="hinweis" maxlength="255"
|
||
placeholder="z. B. gute Verträglichkeit, Zugang links"></div>
|
||
<div class="beh-bestandswarnung" data-warnung="1" style="display:none"></div>`,
|
||
bestaetigenText: "Gabe dokumentieren",
|
||
bestaetigt: async (el, zu) => {
|
||
if (!gewaehlt) return meldung("Bitte ein Medikament wählen.", "fehler");
|
||
try {
|
||
await aufruf("behandlung", "medikamentGeben", {
|
||
id: b.id,
|
||
medikament: gewaehlt,
|
||
menge: +el.querySelector("[name=menge]").value,
|
||
dosierung: el.querySelector("[name=dosierung]").value.trim(),
|
||
hinweis: el.querySelector("[name=hinweis]").value.trim(),
|
||
ohneBestand,
|
||
});
|
||
zu();
|
||
meldung("Gabe dokumentiert.", "gut");
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) {
|
||
meldung(f.message, "fehler");
|
||
// Bestand reicht nicht: Dokumentation trotzdem anbieten
|
||
if (f.message.indexOf("Bestand") !== -1) zeigeBestandsWeg(el);
|
||
}
|
||
},
|
||
nachOeffnen: (hg) => {
|
||
const suchfeld = hg.querySelector("[name=suche]");
|
||
const dosierung = hg.querySelector("[name=dosierung]");
|
||
|
||
hg.querySelectorAll("[data-med]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
gewaehlt = +el.dataset.med;
|
||
ohneBestand = false;
|
||
hg.querySelector("[data-warnung]").style.display = "none";
|
||
hg.querySelectorAll("[data-med]").forEach((k) => k.classList.remove("gewaehlt"));
|
||
el.classList.add("gewaehlt");
|
||
// Standarddosierung als Vorschlag übernehmen, überschreibbar
|
||
if (!dosierung.value) dosierung.value = el.dataset.dosierung;
|
||
});
|
||
});
|
||
|
||
suchfeld.addEventListener("input", () => {
|
||
const begriff = suchfeld.value.trim().toLowerCase();
|
||
hg.querySelectorAll("[data-med]").forEach((el) => {
|
||
el.style.display = !begriff || el.dataset.name.indexOf(begriff) !== -1 ? "" : "none";
|
||
});
|
||
// Leere Gruppen ausblenden
|
||
hg.querySelectorAll("[data-gruppe]").forEach((gruppe) => {
|
||
const sichtbar = [...gruppe.querySelectorAll("[data-med]")]
|
||
.some((el) => el.style.display !== "none");
|
||
gruppe.style.display = sichtbar ? "" : "none";
|
||
});
|
||
});
|
||
},
|
||
});
|
||
|
||
/** Bietet an, die Gabe ohne Bestandsbuchung zu dokumentieren */
|
||
function zeigeBestandsWeg(el) {
|
||
const warnung = el.querySelector("[data-warnung]");
|
||
warnung.style.display = "";
|
||
warnung.innerHTML = `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">📦</span>
|
||
<div><b>Der Lagerbestand reicht nicht.</b> Eine Dokumentation darf daran
|
||
nicht scheitern — etwa wenn das Mittel aus dem RTW stammt.
|
||
<br><button class="knopf leise klein" data-ohne="1"
|
||
style="margin-top:8px">Ohne Bestandsbuchung dokumentieren</button></div>
|
||
</div>`;
|
||
|
||
warnung.querySelector("[data-ohne]").addEventListener("click", () => {
|
||
ohneBestand = true;
|
||
meldung("Bestandsbuchung deaktiviert — jetzt bestätigen.", "gut");
|
||
warnung.querySelector("[data-ohne]").disabled = true;
|
||
});
|
||
}
|
||
|
||
return dialog;
|
||
}
|
||
|
||
/* ---- Anlegen ------------------------------------------------------------- */
|
||
|
||
function anlegenFenster(behaelter) {
|
||
let gewaehlt = null;
|
||
let einsaetze = [];
|
||
|
||
bestaetigen({
|
||
titel: "Behandlung dokumentieren",
|
||
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" data-einsatzfeld="1" style="display:none">
|
||
<label>Zu einem Einsatz zuordnen (freiwillig)</label>
|
||
<select name="einsatz"></select></div>
|
||
<div class="feld"><label>Symptome / Anamnese</label>
|
||
<textarea name="symptome" rows="3"
|
||
placeholder="Was schildert der Patient?"></textarea></div>
|
||
<div class="feld"><label>Erste Untersuchung</label>
|
||
<textarea name="untersuchung" rows="3"
|
||
placeholder="Vitalwerte, erster Eindruck…"></textarea>
|
||
<div class="feld-hinweis">Diagnose, Maßnahmen und Nachsorge trägst du
|
||
anschließend im Bogen nach.</div></div>
|
||
</div>`,
|
||
bestaetigenText: "Anlegen",
|
||
bestaetigt: async (el, zu) => {
|
||
if (!gewaehlt) return meldung("Bitte zuerst einen Patienten wählen.", "fehler");
|
||
const einsatzFeld = el.querySelector("[name=einsatz]");
|
||
try {
|
||
const antwort = await aufruf("behandlung", "anlegen", {
|
||
patient: gewaehlt.id,
|
||
aufnahme: gewaehlt.aufnahme || null,
|
||
einsatz: einsatzFeld && einsatzFeld.value ? +einsatzFeld.value : null,
|
||
symptome: el.querySelector("[name=symptome]").value.trim(),
|
||
untersuchung: el.querySelector("[name=untersuchung]").value.trim(),
|
||
});
|
||
zu();
|
||
meldung("Behandlung " + antwort.kennung + " angelegt.", "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]");
|
||
const einsatzfeld = hg.querySelector("[data-einsatzfeld]");
|
||
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("behandlung", "patientSuche", { begriff });
|
||
const treffer = antwort.treffer || [];
|
||
einsaetze = antwort.einsaetze || [];
|
||
|
||
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">aufgenommen</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 = "";
|
||
|
||
// Laufende Einsätze anbieten, an denen man beteiligt ist
|
||
if (einsaetze.length) {
|
||
einsatzfeld.style.display = "";
|
||
hg.querySelector("[name=einsatz]").innerHTML =
|
||
`<option value="">— keinem Einsatz zuordnen —</option>` +
|
||
einsaetze.map((e) =>
|
||
`<option value="${e.id}">${sicher(e.nummer)} · ${sicher(e.titel)}</option>`
|
||
).join("");
|
||
}
|
||
});
|
||
});
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}, 260);
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ---- Laden ---------------------------------------------------------------- */
|
||
|
||
async function oeffne(behaelter, behandlungId) {
|
||
try {
|
||
const d = await aufruf("behandlung", "oeffnen", { id: behandlungId });
|
||
z.offen = behandlungId;
|
||
z.offeneDaten = d;
|
||
einzelAnsicht(behaelter, d);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}
|
||
|
||
async function rendern(behaelter) {
|
||
try {
|
||
z.daten = await aufruf("behandlung", "uebersicht", {
|
||
ordner: z.ordner, begriff: z.suche,
|
||
});
|
||
|
||
// Sprungziel aus einem Hinweis, z. B. „Dokumentation fehlt"
|
||
const ziel = App.zustand.modulZiel;
|
||
if (ziel && ziel.behandlung) {
|
||
App.zustand.modulZiel = null;
|
||
return oeffne(behaelter, ziel.behandlung);
|
||
}
|
||
|
||
if (z.offen) await oeffne(behaelter, z.offen);
|
||
else uebersicht(behaelter);
|
||
} catch (f) {
|
||
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Behandlungen</h2>
|
||
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
App.registriereAnsicht("behandlung", { rendern });
|
||
|
||
App.registriereSuche(async (begriff) => {
|
||
try {
|
||
const d = await aufruf("behandlung", "uebersicht", {
|
||
ordner: "laufend", begriff,
|
||
});
|
||
return (d.liste || []).slice(0, 5).map((b) => ({
|
||
bereich: "Behandlung",
|
||
titel: b.kennung + " · " + b.patientName,
|
||
unter: b.diagnose || b.massnahmen || "Noch nichts eingetragen",
|
||
}));
|
||
} catch (e) { return []; }
|
||
});
|
||
})();
|