aether-framework/resources/[aether]/aether-medical/html/js/module/bericht.js
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

849 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
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 `<div class="reiter">
${eintraege.map((e) => `
<button class="${z.ordner === e.id ? "aktiv" : ""}" data-reiter="${e.id}">
${sicher(e.text)}${e.zahl > 0 ? `<span class="mdt-zahl">${e.zahl}</span>` : ""}
</button>`).join("")}
</div>`;
}
function kennzahlen(d) {
const zk = d.zaehler;
return `<div class="raster k4">
<div class="kennzahl ${zk.meine > 0 ? "warn" : ""}">
<div class="kz-titel">Eigene Entwürfe</div>
<div class="kz-wert">${zk.meine}</div>
<div class="kz-zusatz">nur du siehst sie</div>
</div>
<div class="kennzahl ${zk.freigabe > 0 ? "warn" : ""}">
<div class="kz-titel">Warten auf Freigabe</div>
<div class="kz-wert">${zk.freigabe}</div>
<div class="kz-zusatz">liegen zur Unterschrift</div>
</div>
<div class="kennzahl">
<div class="kz-titel">Davon von dir</div>
<div class="kz-wert">${zk.meineOffen}</div>
<div class="kz-zusatz">eingereicht, noch nicht gezeichnet</div>
</div>
<div class="kennzahl gut">
<div class="kz-titel">Heute freigegeben</div>
<div class="kz-wert">${zk.freigegeben}</div>
<div class="kz-zusatz">endgültig, unveränderlich</div>
</div>
</div>`;
}
/* ---- Übersicht ---------------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Berichte</h2>
<p>Das System schreibt keinen Satz. Der Weg vom Entwurf zur Freigabe wird
Schritt für Schritt gegangen — freigegeben wird durch Unterschrift,
nie durch Zeitablauf.</p>
</div>
${d.rechte.erstellen
? `<button class="knopf" data-neu="1">+ Bericht anlegen</button>` : ""}
</div>
${kennzahlen(d)}
${reiterleiste(d)}
<div class="filterleiste">
<input type="search" data-suche="1" placeholder="Nummer, Titel oder Patient…"
value="${sicher(z.suche)}">
<span class="feld-hinweis">Fremde Entwürfe sind nicht einsehbar —
erst mit dem Einreichen wird ein Bericht sichtbar.</span>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">${
z.ordner === "meine" ? "Deine offenen Entwürfe"
: z.ordner === "freigabe" ? "Eingereicht, wartet auf Unterschrift"
: z.ordner === "freigegeben" ? "Freigegebene Berichte" : "Alle Berichte"}</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 Berichte in diesem Ordner.</div>`}
</div>
</div>
`;
verdrahteUebersicht(behaelter);
}
function zeile(b) {
return `<div class="liste-zeile" data-oeffne="${b.id}">
<span class="db-nummer">${sicher(b.nummer)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(b.titel)}</div>
<div class="lz-unter">
${b.patientName ? sicher(b.patientName) : "ohne Patientenbezug"}
· ${sicher(b.autor)}
${b.behandlungKennung ? " · " + sicher(b.behandlungKennung) : ""}
${b.einsatzNummer ? " · " + sicher(b.einsatzNummer) : ""}
</div>
</div>
${b.vonMir ? `<span class="marker info">von dir</span>` : ""}
${b.hatDiagnose ? "" : `<span class="marker neutral">ohne Diagnose</span>`}
${b.unterschriften > 0
? `<span class="marker neutral">${b.unterschriften} Unterschrift(en)</span>` : ""}
<span class="marker ${statusArt[b.status] || "neutral"}">
${sicher(statusName(b.status))}</span>
<span class="db-zeit">${zeit(b.erstellt_am)}</span>
</div>`;
}
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 `<div class="beh-feld gesperrt">
<label>${sicher(feld.name)}</label>
<div class="beh-text ${wert ? "" : "leer"}">${wert
? sicher(wert).replace(/\n/g, "<br>") : "Nicht ausgefüllt."}</div>
</div>`;
}
return `<div class="beh-feld">
<label>${sicher(feld.name)}</label>
<textarea name="${sicher(feld.id)}" rows="6">${sicher(wert || "")}</textarea>
</div>`;
}
function einzelAnsicht(behaelter, d) {
const b = d.bericht;
const schreibbar = d.rechte.aendern;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>${sicher(b.titel)}</h2>
<p>${sicher(b.nummer)} · ${sicher(statusName(b.status))}
${b.patientName ? " · " + sicher(b.patientName)
+ (b.patientGeburt ? ", geb. " + sicher(b.patientGeburt) : "") : ""}</p>
</div>
<button class="knopf leise" data-zurueck="1">← Übersicht</button>
</div>
${b.status === "freigegeben" ? `
<div class="warnung">
<span class="w-symbol">🔒</span>
<div><b>Freigegeben.</b> Der Wortlaut steht fest und wird nicht mehr geändert.
Eine weitere Unterschrift ist möglich, eine Änderung nicht.</div>
</div>` : ""}
${b.status === "eingereicht" ? `
<div class="warnung warn">
<span class="w-symbol">✍</span>
<div><b>Liegt zur Freigabe.</b> Bis zur Unterschrift ist der Text gesperrt.
${b.vonMir ? "Zum Weiterschreiben musst du ihn zurückziehen." : ""}</div>
</div>` : ""}
${b.selbstFreigabe ? `
<div class="warnung warn">
<span class="w-symbol">ℹ</span>
<div>Verfasser und Freigabe sind dieselbe Person. Das ist zulässig und
steht so im Dokument.</div>
</div>` : ""}
<div class="beh-kopf">
<div class="mdt-kopf-werte">
<div class="mdt-wert"><span>Nummer</span><b>${sicher(b.nummer)}</b></div>
<div class="mdt-wert"><span>Verfasser</span><b>${sicher(b.autor)}</b></div>
<div class="mdt-wert"><span>Angelegt</span><b>${zeit(b.erstelltAm)}</b></div>
<div class="mdt-wert ${b.patientName ? "" : "fehlt"}"><span>Patient</span>
<b>${sicher(b.patientName || "kein Bezug")}</b></div>
<div class="mdt-wert ${b.behandlungKennung ? "" : "fehlt"}"><span>Behandlung</span>
<b>${sicher(b.behandlungKennung || "keine")}</b></div>
<div class="mdt-wert ${b.einsatzNummer ? "" : "fehlt"}"><span>Einsatz</span>
<b>${sicher(b.einsatzNummer || "keiner")}</b></div>
</div>
<div class="beh-kopf-aktionen">
${b.patient
? `<button class="knopf leise klein" data-akte="1">Patientenakte</button>` : ""}
${schreibbar ? `
<button class="knopf leise klein" data-baustein="1">🧩 Baustein</button>
<button class="knopf klein" data-speichern="1">Entwurf speichern</button>
${b.behandlung
? `<button class="knopf leise klein" data-uebernehmen="1">Aus Behandlung übernehmen</button>` : ""}
<button class="knopf leise klein" data-anlage="1">Anlage</button>
<button class="knopf klein" data-einreichen="1">Einreichen</button>` : ""}
${b.status === "eingereicht" && b.vonMir && d.rechte.erstellen
? `<button class="knopf leise klein" data-zurueckziehen="1">Zurückziehen</button>` : ""}
${b.status === "eingereicht" && d.rechte.freigeben ? `
<button class="knopf klein" data-freigeben="1">Freigeben</button>
<button class="knopf gefahr klein" data-zurueckweisen="1">Zurückweisen</button>` : ""}
${b.status !== "entwurf" && d.rechte.freigeben
? `<button class="knopf leise klein" data-gegenzeichnen="1">Gegenzeichnen</button>` : ""}
</div>
</div>
${schreibbar ? `
<div class="feld" style="margin-bottom:14px">
<label>Titel</label>
<input name="titel" maxlength="160" value="${sicher(b.titel)}">
</div>` : ""}
<div class="beh-bogen">
${d.felder.map((feld) => bogenFeld(feld, b[feld.id], schreibbar)).join("")}
</div>
<div class="db-spalten" style="margin-top:14px">
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Unterschriften</span>
<span class="feld-hinweis">${d.unterschriften.length} · nicht zurücknehmbar</span>
</div>
<div class="liste">
${d.unterschriften.length ? d.unterschriften.map((u) => `
<div class="liste-zeile">
<span class="marker ${u.art === "freigabe" ? "gut"
: u.art === "gegenzeichnung" ? "info" : "neutral"}">
${sicher(unterschriftArt[u.art] || u.art)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(u.name)}</div>
<div class="lz-unter">${sicher(u.rolle || "")}
${u.personalnummer ? " · " + sicher(u.personalnummer) : ""}</div>
</div>
<span class="db-zeit">${zeit(u.am)}</span>
</div>`).join("")
: `<div class="liste-leer">Noch nicht unterschrieben.</div>`}
</div>
<div class="karte-kopf" style="margin-top:14px">
<span class="karte-titel">Anlagen</span>
<span class="feld-hinweis">${d.anlagen.length}</span>
</div>
<div class="liste">
${d.anlagen.length ? d.anlagen.map((a, i) => `
<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.titel)}</div>
<div class="lz-unter">${sicher(a.von)} · ${zeit(a.am)}</div>
</div>
${schreibbar
? `<button class="knopf leise klein" data-anlageweg="${i + 1}">Entfernen</button>`
: ""}
</div>`).join("")
: `<div class="liste-leer">Keine Anlagen.</div>`}
</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Weg des Berichts</span>
<span class="feld-hinweis">aus dem Protokoll, unveränderlich</span>
</div>
<div class="liste">
${d.spur.length ? d.spur.map((s) => `
<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher(s.aktion)}</div>
<div class="lz-unter">${sicher(s.autor)}
${s.grund ? " · " + sicher(s.grund) : ""}</div>
</div>
<span class="db-zeit">${zeit(s.am)}</span>
</div>`).join("")
: `<div class="liste-leer">Noch kein Eintrag.</div>`}
</div>
</div>
</div>
`;
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: `
<div class="feld"><label>Titel</label>
<input name="titel" maxlength="160"
placeholder="z. B. Notarzteinsatzprotokoll, Verlaufsbericht…"></div>
<div class="feld"><label>Patient (freiwillig)</label>
<input name="suche" placeholder="Name oder Geburtsdatum…"></div>
<div class="liste pat-trefferliste" data-treffer="1">
<div class="liste-leer">Nicht jeder Bericht gehört zu einem Patienten.</div>
</div>
<div data-vorgaenge="1" style="display:none">
<div class="feld"><label>Behandlung verknüpfen (freiwillig)</label>
<div class="liste" data-behandlungen="1"></div></div>
<div class="feld"><label>Einsatz verknüpfen (freiwillig)</label>
<div class="liste" data-einsaetze="1"></div></div>
</div>
<div class="feld-hinweis">Eine Verknüpfung erlaubt es später, den Wortlaut
der Behandlung ausdrücklich zu übernehmen. Von selbst wird nichts kopiert.</div>`,
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) => `
<div class="liste-zeile" data-behandlung="${b.id}">
<span class="db-nummer">${sicher(b.kennung)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(b.diagnose || "ohne Diagnose")}</div>
<div class="lz-unter">${zeit(b.erstellt_am)}</div>
</div>
<span class="marker ${b.status === "laufend" ? "warn" : "gut"}">
${sicher(b.status)}</span>
</div>`).join("")
: `<div class="liste-leer">Keine Behandlung erfasst.</div>`;
const einListe = hg.querySelector("[data-einsaetze]");
einListe.innerHTML = antwort.einsaetze.length
? antwort.einsaetze.map((e) => `
<div class="liste-zeile" data-einsatz="${e.id}">
<span class="db-nummer">${sicher(e.nummer)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(e.titel)}</div>
<div class="lz-unter">${zeit(e.erstellt_am)}</div>
</div>
</div>`).join("")
: `<div class="liste-leer">Kein Einsatz verknüpft.</div>`;
// 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) => `
<div class="liste-zeile" data-person="${t.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(t.name)}</div>
<div class="lz-unter">geb. ${sicher(t.dob)}</div>
</div>
${t.blutgruppe
? `<span class="marker neutral">${sicher(t.blutgruppe)}</span>` : ""}
</div>`).join("")
: `<div class="liste-leer">Keine Person gefunden.</div>`;
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: `
<p>Aus <b>${sicher(b.behandlungKennung)}</b> wird der dort dokumentierte Text
in diesen Bericht kopiert.</p>
<div class="warnung">
<span class="w-symbol">ℹ</span>
<div><b>Das System formuliert nichts um</b> 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.</div>
</div>
<div class="feld-hinweis">Ungespeicherte Änderungen im Bogen gehen dabei
verloren — der Entwurf wird zuerst gespeichert.</div>`,
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: `
<div class="feld"><label>Bezeichnung</label>
<input name="titel" maxlength="120"
placeholder="z. B. Röntgenaufnahme Thorax"></div>
<div class="feld"><label>Adresse</label>
<input name="adresse" maxlength="500" placeholder="https://…"></div>
<div class="feld-hinweis">Die Anlage wird verlinkt, nicht hochgeladen.
Entfernen lässt sie sich nur, solange der Bericht ein Entwurf ist.</div>`,
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: `
<p><b>${sicher(b.nummer)}</b> geht zur Freigabe. Du unterschreibst ihn damit
als Verfasser.</p>
${leer.length ? `
<div class="warnung warn">
<span class="w-symbol">ℹ</span>
<div><b>Noch nicht ausgefüllt:</b> ${sicher(leer.join(", "))}.
Das hält die Einreichung nicht auf — es steht nur so im Protokoll.</div>
</div>` : ""}
<div class="warnung">
<span class="w-symbol">✍</span>
<div>Nach dem Einreichen ist der Text gesperrt. Zum Weiterschreiben musst du
den Bericht zurückziehen — das wird festgehalten.</div>
</div>`,
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: `
<p><b>${sicher(b.nummer)}</b> geht zurück in den Entwurf und lässt sich
wieder bearbeiten.</p>
<div class="warnung warn">
<span class="w-symbol">ℹ</span>
<div>Deine Unterschrift als Verfasser bleibt stehen — sie wurde geleistet.
Dass du danach weitergeschrieben hast, steht im Protokoll.</div>
</div>
<div class="feld"><label>Grund (Pflicht)</label>
<input name="grund" maxlength="255"
placeholder="Was fehlt noch, was war falsch?"></div>`,
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: `
<p>Du unterschreibst <b>${sicher(b.nummer)}</b> — „${sicher(b.titel)}",
verfasst von ${sicher(b.autor)}.</p>
<div class="warnung warn">
<span class="w-symbol">🔒</span>
<div><b>Die Freigabe ist endgültig.</b> Danach wird am Wortlaut nichts mehr
geändert — von niemandem.</div>
</div>
${b.vonMir ? `
<div class="warnung">
<span class="w-symbol">ℹ</span>
<div>Du hast diesen Bericht selbst geschrieben. Verfasser und Freigabe in
einer Person ist zulässig und wird so im Dokument vermerkt.</div>
</div>` : ""}
${leer.length ? `
<div class="warnung">
<span class="w-symbol">📄</span>
<div>Nicht ausgefüllt: ${sicher(leer.join(", "))}.</div>
</div>` : ""}
<div class="feld"><label>Vermerk (freiwillig)</label>
<input name="vermerk" maxlength="255"></div>`,
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: `
<p><b>${sicher(b.nummer)}</b> geht als Entwurf an ${sicher(b.autor)} zurück.</p>
<div class="feld"><label>Begründung (Pflicht)</label>
<textarea name="grund" rows="4" maxlength="500"
placeholder="Was muss ergänzt oder geändert werden?"></textarea>
<div class="feld-hinweis">Der Verfasser bekommt diese Begründung angezeigt.</div></div>`,
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: `
<p>Du setzt deine Unterschrift unter <b>${sicher(b.nummer)}</b>.</p>
<div class="warnung">
<span class="w-symbol">✍</span>
<div>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.</div>
</div>
<div class="feld"><label>Vermerk (freiwillig)</label>
<input name="vermerk" maxlength="255"></div>`,
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 = `<div class="seiten-kopf"><div><h2>Berichte</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
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"));
}
},
});
})();