- 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>
260 lines
11 KiB
JavaScript
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);
|
|
},
|
|
});
|
|
})();
|