- 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>
629 lines
21 KiB
JavaScript
629 lines
21 KiB
JavaScript
/* =====================================================================
|
||
aether-medical — Oberflächenkern
|
||
|
||
Der Kern kennt keine Medizin. Er bringt die Hülle mit (Seitenleiste,
|
||
Kopfleiste, Dialoge, Mitteilungen) und ruft Module auf, die sich
|
||
selbst über `App.registriereAnsicht(...)` anmelden.
|
||
|
||
Läuft die Datei im Browser statt in FiveM, schaltet sie in eine
|
||
Vorschau um — so bleibt die Oberfläche eigenständig ansehbar.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
||
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-medical";
|
||
|
||
const App = (() => {
|
||
|
||
/* ---- Zustand ------------------------------------------------------ */
|
||
const zustand = {
|
||
imDienst: false,
|
||
mitarbeiter: null,
|
||
module: [],
|
||
aktivesModul: null,
|
||
modulZiel: null, // Sprungziel für die geöffnete Ansicht
|
||
hinweise: [],
|
||
dienstliste: [],
|
||
status: [],
|
||
funktionen: [],
|
||
stationen: [],
|
||
};
|
||
|
||
// Registrierte Modulansichten: [modulId] = { rendern, aktualisieren? }
|
||
const ansichten = {};
|
||
// Anbieter für die globale Suche
|
||
const suchAnbieter = [];
|
||
// Offene Aufrufe an den Server: [ruf] = { aufloesen, ablehnen }
|
||
const offeneRufe = {};
|
||
let rufZaehler = 0;
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
/* ---- Kleine Helfer ------------------------------------------------ */
|
||
|
||
/** Maskiert Text, damit er gefahrlos in HTML landen kann */
|
||
function sicher(text) {
|
||
if (text === null || text === undefined) return "";
|
||
return String(text).replace(/[&<>"']/g,
|
||
(z) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[z]));
|
||
}
|
||
|
||
/** Formatiert einen Zeitpunkt aus der Datenbank */
|
||
function zeit(wert) {
|
||
if (!wert) return "—";
|
||
const d = new Date(String(wert).replace(" ", "T"));
|
||
if (isNaN(d)) return sicher(wert);
|
||
return d.toLocaleString("de-DE",
|
||
{ day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||
}
|
||
|
||
/** Formatiert eine Dauer in Sekunden */
|
||
function dauer(sekunden) {
|
||
const s = Math.max(0, Math.floor(sekunden || 0));
|
||
const std = Math.floor(s / 3600);
|
||
const min = Math.floor((s % 3600) / 60);
|
||
return (std > 0 ? std + " Std " : "") + min + " Min";
|
||
}
|
||
|
||
/** Formatiert Minuten als lesbare Wartezeit */
|
||
function minuten(wert) {
|
||
const m = Math.max(0, Math.floor(wert || 0));
|
||
if (m < 60) return m + " Min";
|
||
return Math.floor(m / 60) + " Std " + (m % 60) + " Min";
|
||
}
|
||
|
||
/** Formatiert einen Betrag als Euro */
|
||
function euro(betrag) {
|
||
return (Math.round(betrag || 0)).toLocaleString("de-DE",
|
||
{ style: "currency", currency: "EUR", maximumFractionDigits: 0 });
|
||
}
|
||
|
||
/** Schickt etwas an den FiveM-Client */
|
||
function anClient(pfad, daten) {
|
||
if (!IST_FIVEM) return Promise.resolve({});
|
||
return fetch(`https://${RESSOURCE}/${pfad}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
||
body: JSON.stringify(daten || {}),
|
||
}).then((a) => a.json()).catch(() => ({}));
|
||
}
|
||
|
||
/* ---- Aufruf an ein Servermodul ------------------------------------ */
|
||
|
||
/**
|
||
* Ruft eine Modulaktion auf und wartet auf die Antwort.
|
||
* @param {string} modul Modulkennung
|
||
* @param {string} aktion Name der Aktion
|
||
* @param {object} daten Nutzdaten
|
||
* @returns {Promise<object>}
|
||
*/
|
||
function aufruf(modul, aktion, daten) {
|
||
return new Promise((aufloesen, ablehnen) => {
|
||
if (!IST_FIVEM) {
|
||
// Browser-Vorschau: es gibt keinen Server zum Fragen
|
||
ablehnen(new Error("Vorschaumodus — keine Verbindung zum Server."));
|
||
return;
|
||
}
|
||
|
||
const ruf = ++rufZaehler;
|
||
offeneRufe[ruf] = { aufloesen, ablehnen };
|
||
|
||
anClient("aufruf", { modul, aktion, daten: daten || {}, ruf });
|
||
|
||
// Nicht ewig warten, falls eine Antwort ausbleibt
|
||
setTimeout(() => {
|
||
if (offeneRufe[ruf]) {
|
||
delete offeneRufe[ruf];
|
||
ablehnen(new Error("Zeitüberschreitung — der Server hat nicht geantwortet."));
|
||
}
|
||
}, 15000);
|
||
});
|
||
}
|
||
|
||
/* ---- Kurzmeldungen ------------------------------------------------ */
|
||
|
||
/**
|
||
* Zeigt eine kurze Meldung.
|
||
* @param {string} text Inhalt
|
||
* @param {string} art "gut" | "fehler" | ""
|
||
*/
|
||
function meldung(text, art) {
|
||
const el = document.createElement("div");
|
||
el.className = "meldung " + (art || "");
|
||
el.textContent = text;
|
||
$("meldungen").appendChild(el);
|
||
setTimeout(() => el.remove(), 3200);
|
||
}
|
||
|
||
/* ---- Fenster (Dialoge) -------------------------------------------- */
|
||
|
||
/**
|
||
* Öffnet ein Dialogfenster.
|
||
* @param {object} o { titel, inhalt, knoepfe: [{text, art?, klick}], nachOeffnen? }
|
||
*/
|
||
function fenster(o) {
|
||
const ebene = $("fenster-ebene");
|
||
const hg = document.createElement("div");
|
||
hg.className = "fenster-hg";
|
||
|
||
const knoepfe = (o.knoepfe || []).map((k, i) =>
|
||
`<button class="knopf ${k.art || "leise"}" data-k="${i}">${sicher(k.text)}</button>`
|
||
).join("");
|
||
|
||
hg.innerHTML =
|
||
`<div class="fenster">
|
||
<div class="fenster-kopf">
|
||
<h3>${sicher(o.titel || "")}</h3>
|
||
<button class="knopf leise klein" data-zu="1">✕</button>
|
||
</div>
|
||
<div class="fenster-koerper">${o.inhalt || ""}</div>
|
||
${knoepfe ? `<div class="fenster-fuss">${knoepfe}</div>` : ""}
|
||
</div>`;
|
||
|
||
ebene.appendChild(hg);
|
||
|
||
const schliessen = () => hg.remove();
|
||
hg.querySelector("[data-zu]").addEventListener("click", schliessen);
|
||
hg.addEventListener("click", (e) => { if (e.target === hg) schliessen(); });
|
||
|
||
hg.querySelectorAll("[data-k]").forEach((b) => {
|
||
b.addEventListener("click", () => {
|
||
const eintrag = o.knoepfe[+b.dataset.k];
|
||
// Der Rückruf entscheidet selbst, ob das Fenster zugeht
|
||
if (eintrag.klick) eintrag.klick(hg, schliessen);
|
||
else schliessen();
|
||
});
|
||
});
|
||
|
||
if (o.nachOeffnen) o.nachOeffnen(hg);
|
||
return { element: hg, schliessen };
|
||
}
|
||
|
||
/**
|
||
* Fenster mit Bestätigungspflicht.
|
||
* Für jede Maßnahme, die ein Mediziner selbst verantworten muss.
|
||
*/
|
||
function bestaetigen(o) {
|
||
return fenster({
|
||
titel: o.titel,
|
||
inhalt: `${o.inhalt || ""}
|
||
<div class="grundsatz">
|
||
<span>ℹ</span>
|
||
<div><b>Diese Entscheidung triffst du.</b> Das System hat den Vorgang nur
|
||
vorbereitet — dokumentiert wird er erst mit deiner Bestätigung.</div>
|
||
</div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
|
||
{ text: o.bestaetigenText || "Bestätigen", art: o.gefahr ? "gefahr" : "",
|
||
klick: (el, zu) => { if (o.bestaetigt) o.bestaetigt(el, zu); } },
|
||
],
|
||
nachOeffnen: o.nachOeffnen,
|
||
});
|
||
}
|
||
|
||
/* ---- Navigation ---------------------------------------------------- */
|
||
|
||
function zeichneNavigation() {
|
||
const nav = $("navigation");
|
||
|
||
nav.innerHTML = zustand.module.map((m) =>
|
||
`<button class="${zustand.aktivesModul === m.id ? "aktiv" : ""}"
|
||
data-modul="${sicher(m.id)}" title="${sicher(m.beschreibung || m.name)}">
|
||
<span class="nav-icon">${sicher(m.icon)}</span>
|
||
<span class="nav-text">${sicher(m.name)}</span>
|
||
</button>`).join("");
|
||
|
||
nav.querySelectorAll("[data-modul]").forEach((el) => {
|
||
el.addEventListener("click", () => oeffneModul(el.dataset.modul));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Öffnet ein Modul.
|
||
* @param {string} modulId Modul
|
||
* @param {object|null} ziel Sprungziel, z. B. { patient: 12 }.
|
||
* Die Ansicht liest ihn über App.zustand.modulZiel und räumt ihn auf.
|
||
*/
|
||
async function oeffneModul(modulId, ziel) {
|
||
const ansicht = ansichten[modulId];
|
||
if (!ansicht) {
|
||
meldung("Dieses Modul ist nicht geladen.", "fehler");
|
||
return;
|
||
}
|
||
|
||
zustand.aktivesModul = modulId;
|
||
zustand.modulZiel = ziel || null;
|
||
zeichneNavigation();
|
||
|
||
const behaelter = $("inhalt");
|
||
behaelter.innerHTML = `<div class="laden"><div class="spinner"></div><span>Lade …</span></div>`;
|
||
|
||
try {
|
||
await ansicht.rendern(behaelter);
|
||
} catch (fehler) {
|
||
behaelter.innerHTML =
|
||
`<div class="seiten-kopf"><div><h2>Fehler</h2>
|
||
<p>${sicher(fehler.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
/* ---- Dienststatus und Funktion -------------------------------------- */
|
||
|
||
function zeichneStatus() {
|
||
const aktuell = zustand.status.find((s) => s.id === (zustand.mitarbeiter || {}).status)
|
||
|| zustand.status[0];
|
||
|
||
if (aktuell) {
|
||
$("status-text").textContent = aktuell.name;
|
||
$("status-punkt").style.background = aktuell.farbe;
|
||
}
|
||
|
||
$("status-liste").innerHTML = zustand.status.map((s) =>
|
||
`<button data-status="${sicher(s.id)}">
|
||
<span class="status-punkt" style="background:${sicher(s.farbe)}"></span>
|
||
${sicher(s.name)}
|
||
</button>`).join("");
|
||
|
||
$("status-liste").querySelectorAll("[data-status]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
const gewaehlt = el.dataset.status;
|
||
zustand.mitarbeiter.status = gewaehlt;
|
||
anClient("status", { status: gewaehlt });
|
||
$("status-liste").hidden = true;
|
||
zeichneStatus();
|
||
});
|
||
});
|
||
}
|
||
|
||
function zeichneFunktion() {
|
||
const aktuell = zustand.funktionen.find(
|
||
(f) => f.id === (zustand.mitarbeiter || {}).funktion);
|
||
|
||
$("funktion-text").textContent = aktuell ? aktuell.name : "Stationsdienst";
|
||
|
||
$("funktion-liste").innerHTML = zustand.funktionen.map((f) =>
|
||
`<button data-funktion="${sicher(f.id)}">${sicher(f.name)}</button>`).join("");
|
||
|
||
$("funktion-liste").querySelectorAll("[data-funktion]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
const gewaehlt = el.dataset.funktion;
|
||
zustand.mitarbeiter.funktion = gewaehlt;
|
||
anClient("funktion", { funktion: gewaehlt });
|
||
$("funktion-liste").hidden = true;
|
||
zeichneFunktion();
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Mitteilungen --------------------------------------------------- */
|
||
|
||
function zeichneHinweise() {
|
||
const ungelesen = zustand.hinweise.filter((h) => !h.gelesen).length;
|
||
const zaehler = $("hinweise-zaehler");
|
||
zaehler.hidden = ungelesen === 0;
|
||
zaehler.textContent = ungelesen > 99 ? "99+" : ungelesen;
|
||
|
||
const symbole = {
|
||
einsatz: "🚑", patient: "🧍", labor: "🧪", medikament: "💊",
|
||
blut: "🩸", op: "🔪", doku: "📄", system: "ℹ",
|
||
};
|
||
|
||
$("hinweise-inhalt").innerHTML = zustand.hinweise.length
|
||
? zustand.hinweise.map((h) =>
|
||
`<div class="hinweis-eintrag ${h.gelesen ? "" : "ungelesen"} prio-${sicher(h.prioritaet || "normal")}"
|
||
data-hinweis="${h.id || ""}">
|
||
<div class="he-kopf">
|
||
<span class="he-titel">${symbole[h.kategorie] || "•"} ${sicher(h.titel)}</span>
|
||
<span class="he-zeit">${zeit(h.erstellt_am)}</span>
|
||
</div>
|
||
${h.text ? `<div class="he-text">${sicher(h.text)}</div>` : ""}
|
||
</div>`
|
||
).join("")
|
||
: `<div class="liste-leer">Keine Mitteilungen.</div>`;
|
||
|
||
$("hinweise-inhalt").querySelectorAll("[data-hinweis]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
const id = +el.dataset.hinweis;
|
||
if (!id) return;
|
||
const treffer = zustand.hinweise.find((h) => h.id === id);
|
||
if (treffer) treffer.gelesen = 1;
|
||
aufruf("dashboard", "hinweisGelesen", { id }).catch(() => {});
|
||
zeichneHinweise();
|
||
if (treffer) springeZuHinweis(treffer);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Öffnet das Modul, zu dem ein Hinweis gehört.
|
||
* Nur Ziele, die eine Ansicht auch auswertet, werden mitgegeben —
|
||
* alles andere öffnet schlicht das passende Modul.
|
||
*/
|
||
function springeZuHinweis(hinweis) {
|
||
if (!hinweis.zielTyp) return;
|
||
|
||
// zielTyp → [Modul, Schlüssel des Sprungziels oder null]
|
||
const wege = {
|
||
patient: ["mdt", "patient"],
|
||
akte: ["mdt", "akte"],
|
||
aufnahme: ["patienten", null],
|
||
behandlung: ["behandlung", "behandlung"],
|
||
bericht: ["bericht", "bericht"],
|
||
operation: ["operation", null],
|
||
labor: ["labor", null],
|
||
blut: ["blutbank", "blut"],
|
||
medikament: ["medikament", null],
|
||
zimmer: ["station", "zimmer"],
|
||
fahrzeug: ["fahrzeug", "fahrzeug"],
|
||
einsatz: ["einsatz", null],
|
||
intensiv: ["intensiv", "intensiv"],
|
||
nachricht: ["kommunikation", "nachricht"],
|
||
aushang: ["kommunikation", "aushang"],
|
||
};
|
||
|
||
const weg = wege[hinweis.zielTyp];
|
||
if (!weg) return;
|
||
|
||
// Nur öffnen, wenn das Modul für diesen Mitarbeiter sichtbar ist
|
||
if (!zustand.module.some((m) => m.id === weg[0])) return;
|
||
|
||
const ziel = weg[1] && hinweis.zielId ? { [weg[1]]: +hinweis.zielId } : null;
|
||
oeffneModul(weg[0], ziel);
|
||
$("hinweise-schublade").hidden = true;
|
||
}
|
||
|
||
/** Nimmt einen neu eingetroffenen Hinweis auf */
|
||
function neuerHinweis(hinweis) {
|
||
zustand.hinweise.unshift(Object.assign({ erstellt_am: new Date().toISOString() }, hinweis));
|
||
if (zustand.hinweise.length > 50) zustand.hinweise.pop();
|
||
zeichneHinweise();
|
||
meldung(hinweis.titel, hinweis.prioritaet === "kritisch" ? "fehler" : "");
|
||
|
||
// Offene Ansicht auffrischen, falls sie das betrifft
|
||
const ansicht = ansichten[zustand.aktivesModul];
|
||
if (ansicht && ansicht.aktualisieren) ansicht.aktualisieren();
|
||
}
|
||
|
||
/* ---- Aufbau beim Öffnen ---------------------------------------------- */
|
||
|
||
function zeigeTablet(daten) {
|
||
zustand.imDienst = true;
|
||
zustand.mitarbeiter = daten.mitarbeiter || {};
|
||
zustand.module = daten.module || [];
|
||
zustand.status = daten.status || [];
|
||
zustand.funktionen = daten.funktionen || [];
|
||
zustand.stationen = daten.stationen || [];
|
||
|
||
$("anmeldung").hidden = true;
|
||
$("tablet").hidden = false;
|
||
|
||
$("tb-kuerzel").textContent = daten.kuerzel || "LSMC";
|
||
$("tb-klinik").textContent = daten.klinik || "Klinikinformationssystem";
|
||
$("tb-name").textContent = zustand.mitarbeiter.name || "—";
|
||
$("tb-rolle").textContent = zustand.mitarbeiter.rolleName || "—";
|
||
$("tb-nummer").textContent = zustand.mitarbeiter.personalnummer || "—";
|
||
|
||
zeichneNavigation();
|
||
zeichneStatus();
|
||
zeichneFunktion();
|
||
zeichneHinweise();
|
||
|
||
if (zustand.module.length) oeffneModul(zustand.module[0].id);
|
||
}
|
||
|
||
function zeigeAnmeldung(daten) {
|
||
zustand.imDienst = false;
|
||
$("tablet").hidden = true;
|
||
$("anmeldung").hidden = false;
|
||
|
||
$("an-klinik").textContent = daten.klinik || "Los Santos Medical Center";
|
||
$("an-name").textContent = daten.name || "—";
|
||
$("an-nummer").textContent = daten.personalnummer || "—";
|
||
$("an-rolle").textContent = daten.rolleName || "—";
|
||
$("an-station").textContent = daten.station || "—";
|
||
|
||
// Dienstfunktion auswählen — die Vorauswahl ist nur ein Vorschlag
|
||
const funktionen = daten.funktionen || [{ id: "station", name: "Stationsdienst" }];
|
||
let gewaehlt = funktionen[0].id;
|
||
|
||
$("an-funktionen").innerHTML = funktionen.map((f, i) =>
|
||
`<button class="an-funktion-knopf ${i === 0 ? "gewaehlt" : ""}"
|
||
data-funktion="${sicher(f.id)}">${sicher(f.name)}</button>`).join("");
|
||
|
||
$("an-funktionen").querySelectorAll("[data-funktion]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
gewaehlt = el.dataset.funktion;
|
||
$("an-funktionen").querySelectorAll("[data-funktion]")
|
||
.forEach((k) => k.classList.remove("gewaehlt"));
|
||
el.classList.add("gewaehlt");
|
||
});
|
||
});
|
||
|
||
$("an-antreten").onclick = () => anClient("dienstAntreten", { funktion: gewaehlt });
|
||
}
|
||
|
||
/* ---- Verdrahtung der Hülle -------------------------------------------- */
|
||
|
||
function verdrahteShell() {
|
||
$("an-abbrechen").addEventListener("click", () => anClient("schliessen", {}));
|
||
$("tablet-schliessen").addEventListener("click", () => anClient("schliessen", {}));
|
||
|
||
$("dienst-beenden").addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Dienst beenden",
|
||
inhalt: `<p>Du meldest dich vom Dienst ab. Laufende Behandlungen und
|
||
Dokumentationen bleiben offen — sie werden nicht abgeschlossen.</p>`,
|
||
bestaetigenText: "Dienst beenden",
|
||
bestaetigt: (_, zu) => { zu(); anClient("dienstBeenden", {}); },
|
||
});
|
||
});
|
||
|
||
// Statuswähler
|
||
$("status-knopf").addEventListener("click", () => {
|
||
const liste = $("status-liste");
|
||
liste.hidden = !liste.hidden;
|
||
$("funktion-liste").hidden = true;
|
||
});
|
||
|
||
// Funktionswähler
|
||
$("funktion-knopf").addEventListener("click", () => {
|
||
const liste = $("funktion-liste");
|
||
liste.hidden = !liste.hidden;
|
||
$("status-liste").hidden = true;
|
||
});
|
||
|
||
// Mitteilungen
|
||
$("hinweise-knopf").addEventListener("click", () => {
|
||
const schublade = $("hinweise-schublade");
|
||
schublade.hidden = !schublade.hidden;
|
||
if (!schublade.hidden) zeichneHinweise();
|
||
});
|
||
$("hinweise-zu").addEventListener("click", () => { $("hinweise-schublade").hidden = true; });
|
||
|
||
// Design umschalten
|
||
$("thema-wechsel").addEventListener("click", () => {
|
||
document.body.classList.toggle("thema-dunkel");
|
||
document.body.classList.toggle("thema-hell");
|
||
});
|
||
|
||
// Globale Suche
|
||
$("global-suche").addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") {
|
||
const begriff = e.target.value.trim();
|
||
if (begriff.length >= 2) globalSuchen(begriff);
|
||
}
|
||
});
|
||
|
||
// Klick ins Leere schließt die Auswahllisten
|
||
document.addEventListener("click", (e) => {
|
||
if (!e.target.closest("#status-waehler")) $("status-liste").hidden = true;
|
||
if (!e.target.closest("#funktion-waehler")) $("funktion-liste").hidden = true;
|
||
});
|
||
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key !== "Escape") return;
|
||
if (document.querySelector(".fenster-hg")) {
|
||
document.querySelector(".fenster-hg").remove();
|
||
return;
|
||
}
|
||
if (!$("hinweise-schublade").hidden) { $("hinweise-schublade").hidden = true; return; }
|
||
anClient("schliessen", {});
|
||
});
|
||
|
||
// Nachrichten vom Client
|
||
window.addEventListener("message", (e) => {
|
||
const daten = e.data || {};
|
||
|
||
switch (daten.aktion) {
|
||
case "oeffnen":
|
||
zeigeTablet(daten);
|
||
document.body.style.display = "";
|
||
break;
|
||
|
||
case "anmeldung":
|
||
zeigeAnmeldung(daten);
|
||
document.body.style.display = "";
|
||
break;
|
||
|
||
case "schliessen":
|
||
document.body.style.display = "none";
|
||
$("hinweise-schublade").hidden = true;
|
||
$("fenster-ebene").innerHTML = "";
|
||
break;
|
||
|
||
case "antwort": {
|
||
const offen = offeneRufe[daten.ruf];
|
||
if (!offen) break;
|
||
delete offeneRufe[daten.ruf];
|
||
if (daten.fehler) offen.ablehnen(new Error(daten.fehler));
|
||
else offen.aufloesen(daten.ergebnis || {});
|
||
break;
|
||
}
|
||
|
||
case "hinweis":
|
||
neuerHinweis(daten.hinweis || {});
|
||
break;
|
||
|
||
case "dienstliste": {
|
||
zustand.dienstliste = daten.mitarbeiter || [];
|
||
const ansicht = ansichten[zustand.aktivesModul];
|
||
if (ansicht && ansicht.aktualisieren) ansicht.aktualisieren();
|
||
break;
|
||
}
|
||
|
||
case "einsatzAenderung": {
|
||
const ansicht = ansichten[zustand.aktivesModul];
|
||
if (ansicht && ansicht.aktualisieren) ansicht.aktualisieren();
|
||
break;
|
||
}
|
||
|
||
case "einsatzZuweisung":
|
||
meldung(`Zuweisung: ${daten.daten.nummer} — ${daten.daten.titel}`);
|
||
break;
|
||
|
||
case "fehler":
|
||
meldung(daten.text, "fehler");
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ---- Globale Suche ----------------------------------------------------- */
|
||
|
||
/** Registriert einen Anbieter für die globale Suche */
|
||
function registriereSuche(funktion) { suchAnbieter.push(funktion); }
|
||
|
||
async function globalSuchen(begriff) {
|
||
const treffer = [];
|
||
|
||
for (const anbieter of suchAnbieter) {
|
||
try {
|
||
const ergebnis = await anbieter(begriff);
|
||
if (Array.isArray(ergebnis)) treffer.push(...ergebnis);
|
||
} catch (e) { /* einzelne Anbieter dürfen scheitern */ }
|
||
}
|
||
|
||
fenster({
|
||
titel: `Suche: „${begriff}"`,
|
||
inhalt: treffer.length
|
||
? `<div class="liste">${treffer.map((t) =>
|
||
`<div class="liste-zeile">
|
||
<span class="marker neutral">${sicher(t.bereich)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(t.titel)}</div>
|
||
<div class="lz-unter">${sicher(t.unter || "")}</div>
|
||
</div>
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine Treffer.</div>`,
|
||
});
|
||
}
|
||
|
||
/* ---- Öffentliche Schnittstelle ------------------------------------------ */
|
||
return {
|
||
/** Registriert eine Modulansicht */
|
||
registriereAnsicht(modulId, ansicht) { ansichten[modulId] = ansicht; },
|
||
/** Registriert einen Anbieter für die globale Suche */
|
||
registriereSuche,
|
||
zustand, aufruf, meldung, fenster, bestaetigen,
|
||
sicher, zeit, dauer, minuten, euro, $,
|
||
anClient, oeffneModul,
|
||
start() {
|
||
verdrahteShell();
|
||
if (IST_FIVEM) document.body.style.display = "none";
|
||
else {
|
||
// Eigenständige Browser-Ansicht: Anmeldebildschirm zeigen
|
||
zeigeAnmeldung({
|
||
klinik: "Los Santos Medical Center", name: "Vorschau",
|
||
personalnummer: "LSMC-0000", rolleName: "Assistenzarzt", station: "notaufnahme",
|
||
funktionen: [
|
||
{ id: "station", name: "Stationsdienst" },
|
||
{ id: "rtw", name: "Rettungswagen" },
|
||
],
|
||
});
|
||
}
|
||
},
|
||
};
|
||
})();
|
||
|
||
App.start();
|