/* =====================================================================
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 = `
Medizinisches MDT
Patientensuche über Name, Geburtsdatum oder Telefonnummer.
Was in einer Akte steht, hat ein Mediziner eingetragen.
Treffer
${z.treffer.length} Personen
${z.treffer.length ? z.treffer.map(trefferZeile).join("")
: `
${z.suche.length >= 2
? "Keine Person gefunden."
: "Mindestens zwei Zeichen eingeben."}
`}
`;
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 `
${p.lichtbild
? `
`
: `
— `}
${sicher(p.firstname)} ${sicher(p.lastname)}
geb. ${sicher(p.dob)}
${p.telefon ? " · ☎ " + sicher(p.telefon) : ""}
${p.blutgruppe ? " · " + sicher(p.blutgruppe) : " · Blutgruppe unbekannt"}
${p.hinweis ? `
Warnhinweis ` : ""}
${p.allergien ? `
Allergien ` : ""}
${p.imHaus ? `
im Haus ` : ""}
${p.wartet ? `
wartet ` : ""}
`;
}
/* ---- Patientenakte ------------------------------------------------------ */
function akteAnsicht(behaelter) {
const d = z.patient;
const p = d.patient, s = d.stamm;
behaelter.innerHTML = `
${sicher(p.name)}
geb. ${sicher(p.geburt)} · ${sicher(p.geschlecht === "w" ? "weiblich" : "männlich")}
${p.telefon ? " · ☎ " + sicher(p.telefon) : ""}
← Suche
${s.hinweis ? `
⚠
Warnhinweis — ${sicher(s.hinweis)}
${s.gepflegtVon ? `Eingetragen von
${sicher(s.gepflegtVon)}${s.gepflegtAm ? " am " + zeit(s.gepflegtAm) : ""}.
Das System hat diesen Hinweis nicht erzeugt. ` : ""}
` : ""}
${d.aufnahme ? `
🏥
${d.aufnahme.status === "wartend"
? "Wartet auf Aufnahme" : "Aufgenommen"}
— ${sicher(d.aufnahme.grund)}
${d.aufnahme.zimmer ? " · Zimmer " + sicher(d.aufnahme.zimmer) : ""}
Seit ${zeit(d.aufnahme.aufgenommen_am)}
durch ${sicher(d.aufnahme.aufgenommenVon)}.
` : ""}
${d.intensiv ? `
🫀
Auf Intensivstation — Zustand als
${sicher(d.intensiv.zustand)} eingetragen
${d.intensiv.verantwortlichName
? " · zuständig: " + sicher(d.intensiv.verantwortlichName) : ""}
` : ""}
${s.lichtbild
? `
`
: `
Kein Lichtbild `}
${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")}
${d.rechte.stammdaten
? `Stammdaten pflegen ` : ""}
${d.rechte.hinweis
? `
${s.hinweis ? "Warnhinweis ändern" : "Warnhinweis setzen"} ` : ""}
Übersicht
Verlauf
Akten ${d.zaehler.akten}
Vermerke ${d.zaehler.notizen}
`;
verdrahteAkte(behaelter);
zeichneReiter(behaelter);
}
/** Eine Zeile im Stammdatenblock */
function wertZeile(name, wert, ersatz) {
const fehlt = !wert;
return `
${sicher(name)}
${fehlt ? sicher(ersatz || "nicht erfasst") : sicher(wert)}
`;
}
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 = `${sicher(f.message)}
`;
}
}
function zeichneUebersicht(ziel) {
const d = z.patient;
const zaehler = d.zaehler;
ziel.innerHTML = `
Behandlungen
${zaehler.behandlungen}
dokumentiert
Operationen
${zaehler.operationen}
dokumentiert
Laboraufträge
${zaehler.labor}
gestellt
Aufnahmen
${zaehler.aufnahmen}
insgesamt
Akten
${zaehler.akten}
angelegt
Vermerke
${zaehler.notizen}
geschrieben
Fehlende Angaben bedeuten „nicht erhoben", nicht „unauffällig".
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.
`;
}
function zeichneVerlauf(ziel) {
const eintraege = z.unterdaten.eintraege || [];
ziel.innerHTML = eintraege.length ? `
Zeitleiste
${z.unterdaten.gesamt} Einträge insgesamt
${eintraege.map((e) => {
const stil = verlaufsArt[e.art] || { symbol: "•", art: "neutral" };
return `
${stil.symbol}
${sicher(e.titel)}
${zeit(e.am)}
${e.text ? `
${sicher(e.text)}
` : ""}
${e.marker ? `
${sicher(e.marker)}
` : ""}
`;
}).join("")}
`
: `Für diesen Patienten ist noch nichts dokumentiert.
`;
}
function zeichneAkten(ziel, behaelter) {
const d = z.unterdaten;
// Eine einzelne Akte ist geöffnet
if (z.offeneAkte) return zeichneEinzelakte(ziel, behaelter);
ziel.innerHTML = `
Akten dieses Patienten
${z.patient.rechte.akteErstellen
? `+ Akte anlegen ` : ""}
${d.akten.length ? d.akten.map((a) => `
${sicher(a.aktenzeichen)}
${sicher(a.titel)}
${sicher(a.typName)} · ${sicher(a.autor)}
· ${zeit(a.erstellt_am)}
v${a.version}
${sicher(a.status)}
`).join("")
: `
Noch keine Akte angelegt.
`}
`;
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 = `
${sicher(a.aktenzeichen)} · Version ${a.version}
${d.rechte.bearbeiten && a.status !== "geschlossen"
? `Bearbeiten ` : ""}
${d.rechte.freigeben && a.status !== "freigegeben" && a.status !== "geschlossen"
? `Freigeben ` : ""}
${d.rechte.schliessen && a.status !== "geschlossen"
? `Schließen ` : ""}
← Akten
${sicher(a.titel)}
${sicher(a.status)}
Angelegt von ${sicher(a.autor)} am ${zeit(a.erstelltAm)}
${a.freigabeVon ? ` · freigegeben von ${sicher(a.freigabeVon)}
am ${zeit(a.freigabeAm)}` : ""}
${a.inhalt
? sicher(a.inhalt).replace(/\n/g, " ")
: `Diese Akte hat noch keinen Inhalt. `}
Versionsverlauf
Nichts wird überschrieben
${d.versionen.map((v) => `
v${v.version}
${sicher(v.kommentar || "ohne Kommentar")}
${sicher(v.autor)} · ${zeit(v.geaendert_am)}
${v.version === a.version
? `
aktuell `
: `
ansehen `}
`).join("")}
`;
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 = `
Vermerke
+ Vermerk
${notizen.length ? notizen.map((n) => `
${sicher(n.text)}
${sicher(n.autor)}
${n.personalnummer ? " (" + sicher(n.personalnummer) + ")" : ""}
· ${zeit(n.am)}
${n.eigene ? `
von dir ` : ""}
`).join("")
: `
Keine Vermerke.
`}
`;
ziel.querySelector("[data-neu-notiz]").addEventListener("click", () => {
fenster({
titel: "Vermerk anlegen",
inhalt: `Vermerk
Ein Vermerk ist keine Diagnose und ersetzt
keine Behandlungsdokumentation.
`,
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: `
Blutgruppe
— nicht bestimmt —
${d.blutgruppen.map((b) =>
`
${sicher(b)} `).join("")}
Wird nicht aus dem Labor übernommen — die
Feststellung verantwortet ein Mediziner.
Organspende
${Object.keys(d.organstatus).map((k) =>
`
${sicher(d.organstatus[k])} `).join("")}
Allergien
Vorerkrankungen
Dauermedikation
Implantate
Notfallkontakt
Lichtbild (Bild-Adresse)
`,
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: `
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.
Warnhinweis (leer lassen zum Entfernen)
Begründung
`,
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: `
Titel
Aktenart
${arten.map((a) =>
`${sicher(a.name)} `).join("")}
Inhalt
Die Akte startet als Entwurf. Jede spätere Änderung
erzeugt eine neue Version — überschrieben wird nichts.
`,
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: `
Titel
Inhalt
`,
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 nicht mehr ändern . Der Inhalt bleibt lesbar.",
};
bestaetigen({
titel: status === "freigegeben" ? "Akte freigeben" : "Akte schließen",
gefahr: status === "geschlossen",
inhalt: `${sicher(a.aktenzeichen)} — ${sicher(a.titel)}
${beschreibung[status] || ""}
Vermerk (freiwillig)
`,
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: `
${sicher(v.autor)} · ${zeit(v.geaendert_am)}
${v.kommentar ? " · " + sicher(v.kommentar) : ""}
${v.inhalt
? sicher(v.inhalt).replace(/\n/g, " ")
: `Diese Version hatte keinen Inhalt. `}
`,
});
} 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 []; }
});
})();