- 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>
767 lines
31 KiB
JavaScript
767 lines
31 KiB
JavaScript
/* =====================================================================
|
||
aether-medical / Ansicht: Medizinisches MDT
|
||
|
||
Patientensuche und Patientenakte. Alles, was hier rot oder auffällig
|
||
dargestellt wird, hat ein Mediziner eingetragen — das System leitet
|
||
aus keinem Feld eine Warnung, eine Diagnose oder eine Empfehlung ab.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
(() => {
|
||
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen } = App;
|
||
|
||
const z = {
|
||
suche: "",
|
||
treffer: [],
|
||
patient: null, // geöffnete Patientenakte (Serverantwort)
|
||
reiter: "stamm", // stamm | verlauf | akten | notizen
|
||
unterdaten: null, // Daten des aktiven Reiters
|
||
offeneAkte: null,
|
||
};
|
||
|
||
// Symbol und Farbe je Verlaufsart
|
||
const verlaufsArt = {
|
||
aufnahme: { symbol: "🏥", art: "info" },
|
||
entlassung: { symbol: "🚪", art: "neutral" },
|
||
behandlung: { symbol: "🩺", art: "gut" },
|
||
operation: { symbol: "🔪", art: "warn" },
|
||
labor: { symbol: "🧪", art: "info" },
|
||
medikament: { symbol: "💊", art: "neutral" },
|
||
notiz: { symbol: "📝", art: "neutral" },
|
||
};
|
||
|
||
const statusArt = {
|
||
entwurf: "neutral", offen: "warn", freigegeben: "gut", geschlossen: "info",
|
||
};
|
||
|
||
/* ---- Suche ------------------------------------------------------------ */
|
||
|
||
function sucheAnsicht(behaelter) {
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Medizinisches MDT</h2>
|
||
<p>Patientensuche über Name, Geburtsdatum oder Telefonnummer.
|
||
Was in einer Akte steht, hat ein Mediziner eingetragen.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filterleiste">
|
||
<input data-suche="1" placeholder="Name, Geburtsdatum (JJJJ-MM-TT) oder Telefonnummer…"
|
||
value="${sicher(z.suche)}" autofocus>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Treffer</span>
|
||
<span class="feld-hinweis">${z.treffer.length} Personen</span>
|
||
</div>
|
||
<div class="liste">
|
||
${z.treffer.length ? z.treffer.map(trefferZeile).join("")
|
||
: `<div class="liste-leer">${z.suche.length >= 2
|
||
? "Keine Person gefunden."
|
||
: "Mindestens zwei Zeichen eingeben."}</div>`}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const feld = behaelter.querySelector("[data-suche]");
|
||
let warten = null;
|
||
feld.addEventListener("input", () => {
|
||
clearTimeout(warten);
|
||
warten = setTimeout(async () => {
|
||
z.suche = feld.value.trim();
|
||
if (z.suche.length < 2) { z.treffer = []; return sucheAnsicht(behaelter); }
|
||
try {
|
||
const d = await aufruf("mdt", "suche", { begriff: z.suche });
|
||
z.treffer = d.treffer || [];
|
||
sucheAnsicht(behaelter);
|
||
// Den Fokus zurück ins Suchfeld, damit man weitertippen kann
|
||
const neu = behaelter.querySelector("[data-suche]");
|
||
neu.focus();
|
||
neu.setSelectionRange(neu.value.length, neu.value.length);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}, 280);
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-patient]").forEach((el) => {
|
||
el.addEventListener("click", () => oeffnePatient(behaelter, +el.dataset.patient));
|
||
});
|
||
}
|
||
|
||
function trefferZeile(p) {
|
||
return `<div class="liste-zeile" data-patient="${p.id}">
|
||
<div class="mdt-bild klein">${p.lichtbild
|
||
? `<img src="${sicher(p.lichtbild)}" alt="">`
|
||
: `<span>—</span>`}</div>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(p.firstname)} ${sicher(p.lastname)}</div>
|
||
<div class="lz-unter">
|
||
geb. ${sicher(p.dob)}
|
||
${p.telefon ? " · ☎ " + sicher(p.telefon) : ""}
|
||
${p.blutgruppe ? " · " + sicher(p.blutgruppe) : " · Blutgruppe unbekannt"}
|
||
</div>
|
||
</div>
|
||
${p.hinweis ? `<span class="marker kritisch" title="${sicher(p.hinweis)}">Warnhinweis</span>` : ""}
|
||
${p.allergien ? `<span class="marker warn" title="${sicher(p.allergien)}">Allergien</span>` : ""}
|
||
${p.imHaus ? `<span class="marker info">im Haus</span>` : ""}
|
||
${p.wartet ? `<span class="marker warn">wartet</span>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
/* ---- Patientenakte ------------------------------------------------------ */
|
||
|
||
function akteAnsicht(behaelter) {
|
||
const d = z.patient;
|
||
const p = d.patient, s = d.stamm;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>${sicher(p.name)}</h2>
|
||
<p>geb. ${sicher(p.geburt)} · ${sicher(p.geschlecht === "w" ? "weiblich" : "männlich")}
|
||
${p.telefon ? " · ☎ " + sicher(p.telefon) : ""}</p>
|
||
</div>
|
||
<button class="knopf leise" data-zurueck="1">← Suche</button>
|
||
</div>
|
||
|
||
${s.hinweis ? `
|
||
<div class="warnung kritisch">
|
||
<span class="w-symbol">⚠</span>
|
||
<div><b>Warnhinweis</b> — ${sicher(s.hinweis)}
|
||
${s.gepflegtVon ? `<br><span class="feld-hinweis">Eingetragen von
|
||
${sicher(s.gepflegtVon)}${s.gepflegtAm ? " am " + zeit(s.gepflegtAm) : ""}.
|
||
Das System hat diesen Hinweis nicht erzeugt.</span>` : ""}
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
${d.aufnahme ? `
|
||
<div class="warnung ${d.aufnahme.status === "wartend" ? "warn" : ""}">
|
||
<span class="w-symbol">🏥</span>
|
||
<div><b>${d.aufnahme.status === "wartend"
|
||
? "Wartet auf Aufnahme" : "Aufgenommen"}</b>
|
||
— ${sicher(d.aufnahme.grund)}
|
||
${d.aufnahme.zimmer ? " · Zimmer " + sicher(d.aufnahme.zimmer) : ""}
|
||
<br><span class="feld-hinweis">Seit ${zeit(d.aufnahme.aufgenommen_am)}
|
||
durch ${sicher(d.aufnahme.aufgenommenVon)}.</span>
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
${d.intensiv ? `
|
||
<div class="warnung ${d.intensiv.zustand === "kritisch" ? "kritisch" : ""}">
|
||
<span class="w-symbol">🫀</span>
|
||
<div><b>Auf Intensivstation</b> — Zustand als
|
||
<b>${sicher(d.intensiv.zustand)}</b> eingetragen
|
||
${d.intensiv.verantwortlichName
|
||
? " · zuständig: " + sicher(d.intensiv.verantwortlichName) : ""}
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
<div class="mdt-kopf">
|
||
<div class="mdt-bild">${s.lichtbild
|
||
? `<img src="${sicher(s.lichtbild)}" alt="Lichtbild">`
|
||
: `<span>Kein Lichtbild</span>`}</div>
|
||
|
||
<div class="mdt-kopf-werte">
|
||
${wertZeile("Blutgruppe", s.blutgruppe, !s.blutgruppe && "nicht bestimmt")}
|
||
${wertZeile("Allergien", s.allergien, "keine erfasst")}
|
||
${wertZeile("Vorerkrankungen", s.vorerkrankungen, "keine erfasst")}
|
||
${wertZeile("Dauermedikation", s.dauermedikation, "keine erfasst")}
|
||
${wertZeile("Implantate", s.implantate, "keine erfasst")}
|
||
${wertZeile("Organspende", s.organspenderName)}
|
||
${wertZeile("Notfallkontakt", s.notfallkontakt, "keiner hinterlegt")}
|
||
</div>
|
||
|
||
<div class="mdt-kopf-aktionen">
|
||
${d.rechte.stammdaten
|
||
? `<button class="knopf klein" data-stamm="1">Stammdaten pflegen</button>` : ""}
|
||
${d.rechte.hinweis
|
||
? `<button class="knopf leise klein" data-hinweis="1">
|
||
${s.hinweis ? "Warnhinweis ändern" : "Warnhinweis setzen"}</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reiter">
|
||
<button class="${z.reiter === "stamm" ? "aktiv" : ""}" data-reiter="stamm">
|
||
Übersicht</button>
|
||
<button class="${z.reiter === "verlauf" ? "aktiv" : ""}" data-reiter="verlauf">
|
||
Verlauf</button>
|
||
<button class="${z.reiter === "akten" ? "aktiv" : ""}" data-reiter="akten">
|
||
Akten <span class="mdt-zahl">${d.zaehler.akten}</span></button>
|
||
<button class="${z.reiter === "notizen" ? "aktiv" : ""}" data-reiter="notizen">
|
||
Vermerke <span class="mdt-zahl">${d.zaehler.notizen}</span></button>
|
||
</div>
|
||
|
||
<div id="mdt-unterinhalt">
|
||
<div class="laden"><div class="spinner"></div><span>Lade …</span></div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteAkte(behaelter);
|
||
zeichneReiter(behaelter);
|
||
}
|
||
|
||
/** Eine Zeile im Stammdatenblock */
|
||
function wertZeile(name, wert, ersatz) {
|
||
const fehlt = !wert;
|
||
return `<div class="mdt-wert ${fehlt ? "fehlt" : ""}">
|
||
<span>${sicher(name)}</span>
|
||
<b>${fehlt ? sicher(ersatz || "nicht erfasst") : sicher(wert)}</b>
|
||
</div>`;
|
||
}
|
||
|
||
function verdrahteAkte(behaelter) {
|
||
behaelter.querySelector("[data-zurueck]").addEventListener("click", () => {
|
||
z.patient = null;
|
||
z.offeneAkte = null;
|
||
sucheAnsicht(behaelter);
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
z.reiter = el.dataset.reiter;
|
||
z.offeneAkte = null;
|
||
akteAnsicht(behaelter);
|
||
});
|
||
});
|
||
|
||
const stamm = behaelter.querySelector("[data-stamm]");
|
||
if (stamm) stamm.addEventListener("click", () => stammdatenFenster(behaelter));
|
||
|
||
const hinweis = behaelter.querySelector("[data-hinweis]");
|
||
if (hinweis) hinweis.addEventListener("click", () => hinweisFenster(behaelter));
|
||
}
|
||
|
||
/* ---- Reiterinhalte ------------------------------------------------------- */
|
||
|
||
async function zeichneReiter(behaelter) {
|
||
const ziel = behaelter.querySelector("#mdt-unterinhalt");
|
||
const patientId = z.patient.patient.id;
|
||
|
||
try {
|
||
if (z.reiter === "verlauf") {
|
||
z.unterdaten = await aufruf("mdt", "verlauf", { patient: patientId });
|
||
zeichneVerlauf(ziel);
|
||
} else if (z.reiter === "akten") {
|
||
z.unterdaten = await aufruf("mdt", "akten", { patient: patientId });
|
||
zeichneAkten(ziel, behaelter);
|
||
} else if (z.reiter === "notizen") {
|
||
z.unterdaten = await aufruf("mdt", "notizen", { patient: patientId });
|
||
zeichneNotizen(ziel, behaelter);
|
||
} else {
|
||
zeichneUebersicht(ziel);
|
||
}
|
||
} catch (f) {
|
||
ziel.innerHTML = `<div class="liste-leer">${sicher(f.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function zeichneUebersicht(ziel) {
|
||
const d = z.patient;
|
||
const zaehler = d.zaehler;
|
||
|
||
ziel.innerHTML = `
|
||
<div class="raster k3">
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Behandlungen</div>
|
||
<div class="kz-wert">${zaehler.behandlungen}</div>
|
||
<div class="kz-zusatz">dokumentiert</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Operationen</div>
|
||
<div class="kz-wert">${zaehler.operationen}</div>
|
||
<div class="kz-zusatz">dokumentiert</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Laboraufträge</div>
|
||
<div class="kz-wert">${zaehler.labor}</div>
|
||
<div class="kz-zusatz">gestellt</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Aufnahmen</div>
|
||
<div class="kz-wert">${zaehler.aufnahmen}</div>
|
||
<div class="kz-zusatz">insgesamt</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Akten</div>
|
||
<div class="kz-wert">${zaehler.akten}</div>
|
||
<div class="kz-zusatz">angelegt</div>
|
||
</div>
|
||
<div class="kennzahl">
|
||
<div class="kz-titel">Vermerke</div>
|
||
<div class="kz-wert">${zaehler.notizen}</div>
|
||
<div class="kz-zusatz">geschrieben</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="db-grundsatz">
|
||
<b>Fehlende Angaben bedeuten „nicht erhoben", nicht „unauffällig".</b>
|
||
Eine leere Allergieliste heißt, dass niemand danach gefragt oder es
|
||
eingetragen hat. Das System füllt keines dieser Felder von selbst und
|
||
schließt aus keinem etwas.
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function zeichneVerlauf(ziel) {
|
||
const eintraege = z.unterdaten.eintraege || [];
|
||
|
||
ziel.innerHTML = eintraege.length ? `
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Zeitleiste</span>
|
||
<span class="feld-hinweis">${z.unterdaten.gesamt} Einträge insgesamt</span>
|
||
</div>
|
||
<div class="mdt-verlauf">
|
||
${eintraege.map((e) => {
|
||
const stil = verlaufsArt[e.art] || { symbol: "•", art: "neutral" };
|
||
return `<div class="mdt-vl-schritt">
|
||
<div class="mdt-vl-punkt ${sicher(stil.art)}">${stil.symbol}</div>
|
||
<div class="mdt-vl-inhalt">
|
||
<div class="mdt-vl-kopf">
|
||
<b>${sicher(e.titel)}</b>
|
||
<span class="db-zeit">${zeit(e.am)}</span>
|
||
</div>
|
||
${e.text ? `<div class="mdt-vl-text">${sicher(e.text)}</div>` : ""}
|
||
${e.marker ? `<div class="mdt-vl-meta">${sicher(e.marker)}</div>` : ""}
|
||
</div>
|
||
</div>`;
|
||
}).join("")}
|
||
</div>
|
||
</div>`
|
||
: `<div class="liste-leer">Für diesen Patienten ist noch nichts dokumentiert.</div>`;
|
||
}
|
||
|
||
function zeichneAkten(ziel, behaelter) {
|
||
const d = z.unterdaten;
|
||
|
||
// Eine einzelne Akte ist geöffnet
|
||
if (z.offeneAkte) return zeichneEinzelakte(ziel, behaelter);
|
||
|
||
ziel.innerHTML = `
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Akten dieses Patienten</span>
|
||
${z.patient.rechte.akteErstellen
|
||
? `<button class="knopf klein" data-neu="1">+ Akte anlegen</button>` : ""}
|
||
</div>
|
||
<div class="liste">
|
||
${d.akten.length ? d.akten.map((a) => `
|
||
<div class="liste-zeile" data-akte="${a.id}">
|
||
<span class="db-nummer">${sicher(a.aktenzeichen)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(a.titel)}</div>
|
||
<div class="lz-unter">${sicher(a.typName)} · ${sicher(a.autor)}
|
||
· ${zeit(a.erstellt_am)}</div>
|
||
</div>
|
||
<span class="marker neutral">v${a.version}</span>
|
||
<span class="marker ${statusArt[a.status] || "neutral"}">${sicher(a.status)}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch keine Akte angelegt.</div>`}
|
||
</div>
|
||
</div>`;
|
||
|
||
ziel.querySelectorAll("[data-akte]").forEach((el) => {
|
||
el.addEventListener("click", async () => {
|
||
try {
|
||
z.offeneAkte = await aufruf("mdt", "akteOeffnen", { id: +el.dataset.akte });
|
||
zeichneEinzelakte(ziel, behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
});
|
||
|
||
const neu = ziel.querySelector("[data-neu]");
|
||
if (neu) neu.addEventListener("click", () => akteAnlegenFenster(behaelter));
|
||
}
|
||
|
||
function zeichneEinzelakte(ziel, behaelter) {
|
||
const d = z.offeneAkte;
|
||
const a = d.akte;
|
||
|
||
ziel.innerHTML = `
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">${sicher(a.aktenzeichen)} · Version ${a.version}</span>
|
||
<div class="mdt-akte-aktionen">
|
||
${d.rechte.bearbeiten && a.status !== "geschlossen"
|
||
? `<button class="knopf leise klein" data-bearbeiten="1">Bearbeiten</button>` : ""}
|
||
${d.rechte.freigeben && a.status !== "freigegeben" && a.status !== "geschlossen"
|
||
? `<button class="knopf leise klein" data-status="freigegeben">Freigeben</button>` : ""}
|
||
${d.rechte.schliessen && a.status !== "geschlossen"
|
||
? `<button class="knopf leise klein" data-status="geschlossen">Schließen</button>` : ""}
|
||
<button class="knopf leise klein" data-zurueckliste="1">← Akten</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 class="mdt-akte-titel">${sicher(a.titel)}</h3>
|
||
<div class="mdt-akte-meta">
|
||
<span class="marker ${statusArt[a.status] || "neutral"}">${sicher(a.status)}</span>
|
||
<span class="feld-hinweis">Angelegt von ${sicher(a.autor)} am ${zeit(a.erstelltAm)}
|
||
${a.freigabeVon ? ` · freigegeben von ${sicher(a.freigabeVon)}
|
||
am ${zeit(a.freigabeAm)}` : ""}</span>
|
||
</div>
|
||
|
||
<div class="mdt-akte-inhalt">${a.inhalt
|
||
? sicher(a.inhalt).replace(/\n/g, "<br>")
|
||
: `<span class="feld-hinweis">Diese Akte hat noch keinen Inhalt.</span>`}</div>
|
||
</div>
|
||
|
||
<div class="karte" style="margin-top:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Versionsverlauf</span>
|
||
<span class="feld-hinweis">Nichts wird überschrieben</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.versionen.map((v) => `
|
||
<div class="liste-zeile klickbar" data-version="${v.version}">
|
||
<span class="marker ${v.version === a.version ? "gut" : "neutral"}">
|
||
v${v.version}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(v.kommentar || "ohne Kommentar")}</div>
|
||
<div class="lz-unter">${sicher(v.autor)} · ${zeit(v.geaendert_am)}</div>
|
||
</div>
|
||
${v.version === a.version
|
||
? `<span class="feld-hinweis">aktuell</span>`
|
||
: `<span class="feld-hinweis">ansehen</span>`}
|
||
</div>`).join("")}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
ziel.querySelector("[data-zurueckliste]").addEventListener("click", () => {
|
||
z.offeneAkte = null;
|
||
zeichneReiter(behaelter);
|
||
});
|
||
|
||
const bearbeiten = ziel.querySelector("[data-bearbeiten]");
|
||
if (bearbeiten) bearbeiten.addEventListener("click", () => akteBearbeitenFenster(behaelter));
|
||
|
||
ziel.querySelectorAll("[data-status]").forEach((el) => {
|
||
el.addEventListener("click", () => akteStatusFenster(behaelter, el.dataset.status));
|
||
});
|
||
|
||
ziel.querySelectorAll("[data-version]").forEach((el) => {
|
||
el.addEventListener("click", () => versionAnsehen(+el.dataset.version));
|
||
});
|
||
}
|
||
|
||
function zeichneNotizen(ziel, behaelter) {
|
||
const notizen = z.unterdaten.notizen || [];
|
||
|
||
ziel.innerHTML = `
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Vermerke</span>
|
||
<button class="knopf klein" data-neu-notiz="1">+ Vermerk</button>
|
||
</div>
|
||
<div class="liste">
|
||
${notizen.length ? notizen.map((n) => `
|
||
<div class="liste-zeile">
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(n.text)}</div>
|
||
<div class="lz-unter">${sicher(n.autor)}
|
||
${n.personalnummer ? " (" + sicher(n.personalnummer) + ")" : ""}
|
||
· ${zeit(n.am)}</div>
|
||
</div>
|
||
${n.eigene ? `<span class="marker neutral">von dir</span>` : ""}
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Vermerke.</div>`}
|
||
</div>
|
||
</div>`;
|
||
|
||
ziel.querySelector("[data-neu-notiz]").addEventListener("click", () => {
|
||
fenster({
|
||
titel: "Vermerk anlegen",
|
||
inhalt: `<div class="feld"><label>Vermerk</label>
|
||
<textarea name="text" rows="4"
|
||
placeholder="Kurzer Hinweis zum Patienten…"></textarea>
|
||
<div class="feld-hinweis">Ein Vermerk ist keine Diagnose und ersetzt
|
||
keine Behandlungsdokumentation.</div></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: "Speichern", klick: async (el, zu) => {
|
||
try {
|
||
await aufruf("mdt", "notizAnlegen", {
|
||
patient: z.patient.patient.id,
|
||
text: el.querySelector("[name=text]").value.trim(),
|
||
});
|
||
zu();
|
||
meldung("Vermerk gespeichert.", "gut");
|
||
oeffnePatient(behaelter, z.patient.patient.id, "notizen");
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
} },
|
||
],
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Dialoge -------------------------------------------------------------- */
|
||
|
||
function stammdatenFenster(behaelter) {
|
||
const d = z.patient;
|
||
const s = d.stamm;
|
||
|
||
fenster({
|
||
titel: "Stammdaten pflegen",
|
||
inhalt: `
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Blutgruppe</label>
|
||
<select name="blutgruppe">
|
||
<option value="">— nicht bestimmt —</option>
|
||
${d.blutgruppen.map((b) =>
|
||
`<option value="${sicher(b)}" ${b === s.blutgruppe ? "selected" : ""}>
|
||
${sicher(b)}</option>`).join("")}
|
||
</select>
|
||
<div class="feld-hinweis">Wird nicht aus dem Labor übernommen — die
|
||
Feststellung verantwortet ein Mediziner.</div></div>
|
||
<div class="feld"><label>Organspende</label>
|
||
<select name="organspender">
|
||
${Object.keys(d.organstatus).map((k) =>
|
||
`<option value="${sicher(k)}" ${k === s.organspender ? "selected" : ""}>
|
||
${sicher(d.organstatus[k])}</option>`).join("")}
|
||
</select></div>
|
||
</div>
|
||
|
||
<div class="feld"><label>Allergien</label>
|
||
<textarea name="allergien" rows="2"
|
||
placeholder="z. B. Penicillin, Latex">${sicher(s.allergien || "")}</textarea></div>
|
||
<div class="feld"><label>Vorerkrankungen</label>
|
||
<textarea name="vorerkrankungen" rows="3">${sicher(s.vorerkrankungen || "")}</textarea></div>
|
||
<div class="feld"><label>Dauermedikation</label>
|
||
<textarea name="dauermedikation" rows="3">${sicher(s.dauermedikation || "")}</textarea></div>
|
||
<div class="feld"><label>Implantate</label>
|
||
<textarea name="implantate" rows="2"
|
||
placeholder="z. B. Herzschrittmacher">${sicher(s.implantate || "")}</textarea></div>
|
||
<div class="feld"><label>Notfallkontakt</label>
|
||
<input name="notfallkontakt" maxlength="160"
|
||
value="${sicher(s.notfallkontakt || "")}"
|
||
placeholder="Name und Telefonnummer"></div>
|
||
<div class="feld"><label>Lichtbild (Bild-Adresse)</label>
|
||
<input name="lichtbild" value="${sicher(s.lichtbild || "")}"
|
||
placeholder="https://…"></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: "Speichern", klick: async (el, zu) => {
|
||
const wert = (name) => el.querySelector(`[name=${name}]`).value.trim();
|
||
try {
|
||
await aufruf("mdt", "stammdaten", {
|
||
patient: d.patient.id,
|
||
blutgruppe: wert("blutgruppe"),
|
||
organspender: wert("organspender"),
|
||
allergien: wert("allergien"),
|
||
vorerkrankungen: wert("vorerkrankungen"),
|
||
dauermedikation: wert("dauermedikation"),
|
||
implantate: wert("implantate"),
|
||
notfallkontakt: wert("notfallkontakt"),
|
||
lichtbild: wert("lichtbild"),
|
||
});
|
||
zu();
|
||
meldung("Stammdaten gespeichert.", "gut");
|
||
oeffnePatient(behaelter, d.patient.id, z.reiter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
} },
|
||
],
|
||
});
|
||
}
|
||
|
||
function hinweisFenster(behaelter) {
|
||
const d = z.patient;
|
||
|
||
bestaetigen({
|
||
titel: d.stamm.hinweis ? "Warnhinweis ändern" : "Warnhinweis setzen",
|
||
inhalt: `
|
||
<p>Ein Warnhinweis erscheint bei jeder Suche und ganz oben in der Akte.
|
||
Er ist für Dinge gedacht, die vor jeder Maßnahme bekannt sein müssen.</p>
|
||
<div class="feld"><label>Warnhinweis (leer lassen zum Entfernen)</label>
|
||
<textarea name="hinweis" rows="3"
|
||
placeholder="z. B. Gerinnungsstörung — keine Antikoagulantien">${sicher(d.stamm.hinweis || "")}</textarea></div>
|
||
<div class="feld"><label>Begründung</label>
|
||
<input name="grund" maxlength="255"
|
||
placeholder="Woher stammt die Information?"></div>`,
|
||
bestaetigenText: "Übernehmen",
|
||
bestaetigt: async (el, zu) => {
|
||
try {
|
||
await aufruf("mdt", "hinweisSetzen", {
|
||
patient: d.patient.id,
|
||
hinweis: el.querySelector("[name=hinweis]").value.trim(),
|
||
grund: el.querySelector("[name=grund]").value.trim(),
|
||
});
|
||
zu();
|
||
meldung("Warnhinweis übernommen.", "gut");
|
||
oeffnePatient(behaelter, d.patient.id, z.reiter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
}
|
||
|
||
function akteAnlegenFenster(behaelter) {
|
||
const arten = z.unterdaten.aktenarten || [];
|
||
|
||
fenster({
|
||
titel: "Akte anlegen",
|
||
inhalt: `
|
||
<div class="feld"><label>Titel</label>
|
||
<input name="titel" maxlength="160" placeholder="Worum geht es?"></div>
|
||
<div class="feld"><label>Aktenart</label>
|
||
<select name="typ">
|
||
${arten.map((a) =>
|
||
`<option value="${sicher(a.id)}">${sicher(a.name)}</option>`).join("")}
|
||
</select></div>
|
||
<div class="feld"><label>Inhalt</label>
|
||
<textarea name="inhalt" rows="10"
|
||
placeholder="Text der Akte…"></textarea>
|
||
<div class="feld-hinweis">Die Akte startet als Entwurf. Jede spätere Änderung
|
||
erzeugt eine neue Version — überschrieben wird nichts.</div></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: "Anlegen", klick: async (el, zu) => {
|
||
try {
|
||
const antwort = await aufruf("mdt", "akteAnlegen", {
|
||
patient: z.patient.patient.id,
|
||
titel: el.querySelector("[name=titel]").value.trim(),
|
||
typ: el.querySelector("[name=typ]").value,
|
||
inhalt: el.querySelector("[name=inhalt]").value,
|
||
});
|
||
zu();
|
||
meldung("Akte " + antwort.aktenzeichen + " angelegt.", "gut");
|
||
oeffnePatient(behaelter, z.patient.patient.id, "akten");
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
} },
|
||
],
|
||
});
|
||
}
|
||
|
||
function akteBearbeitenFenster(behaelter) {
|
||
const a = z.offeneAkte.akte;
|
||
|
||
fenster({
|
||
titel: "Akte bearbeiten · " + a.aktenzeichen,
|
||
inhalt: `
|
||
<div class="feld"><label>Titel</label>
|
||
<input name="titel" maxlength="160" value="${sicher(a.titel)}"></div>
|
||
<div class="feld"><label>Inhalt</label>
|
||
<textarea name="inhalt" rows="12">${sicher(a.inhalt || "")}</textarea></div>
|
||
<div class="feld"><label>Was wurde geändert? (Pflicht)</label>
|
||
<input name="kommentar" maxlength="255"
|
||
placeholder="z. B. Befund vom Labor ergänzt">
|
||
<div class="feld-hinweis">Der Kommentar steht dauerhaft im Versionsverlauf.
|
||
Version ${a.version} bleibt unverändert erhalten.</div></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: "Neue Version speichern", klick: async (el, zu) => {
|
||
const kommentar = el.querySelector("[name=kommentar]").value.trim();
|
||
if (kommentar.length < 3) {
|
||
return meldung("Bitte angeben, was geändert wurde.", "fehler");
|
||
}
|
||
try {
|
||
await aufruf("mdt", "akteSpeichern", {
|
||
id: a.id,
|
||
titel: el.querySelector("[name=titel]").value.trim(),
|
||
inhalt: el.querySelector("[name=inhalt]").value,
|
||
kommentar,
|
||
});
|
||
zu();
|
||
meldung("Neue Version gespeichert.", "gut");
|
||
z.offeneAkte = await aufruf("mdt", "akteOeffnen", { id: a.id });
|
||
zeichneEinzelakte(behaelter.querySelector("#mdt-unterinhalt"), behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
} },
|
||
],
|
||
});
|
||
}
|
||
|
||
function akteStatusFenster(behaelter, status) {
|
||
const a = z.offeneAkte.akte;
|
||
const beschreibung = {
|
||
freigegeben: "Die Akte gilt damit als geprüft und freigegeben. Sie lässt sich weiterhin bearbeiten.",
|
||
geschlossen: "Eine geschlossene Akte lässt sich <b>nicht mehr ändern</b>. Der Inhalt bleibt lesbar.",
|
||
};
|
||
|
||
bestaetigen({
|
||
titel: status === "freigegeben" ? "Akte freigeben" : "Akte schließen",
|
||
gefahr: status === "geschlossen",
|
||
inhalt: `<p><b>${sicher(a.aktenzeichen)}</b> — ${sicher(a.titel)}</p>
|
||
<p>${beschreibung[status] || ""}</p>
|
||
<div class="feld"><label>Vermerk (freiwillig)</label>
|
||
<input name="grund" maxlength="255"></div>`,
|
||
bestaetigenText: status === "freigegeben" ? "Freigeben" : "Schließen",
|
||
bestaetigt: async (el, zu) => {
|
||
try {
|
||
await aufruf("mdt", "akteStatus", {
|
||
id: a.id, status,
|
||
grund: el.querySelector("[name=grund]").value.trim(),
|
||
});
|
||
zu();
|
||
meldung("Status geändert.", "gut");
|
||
z.offeneAkte = await aufruf("mdt", "akteOeffnen", { id: a.id });
|
||
zeichneEinzelakte(behaelter.querySelector("#mdt-unterinhalt"), behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
}
|
||
|
||
async function versionAnsehen(version) {
|
||
try {
|
||
const v = await aufruf("mdt", "akteVersion", { id: z.offeneAkte.akte.id, version });
|
||
|
||
fenster({
|
||
titel: `Version ${v.version} · ${z.offeneAkte.akte.aktenzeichen}`,
|
||
inhalt: `
|
||
<p class="feld-hinweis">${sicher(v.autor)} · ${zeit(v.geaendert_am)}
|
||
${v.kommentar ? " · " + sicher(v.kommentar) : ""}</p>
|
||
<div class="mdt-akte-inhalt">${v.inhalt
|
||
? sicher(v.inhalt).replace(/\n/g, "<br>")
|
||
: `<span class="feld-hinweis">Diese Version hatte keinen Inhalt.</span>`}</div>`,
|
||
});
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}
|
||
|
||
/* ---- Laden ----------------------------------------------------------------- */
|
||
|
||
async function oeffnePatient(behaelter, patientId, reiter) {
|
||
try {
|
||
z.patient = await aufruf("mdt", "oeffnen", { patient: patientId });
|
||
z.reiter = reiter || "stamm";
|
||
z.offeneAkte = null;
|
||
akteAnsicht(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}
|
||
|
||
async function rendern(behaelter) {
|
||
// Sprungziel aus einem anderen Modul oder aus einem Hinweis
|
||
const ziel = App.zustand.modulZiel;
|
||
if (ziel && ziel.patient) {
|
||
App.zustand.modulZiel = null;
|
||
return oeffnePatient(behaelter, ziel.patient);
|
||
}
|
||
if (ziel && ziel.akte) {
|
||
App.zustand.modulZiel = null;
|
||
try {
|
||
const akte = await aufruf("mdt", "akteOeffnen", { id: ziel.akte });
|
||
if (akte.akte.patient) {
|
||
await oeffnePatient(behaelter, akte.akte.patient, "akten");
|
||
z.offeneAkte = akte;
|
||
zeichneEinzelakte(behaelter.querySelector("#mdt-unterinhalt"), behaelter);
|
||
return;
|
||
}
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}
|
||
|
||
if (z.patient) akteAnsicht(behaelter);
|
||
else sucheAnsicht(behaelter);
|
||
}
|
||
|
||
App.registriereAnsicht("mdt", { rendern });
|
||
|
||
App.registriereSuche(async (begriff) => {
|
||
try {
|
||
const d = await aufruf("mdt", "suche", { begriff });
|
||
return (d.treffer || []).slice(0, 5).map((p) => ({
|
||
bereich: "Patient",
|
||
titel: p.firstname + " " + p.lastname,
|
||
unter: "geb. " + p.dob + (p.blutgruppe ? " · " + p.blutgruppe : ""),
|
||
}));
|
||
} catch (e) { return []; }
|
||
});
|
||
})();
|