/* =====================================================================
aether-medical / Ansicht: Berichte
Ein Bericht ist ein Dokument mit einer Unterschrift darunter. Kein
Satz darin entsteht von selbst: Text aus einer Behandlung wird nur
übernommen, wenn du es auslöst, und die Freigabe ist eine Unterschrift
von Hand. Ein freigegebener Bericht bleibt, wie er ist.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen } = App;
const z = {
ordner: "meine", // meine | freigabe | freigegeben | alle
suche: "",
offen: null, // geöffneter Bericht
daten: null,
};
// Marker-Art je Status
const statusArt = { entwurf: "neutral", eingereicht: "warn", freigegeben: "gut" };
// Beschriftung der Unterschriftsarten
const unterschriftArt = {
verfasser: "Verfasser", freigabe: "Freigabe", gegenzeichnung: "Gegengezeichnet",
};
/* ---- Kleine Helfer ---------------------------------------------------- */
/** Anzeigename eines Status */
function statusName(kennung) {
const treffer = (z.daten.status || []).find((s) => s.id === kennung);
return treffer ? treffer.name : kennung;
}
/* ---- Gerüst ------------------------------------------------------------ */
function reiterleiste(d) {
const eintraege = [
{ id: "meine", text: "Meine Entwürfe", zahl: d.zaehler.meine },
{ id: "freigabe", text: "Zur Freigabe", zahl: d.zaehler.freigabe },
{ id: "freigegeben", text: "Freigegeben", zahl: d.zaehler.freigegeben },
{ id: "alle", text: "Alle", zahl: 0 },
];
return `
${eintraege.map((e) => `
${sicher(e.text)}${e.zahl > 0 ? `${e.zahl} ` : ""}
`).join("")}
`;
}
function kennzahlen(d) {
const zk = d.zaehler;
return `
Eigene Entwürfe
${zk.meine}
nur du siehst sie
Warten auf Freigabe
${zk.freigabe}
liegen zur Unterschrift
Davon von dir
${zk.meineOffen}
eingereicht, noch nicht gezeichnet
Heute freigegeben
${zk.freigegeben}
endgültig, unveränderlich
`;
}
/* ---- Übersicht ---------------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
Berichte
Das System schreibt keinen Satz. Der Weg vom Entwurf zur Freigabe wird
Schritt für Schritt gegangen — freigegeben wird durch Unterschrift,
nie durch Zeitablauf.
${d.rechte.erstellen
? `
+ Bericht anlegen ` : ""}
${kennzahlen(d)}
${reiterleiste(d)}
Fremde Entwürfe sind nicht einsehbar —
erst mit dem Einreichen wird ein Bericht sichtbar.
${
z.ordner === "meine" ? "Deine offenen Entwürfe"
: z.ordner === "freigabe" ? "Eingereicht, wartet auf Unterschrift"
: z.ordner === "freigegeben" ? "Freigegebene Berichte" : "Alle Berichte"}
${d.liste.length} Einträge
${d.liste.length ? d.liste.map(zeile).join("")
: `
Keine Berichte in diesem Ordner.
`}
`;
verdrahteUebersicht(behaelter);
}
function zeile(b) {
return `
${sicher(b.nummer)}
${sicher(b.titel)}
${b.patientName ? sicher(b.patientName) : "ohne Patientenbezug"}
· ${sicher(b.autor)}
${b.behandlungKennung ? " · " + sicher(b.behandlungKennung) : ""}
${b.einsatzNummer ? " · " + sicher(b.einsatzNummer) : ""}
${b.vonMir ? `
von dir ` : ""}
${b.hatDiagnose ? "" : `
ohne Diagnose `}
${b.unterschriften > 0
? `
${b.unterschriften} Unterschrift(en) ` : ""}
${sicher(statusName(b.status))}
${zeit(b.erstellt_am)}
`;
}
function verdrahteUebersicht(behaelter) {
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
el.addEventListener("click", () => {
z.ordner = el.dataset.reiter;
rendern(behaelter);
});
});
const suche = behaelter.querySelector("[data-suche]");
let warten = null;
suche.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(() => {
z.suche = suche.value.trim();
rendern(behaelter);
}, 300);
});
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));
}
/* ---- Einzelansicht -------------------------------------------------------- */
/** Ein Feld des Berichtsbogens */
function bogenFeld(feld, wert, schreibbar) {
if (!schreibbar) {
return `
${sicher(feld.name)}
${wert
? sicher(wert).replace(/\n/g, " ") : "Nicht ausgefüllt."}
`;
}
return `
${sicher(feld.name)}
`;
}
function einzelAnsicht(behaelter, d) {
const b = d.bericht;
const schreibbar = d.rechte.aendern;
behaelter.innerHTML = `
${sicher(b.titel)}
${sicher(b.nummer)} · ${sicher(statusName(b.status))}
${b.patientName ? " · " + sicher(b.patientName)
+ (b.patientGeburt ? ", geb. " + sicher(b.patientGeburt) : "") : ""}
← Übersicht
${b.status === "freigegeben" ? `
🔒
Freigegeben. Der Wortlaut steht fest und wird nicht mehr geändert.
Eine weitere Unterschrift ist möglich, eine Änderung nicht.
` : ""}
${b.status === "eingereicht" ? `
✍
Liegt zur Freigabe. Bis zur Unterschrift ist der Text gesperrt.
${b.vonMir ? "Zum Weiterschreiben musst du ihn zurückziehen." : ""}
` : ""}
${b.selbstFreigabe ? `
ℹ
Verfasser und Freigabe sind dieselbe Person. Das ist zulässig und
steht so im Dokument.
` : ""}
Nummer ${sicher(b.nummer)}
Verfasser ${sicher(b.autor)}
Angelegt ${zeit(b.erstelltAm)}
Patient
${sicher(b.patientName || "kein Bezug")}
Behandlung
${sicher(b.behandlungKennung || "keine")}
Einsatz
${sicher(b.einsatzNummer || "keiner")}
${b.patient
? `Patientenakte ` : ""}
${schreibbar ? `
🧩 Baustein
Entwurf speichern
${b.behandlung
? `Aus Behandlung übernehmen ` : ""}
Anlage
Einreichen ` : ""}
${b.status === "eingereicht" && b.vonMir && d.rechte.erstellen
? `Zurückziehen ` : ""}
${b.status === "eingereicht" && d.rechte.freigeben ? `
Freigeben
Zurückweisen ` : ""}
${b.status !== "entwurf" && d.rechte.freigeben
? `Gegenzeichnen ` : ""}
${schreibbar ? `
Titel
` : ""}
${d.felder.map((feld) => bogenFeld(feld, b[feld.id], schreibbar)).join("")}
Unterschriften
${d.unterschriften.length} · nicht zurücknehmbar
${d.unterschriften.length ? d.unterschriften.map((u) => `
${sicher(unterschriftArt[u.art] || u.art)}
${sicher(u.name)}
${sicher(u.rolle || "")}
${u.personalnummer ? " · " + sicher(u.personalnummer) : ""}
${zeit(u.am)}
`).join("")
: `
Noch nicht unterschrieben.
`}
Anlagen
${d.anlagen.length}
${d.anlagen.length ? d.anlagen.map((a, i) => `
${sicher(a.titel)}
${sicher(a.von)} · ${zeit(a.am)}
${schreibbar
? `
Entfernen `
: ""}
`).join("")
: `
Keine Anlagen.
`}
Weg des Berichts
aus dem Protokoll, unveränderlich
${d.spur.length ? d.spur.map((s) => `
${sicher(s.aktion)}
${sicher(s.autor)}
${s.grund ? " · " + sicher(s.grund) : ""}
${zeit(s.am)}
`).join("")
: `
Noch kein Eintrag.
`}
`;
verdrahteEinzel(behaelter, d);
}
function verdrahteEinzel(behaelter, d) {
const b = d.bericht;
/** Liest die Felder des Bogens aus */
const bogen = () => {
const werte = { id: b.id };
const titel = behaelter.querySelector("[name=titel]");
if (titel) werte.titel = titel.value.trim();
d.felder.forEach((feld) => {
const el = behaelter.querySelector(`[name=${feld.id}]`);
if (el) werte[feld.id] = el.value.trim();
});
return werte;
};
behaelter.querySelector("[data-zurueck]").addEventListener("click", () => {
z.offen = null;
rendern(behaelter);
});
const akte = behaelter.querySelector("[data-akte]");
if (akte) {
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: "bericht", behaelter });
});
}
const speichern = behaelter.querySelector("[data-speichern]");
if (speichern) {
speichern.addEventListener("click", async () => {
try {
await aufruf("bericht", "speichern", bogen());
meldung("Entwurf gespeichert.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
});
}
const uebernehmen = behaelter.querySelector("[data-uebernehmen]");
if (uebernehmen) {
uebernehmen.addEventListener("click", () => uebernehmenFenster(behaelter, b, bogen));
}
const anlage = behaelter.querySelector("[data-anlage]");
if (anlage) anlage.addEventListener("click", () => anlageFenster(behaelter, b));
behaelter.querySelectorAll("[data-anlageweg]").forEach((el) => {
el.addEventListener("click", async () => {
try {
await aufruf("bericht", "anlage", { id: b.id, entfernen: +el.dataset.anlageweg });
meldung("Anlage entfernt.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
});
});
const einreichen = behaelter.querySelector("[data-einreichen]");
if (einreichen) {
einreichen.addEventListener("click", () => einreichenFenster(behaelter, b, d, bogen));
}
const zurueckziehen = behaelter.querySelector("[data-zurueckziehen]");
if (zurueckziehen) {
zurueckziehen.addEventListener("click", () => zurueckziehenFenster(behaelter, b));
}
const freigeben = behaelter.querySelector("[data-freigeben]");
if (freigeben) freigeben.addEventListener("click", () => freigebenFenster(behaelter, b, d));
const zurueckweisen = behaelter.querySelector("[data-zurueckweisen]");
if (zurueckweisen) {
zurueckweisen.addEventListener("click", () => zurueckweisenFenster(behaelter, b));
}
const gegenzeichnen = behaelter.querySelector("[data-gegenzeichnen]");
if (gegenzeichnen) {
gegenzeichnen.addEventListener("click", () => gegenzeichnenFenster(behaelter, b));
}
}
/* ---- Bericht anlegen ------------------------------------------------------- */
function anlegenFenster(behaelter) {
let patient = null; // gewählter Patient
let behandlung = null; // gewählte Behandlung
let einsatz = null; // gewählter Einsatz
bestaetigen({
titel: "Bericht anlegen",
inhalt: `
Titel
Patient (freiwillig)
Nicht jeder Bericht gehört zu einem Patienten.
Behandlung verknüpfen (freiwillig)
Einsatz verknüpfen (freiwillig)
Eine Verknüpfung erlaubt es später, den Wortlaut
der Behandlung ausdrücklich zu übernehmen. Von selbst wird nichts kopiert.
`,
bestaetigenText: "Anlegen",
bestaetigt: async (el, zu) => {
const titel = el.querySelector("[name=titel]").value.trim();
if (!titel) return meldung("Bitte einen Titel angeben.", "fehler");
try {
const antwort = await aufruf("bericht", "anlegen", {
titel, patient, behandlung, einsatz,
});
zu();
meldung(`Bericht ${antwort.nummer} angelegt.`, "gut");
z.ordner = "meine";
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 vorgaenge = hg.querySelector("[data-vorgaenge]");
let warten = null;
/** Lädt Behandlungen und Einsätze zum gewählten Patienten */
async function ladeVorgaenge() {
let antwort;
try {
antwort = await aufruf("bericht", "quellen", { patient });
} catch (f) { return meldung(f.message, "fehler"); }
vorgaenge.style.display = "";
const behListe = hg.querySelector("[data-behandlungen]");
behListe.innerHTML = antwort.behandlungen.length
? antwort.behandlungen.map((b) => `
${sicher(b.kennung)}
${sicher(b.diagnose || "ohne Diagnose")}
${zeit(b.erstellt_am)}
${sicher(b.status)}
`).join("")
: `Keine Behandlung erfasst.
`;
const einListe = hg.querySelector("[data-einsaetze]");
einListe.innerHTML = antwort.einsaetze.length
? antwort.einsaetze.map((e) => `
${sicher(e.nummer)}
${sicher(e.titel)}
${zeit(e.erstellt_am)}
`).join("")
: `Kein Einsatz verknüpft.
`;
// Ein zweiter Klick nimmt die Verknüpfung wieder zurück
behListe.querySelectorAll("[data-behandlung]").forEach((el) => {
el.addEventListener("click", () => {
const schon = behandlung === +el.dataset.behandlung;
behListe.querySelectorAll("[data-behandlung]")
.forEach((k) => k.classList.remove("gewaehlt"));
behandlung = schon ? null : +el.dataset.behandlung;
if (!schon) el.classList.add("gewaehlt");
});
});
einListe.querySelectorAll("[data-einsatz]").forEach((el) => {
el.addEventListener("click", () => {
const schon = einsatz === +el.dataset.einsatz;
einListe.querySelectorAll("[data-einsatz]")
.forEach((k) => k.classList.remove("gewaehlt"));
einsatz = schon ? null : +el.dataset.einsatz;
if (!schon) el.classList.add("gewaehlt");
});
});
}
suchfeld.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(async () => {
const begriff = suchfeld.value.trim();
if (begriff.length < 2) return;
try {
const antwort = await aufruf("bericht", "quellen", { begriff });
const treffer = antwort.treffer || [];
liste.innerHTML = treffer.length ? treffer.map((t) => `
${sicher(t.name)}
geb. ${sicher(t.dob)}
${t.blutgruppe
? `
${sicher(t.blutgruppe)} ` : ""}
`).join("")
: `Keine Person gefunden.
`;
liste.querySelectorAll("[data-person]").forEach((el) => {
el.addEventListener("click", () => {
patient = +el.dataset.person;
behandlung = null;
einsatz = null;
liste.querySelectorAll("[data-person]")
.forEach((k) => k.classList.remove("gewaehlt"));
el.classList.add("gewaehlt");
ladeVorgaenge();
});
});
} catch (f) { meldung(f.message, "fehler"); }
}, 280);
});
},
});
}
/* ---- Wortlaut übernehmen ----------------------------------------------------- */
function uebernehmenFenster(behaelter, b, bogen) {
bestaetigen({
titel: "Wortlaut aus der Behandlung übernehmen",
inhalt: `
Aus ${sicher(b.behandlungKennung)} wird der dort dokumentierte Text
in diesen Bericht kopiert.
ℹ
Das System formuliert nichts um und fasst nichts zusammen —
es kopiert den Wortlaut. Bereits ausgefüllte Felder bleiben unberührt;
nur leere werden gefüllt. Was danach im Bericht steht, verantwortest du.
Ungespeicherte Änderungen im Bogen gehen dabei
verloren — der Entwurf wird zuerst gespeichert.
`,
bestaetigenText: "Übernehmen",
bestaetigt: async (_, zu) => {
try {
// Erst sichern, was der Verfasser bereits geschrieben hat
await aufruf("bericht", "speichern", bogen());
const antwort = await aufruf("bericht", "uebernehmen", {
id: b.id, behandlung: b.behandlung,
});
zu();
meldung(`Übernommen: ${antwort.uebernommen.join(", ")}.`, "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Anlage ------------------------------------------------------------------- */
function anlageFenster(behaelter, b) {
bestaetigen({
titel: "Anlage hinzufügen",
inhalt: `
Bezeichnung
Adresse
Die Anlage wird verlinkt, nicht hochgeladen.
Entfernen lässt sie sich nur, solange der Bericht ein Entwurf ist.
`,
bestaetigenText: "Hinzufügen",
bestaetigt: async (el, zu) => {
try {
await aufruf("bericht", "anlage", {
id: b.id,
titel: el.querySelector("[name=titel]").value.trim(),
adresse: el.querySelector("[name=adresse]").value.trim(),
});
zu();
meldung("Anlage hinzugefügt.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Einreichen und zurückziehen ------------------------------------------------ */
function einreichenFenster(behaelter, b, d, bogen) {
// Welche Felder sind im Bogen gerade leer?
const werte = bogen();
const leer = d.felder.filter((f) => !werte[f.id]).map((f) => f.name);
bestaetigen({
titel: "Bericht einreichen",
inhalt: `
${sicher(b.nummer)} geht zur Freigabe. Du unterschreibst ihn damit
als Verfasser.
${leer.length ? `
ℹ
Noch nicht ausgefüllt: ${sicher(leer.join(", "))}.
Das hält die Einreichung nicht auf — es steht nur so im Protokoll.
` : ""}
✍
Nach dem Einreichen ist der Text gesperrt. Zum Weiterschreiben musst du
den Bericht zurückziehen — das wird festgehalten.
`,
bestaetigenText: "Unterschreiben und einreichen",
bestaetigt: async (_, zu) => {
try {
// Der zuletzt getippte Stand ist der, der eingereicht wird
await aufruf("bericht", "speichern", werte);
await aufruf("bericht", "einreichen", { id: b.id });
zu();
meldung("Bericht eingereicht.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
function zurueckziehenFenster(behaelter, b) {
bestaetigen({
titel: "Bericht zurückziehen",
inhalt: `
${sicher(b.nummer)} geht zurück in den Entwurf und lässt sich
wieder bearbeiten.
ℹ
Deine Unterschrift als Verfasser bleibt stehen — sie wurde geleistet.
Dass du danach weitergeschrieben hast, steht im Protokoll.
Grund (Pflicht)
`,
bestaetigenText: "Zurückziehen",
bestaetigt: async (el, zu) => {
const grund = el.querySelector("[name=grund]").value.trim();
if (!grund) return meldung("Bitte einen Grund angeben.", "fehler");
try {
await aufruf("bericht", "zurueckziehen", { id: b.id, grund });
zu();
meldung("Bericht zurückgezogen.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Freigabe ------------------------------------------------------------------- */
function freigebenFenster(behaelter, b, d) {
const leer = d.felder.filter((f) => !b[f.id]).map((f) => f.name);
bestaetigen({
titel: "Bericht freigeben",
inhalt: `
Du unterschreibst ${sicher(b.nummer)} — „${sicher(b.titel)}",
verfasst von ${sicher(b.autor)}.
🔒
Die Freigabe ist endgültig. Danach wird am Wortlaut nichts mehr
geändert — von niemandem.
${b.vonMir ? `
ℹ
Du hast diesen Bericht selbst geschrieben. Verfasser und Freigabe in
einer Person ist zulässig und wird so im Dokument vermerkt.
` : ""}
${leer.length ? `
📄
Nicht ausgefüllt: ${sicher(leer.join(", "))}.
` : ""}
Vermerk (freiwillig)
`,
bestaetigenText: "Freigeben",
bestaetigt: async (el, zu) => {
try {
await aufruf("bericht", "freigeben", {
id: b.id, vermerk: el.querySelector("[name=vermerk]").value.trim(),
});
zu();
meldung("Bericht freigegeben.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
function zurueckweisenFenster(behaelter, b) {
bestaetigen({
titel: "Bericht zurückweisen",
gefahr: true,
inhalt: `
${sicher(b.nummer)} geht als Entwurf an ${sicher(b.autor)} zurück.
Begründung (Pflicht)
Der Verfasser bekommt diese Begründung angezeigt.
`,
bestaetigenText: "Zurückweisen",
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("bericht", "zurueckweisen", { id: b.id, grund });
zu();
meldung("Bericht zurückgewiesen.", "gut");
z.offen = null;
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
function gegenzeichnenFenster(behaelter, b) {
bestaetigen({
titel: "Bericht gegenzeichnen",
inhalt: `
Du setzt deine Unterschrift unter ${sicher(b.nummer)} .
✍
Eine Gegenzeichnung ändert nichts am Wortlaut und lässt sich nicht
zurücknehmen. Sie sagt nur: du hast diesen Bericht gelesen und trägst ihn mit.
Vermerk (freiwillig)
`,
bestaetigenText: "Gegenzeichnen",
bestaetigt: async (el, zu) => {
try {
await aufruf("bericht", "gegenzeichnen", {
id: b.id, vermerk: el.querySelector("[name=vermerk]").value.trim(),
});
zu();
meldung("Gegengezeichnet.", "gut");
oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Laden -------------------------------------------------------------------- */
async function oeffne(behaelter, berichtId) {
try {
const d = await aufruf("bericht", "oeffnen", { id: berichtId });
z.offen = berichtId;
einzelAnsicht(behaelter, d);
} catch (f) {
meldung(f.message, "fehler");
z.offen = null;
}
}
async function rendern(behaelter) {
try {
z.daten = await aufruf("bericht", "uebersicht", {
ordner: z.ordner, suche: z.suche,
});
// Sprungziel aus einem Hinweis, z. B. „Bericht zurückgewiesen"
const ziel = App.zustand.modulZiel;
if (ziel && ziel.bericht) {
App.zustand.modulZiel = null;
return oeffne(behaelter, ziel.bericht);
}
if (z.offen) await oeffne(behaelter, z.offen);
else uebersicht(behaelter);
} catch (f) {
behaelter.innerHTML = `Berichte
Konnte nicht geladen werden: ${sicher(f.message)}
`;
}
}
App.registriereAnsicht("bericht", {
rendern,
aktualisieren() {
// Einen offenen Bogen nie überschreiben — dort steht ungespeicherter Text
if (App.zustand.aktivesModul === "bericht" && !z.offen
&& !document.querySelector(".fenster-hg")) {
rendern(App.$("inhalt"));
}
},
});
})();