aether-framework/resources/[aether]/aether-medical/html/js/module/dashboard.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

260 lines
11 KiB
JavaScript

/* =====================================================================
aether-medical / Ansicht: Übersicht
Der Lagebericht der Klinik. Jede Zahl kommt frisch vom Server; keine
ist fest verdrahtet. Die Warnungen betreffen ausschliesslich Bestände,
Fristen und Besetzung — nie den Zustand eines Patienten, den das
System selbst bewertet hätte.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, minuten, aufruf, meldung } = App;
const z = { daten: null, behaelter: null };
/* ---- Bausteine ------------------------------------------------------ */
/**
* Kennzahlkachel
* @param {string|number} wert Angezeigter Wert
* @param {string} titel Beschriftung
* @param {string} zusatz Zusatzzeile
* @param {string} art "", "gut", "warn", "hoch"
* @param {string} ziel Modul, in das ein Klick springt
*/
function kachel(wert, titel, zusatz, art, ziel) {
return `<div class="kennzahl ${art || ""} ${ziel ? "klickbar" : ""}"
${ziel ? `data-springe="${sicher(ziel)}"` : ""}>
<div class="kz-titel">${sicher(titel)}</div>
<div class="kz-wert">${sicher(wert)}</div>
${zusatz ? `<div class="kz-zusatz">${sicher(zusatz)}</div>` : ""}
</div>`;
}
/** Waagerechter Belegungsbalken */
function belegungsBalken(name, belegt, gesamt) {
const anteil = gesamt > 0 ? Math.round((belegt / gesamt) * 100) : 0;
// Je voller, desto dringlicher die Farbe
const farbe = anteil >= 90 ? "var(--kritisch)"
: anteil >= 70 ? "var(--warn)" : "var(--gut)";
return `<div class="db-bel-zeile">
<span class="db-bel-name">${sicher(name)}</span>
<div class="db-bel-balken"><i style="width:${anteil}%;background:${farbe}"></i></div>
<span class="db-bel-zahl">${belegt}/${gesamt}</span>
</div>`;
}
/** Name einer Station anhand ihrer Kennung */
function stationsName(id) {
const treffer = (App.zustand.stationen || []).find((s) => s.id === id);
return treffer ? treffer.name : (id || "—");
}
/* ---- Zeichnen -------------------------------------------------------- */
function zeichne(behaelter) {
const d = z.daten;
const dienst = d.dienst, ein = d.einsaetze, pat = d.patienten;
const bel = d.belegung, offen = d.offen;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Übersicht</h2>
<p>Stand der Klinik. Alle Zahlen sind Momentaufnahmen —
sie lösen nichts aus und ersetzen keine Einschätzung am Patienten.</p>
</div>
<div class="db-zeit">${zeit(new Date().toISOString())}</div>
</div>
${d.warnungen.length ? `<div class="db-warnungen">
${d.warnungen.map((w) => `
<div class="warnung ${sicher(w.art)} ${w.ziel ? "klickbar" : ""}"
${w.ziel ? `data-springe="${sicher(w.ziel)}"` : ""}>
<span class="w-symbol">${w.art === "kritisch" ? "⚠" : "❗"}</span>
<div><b>${sicher(w.titel)}</b> — ${sicher(w.text)}</div>
</div>`).join("")}
</div>` : ""}
<div class="raster k4">
${kachel(dienst.imDienst, "Im Dienst",
dienst.leitstelle ? "Leitstelle: " + dienst.leitstelle : "Leitstelle unbesetzt",
dienst.imDienst === 0 ? "hoch" : "gut")}
${kachel(dienst.notaerzte, "Notärzte",
dienst.aufRtw + " auf RTW · " + dienst.imOp + " im OP")}
${kachel(ein.offen, "Offene Notrufe",
ein.laufend + " laufend · " + ein.dringend + " dringend",
ein.offen > 0 ? "warn" : "", "leitstelle")}
${kachel(pat.aufgenommen, "Patienten im Haus",
pat.wartend + " warten auf Aufnahme", "", "patienten")}
</div>
<div class="raster k4">
${kachel(pat.intensiv, "Intensivpatienten",
pat.kritisch + " als kritisch eingetragen",
pat.kritisch > 0 ? "hoch" : "", "intensiv")}
${kachel(bel.opLaufend + "/" + bel.opSaeleGesamt, "OP-Säle belegt",
bel.opGeplant + " geplant", "", "operation")}
${kachel(bel.bettenFrei, "Freie Betten",
"von " + bel.bettenGesamt + " insgesamt",
bel.bettenFrei === 0 ? "hoch" : bel.bettenFrei <= 2 ? "warn" : "gut", "klinik")}
${kachel(bel.behandlungFrei, "Freie Behandlungsräume",
"von " + bel.behandlungGesamt + " insgesamt",
bel.behandlungFrei === 0 ? "warn" : "", "klinik")}
</div>
<div class="raster k4">
${kachel(offen.dokumentationen, "Offene Dokumentationen",
offen.eigeneDoku + " davon von dir",
offen.eigeneDoku > 0 ? "warn" : "", "behandlung")}
${kachel(offen.laborOffen, "Laboraufträge offen",
offen.laborUnbefundet + " warten auf Bewertung", "", "labor")}
${kachel(offen.berichteEntwurf, "Eigene Berichtsentwürfe",
"noch nicht eingereicht",
offen.berichteEntwurf > 0 ? "warn" : "", "bericht")}
${kachel(pat.laengsteWartezeit > 0 ? minuten(pat.laengsteWartezeit) : "—",
"Längste Wartezeit",
pat.wartend + " Patienten warten",
pat.laengsteWartezeit >= 30 ? "hoch"
: pat.laengsteWartezeit >= 10 ? "warn" : "", "patienten")}
</div>
<div class="db-spalten">
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Wartende Patienten</span>
<span class="feld-hinweis">Aufnahme entscheidet ein Mediziner</span>
</div>
<div class="liste">
${pat.warteliste.length ? pat.warteliste.map((p) => `
<div class="liste-zeile">
<span class="prio p${sicher(p.prioritaet)}"></span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(p.firstname)} ${sicher(p.lastname)}</div>
<div class="lz-unter">${sicher(String(p.grund).slice(0, 70))}</div>
</div>
<span class="db-zeit">wartet ${minuten(p.wartet)}</span>
</div>`).join("")
: `<div class="liste-leer">Niemand wartet auf Aufnahme.</div>`}
</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Offene und laufende Einsätze</span>
<span class="feld-hinweis">Zuweisung durch die Leitstelle</span>
</div>
<div class="liste">
${ein.liste.length ? ein.liste.map((e) => `
<div class="liste-zeile">
<span class="prio p${sicher(e.prioritaet)}"></span>
<span class="db-nummer">${sicher(e.nummer)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(e.titel)}</div>
<div class="lz-unter">${sicher(e.ort || "Ort offen")}</div>
</div>
<span class="marker ${e.status === "offen" ? "warn" : "info"}">
${e.status === "offen" ? "offen" : "laufend"}</span>
</div>`).join("")
: `<div class="liste-leer">Keine offenen Einsätze.</div>`}
</div>
</div>
</div>
<div class="db-spalten" style="margin-top:14px">
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Belegung</span></div>
<div class="db-belegung">
${belegungsBalken("Betten", bel.bettenBelegt, bel.bettenGesamt)}
${belegungsBalken("Behandlungsräume",
bel.behandlungGesamt - bel.behandlungFrei, bel.behandlungGesamt)}
${belegungsBalken("OP-Säle", bel.opLaufend, bel.opSaeleGesamt)}
</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Kollegen im Dienst</span>
<span class="feld-hinweis">${dienst.imDienst} anwesend</span>
</div>
<div class="liste">
${dienst.liste.length ? dienst.liste.slice(0, 8).map((m) => `
<div class="liste-zeile">
<span class="db-dienstpunkt" style="background:${farbeZuStatus(m.status)}"></span>
<span class="db-nummer">${sicher(m.personalnummer)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(m.name)}</div>
<div class="lz-unter">${sicher(m.rolleName)}
· ${sicher(stationsName(m.station))}</div>
</div>
${m.leitstelle ? `<span class="marker info">Leitstelle</span>` : ""}
<span class="marker neutral">${sicher(funktionsName(m.funktion))}</span>
</div>`).join("")
: `<div class="liste-leer">Niemand im Dienst.</div>`}
</div>
</div>
</div>
<div class="db-grundsatz">
<b>Das System unterstützt medizinische Entscheidungen — es trifft niemals selbst
welche.</b> Es nimmt niemanden auf, entlässt niemanden, stellt keine Diagnose,
gibt kein Medikament aus und stellt keinen Tod fest. Wartezeiten und Fristen
laufen ab, ohne dass etwas passiert, bis ein Mediziner entscheidet.
</div>
`;
verdrahte(behaelter);
}
/** Farbe zum Dienststatus aus der Konfiguration */
function farbeZuStatus(status) {
const treffer = (App.zustand.status || []).find((s) => s.id === status);
return treffer ? treffer.farbe : "var(--flaeche-3)";
}
/** Klartextname einer Dienstfunktion */
function funktionsName(funktion) {
const treffer = (App.zustand.funktionen || []).find((f) => f.id === funktion);
return treffer ? treffer.name : (funktion || "—");
}
function verdrahte(behaelter) {
// Kacheln und Warnungen springen in das zuständige Modul
behaelter.querySelectorAll("[data-springe]").forEach((el) => {
el.addEventListener("click", () => {
const ziel = el.dataset.springe;
if (!App.zustand.module.some((m) => m.id === ziel)) {
meldung("Dieses Modul steht dir nicht zur Verfügung.", "fehler");
return;
}
App.oeffneModul(ziel);
});
});
}
/* ---- Laden ------------------------------------------------------------ */
async function rendern(behaelter) {
z.behaelter = behaelter;
try {
z.daten = await aufruf("dashboard", "laden", {});
// Hinweise aus dem ersten Laden in den Kern übernehmen
App.zustand.hinweise = z.daten.hinweise || [];
zeichne(behaelter);
} catch (f) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Übersicht</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("dashboard", {
rendern,
// Wird gerufen, wenn sich Dienstliste oder Einsätze ändern
aktualisieren() {
if (z.behaelter && App.zustand.aktivesModul === "dashboard") rendern(z.behaelter);
},
});
})();