- 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>
849 lines
33 KiB
JavaScript
849 lines
33 KiB
JavaScript
/* =====================================================================
|
||
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"));
|
||
}
|
||
},
|
||
});
|
||
})();
|