- 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>
247 lines
9.6 KiB
JavaScript
247 lines
9.6 KiB
JavaScript
/* =====================================================================
|
||
aether-police / Ansicht: Dashboard
|
||
|
||
Zeigt die Lage der Dienststelle. Sämtliche Zahlen kommen bei jedem
|
||
Aufruf frisch vom Server — nichts ist fest hinterlegt.
|
||
Die Ansicht bewertet nichts und löst keine Maßnahme aus.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
(() => {
|
||
const { sicher, zeit, dauer, aufruf, meldung, anClient } = App;
|
||
|
||
/** Zwischenspeicher der zuletzt geladenen Daten (für Auffrischungen) */
|
||
let letzteDaten = null;
|
||
|
||
/* ---- Bausteine --------------------------------------------------- */
|
||
|
||
function kennzahl(titel, wert, zusatz, art) {
|
||
return `<div class="kennzahl ${art || ""}">
|
||
<div class="kz-titel">${sicher(titel)}</div>
|
||
<div class="kz-wert">${sicher(wert)}</div>
|
||
${zusatz ? `<div class="kz-zusatz">${sicher(zusatz)}</div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
/** Übersetzt eine Prioritätszahl in eine Streifenklasse */
|
||
const prioKlasse = (p) => "p" + Math.min(4, Math.max(1, p || 3));
|
||
|
||
function warnungen(liste) {
|
||
if (!liste || !liste.length) return "";
|
||
const symbol = { hoch: "⚠", normal: "ℹ", niedrig: "•" };
|
||
return liste.map((w) =>
|
||
`<div class="warnung ${sicher(w.prioritaet)}" data-springe="${sicher(w.zielTyp || "")}">
|
||
<span class="w-symbol">${symbol[w.prioritaet] || "•"}</span>
|
||
<span>${sicher(w.text)}</span>
|
||
</div>`
|
||
).join("");
|
||
}
|
||
|
||
function beamtenListe(beamte) {
|
||
if (!beamte.length) return `<div class="liste-leer">Niemand im Dienst.</div>`;
|
||
|
||
const statusFarbe = {
|
||
verfuegbar: "gut", einsatz: "warn", pause: "neutral", nicht: "kritisch",
|
||
};
|
||
|
||
return `<div class="liste">${beamte.map((b) =>
|
||
`<div class="liste-zeile">
|
||
<span class="marker ${statusFarbe[b.status] || "neutral"}">${sicher(b.dienstnummer)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.name)}${b.leitstelle ? ' <span class="marker info">Leitstelle</span>' : ""}</div>
|
||
<div class="lz-unter">${sicher(b.rolleName)} · ${sicher(b.abteilung)}</div>
|
||
</div>
|
||
<span class="lz-unter">${dauer((Date.now() / 1000) - b.seit)}</span>
|
||
</div>`).join("")}</div>`;
|
||
}
|
||
|
||
function einsatzListe(einsaetze) {
|
||
if (!einsaetze.length) return `<div class="liste-leer">Keine offenen Einsätze.</div>`;
|
||
|
||
return `<div class="liste">${einsaetze.map((e) =>
|
||
`<div class="liste-zeile">
|
||
<span class="prio ${prioKlasse(e.prioritaet)}"></span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(e.titel)}</div>
|
||
<div class="lz-unter">${sicher(e.nummer)} · ${sicher(e.ort || "Ort unbekannt")} · ${zeit(e.erstellt_am)}</div>
|
||
</div>
|
||
<span class="marker ${e.status === "offen" ? "kritisch" : "neutral"}">${sicher(e.status)}</span>
|
||
${e.pos_x ? `<button class="knopf leise klein" data-weg="${e.pos_x},${e.pos_y}">Route</button>` : ""}
|
||
</div>`).join("")}</div>`;
|
||
}
|
||
|
||
function gewahrsamListe(eintraege) {
|
||
if (!eintraege.length) return `<div class="liste-leer">Niemand in Gewahrsam.</div>`;
|
||
|
||
return `<div class="liste">${eintraege.map((g) => {
|
||
const rest = Number(g.rest) || 0;
|
||
const abgelaufen = rest <= 0;
|
||
return `<div class="liste-zeile">
|
||
<span class="prio ${abgelaufen ? "p1" : "p3"}"></span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(g.firstname + " " + g.lastname)}</div>
|
||
<div class="lz-unter">${sicher(g.grund || "—")}${g.zelle ? " · Zelle " + sicher(g.zelle) : ""}</div>
|
||
</div>
|
||
<span class="marker ${abgelaufen ? "kritisch" : "warn"}">
|
||
${abgelaufen ? "Frist abgelaufen" : rest + " Min"}
|
||
</span>
|
||
</div>`;
|
||
}).join("")}</div>`;
|
||
}
|
||
|
||
function fahndungsListe(fahndungen) {
|
||
if (!fahndungen.length) return `<div class="liste-leer">Keine aktiven Fahndungen.</div>`;
|
||
|
||
const gefahrKlasse = { gefaehrlich: "kritisch", bewaffnet: "warn", normal: "neutral" };
|
||
|
||
return `<div class="liste">${fahndungen.map((f) =>
|
||
`<div class="liste-zeile">
|
||
<span class="prio ${prioKlasse(f.prioritaet)}"></span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(f.bezeichnung)}</div>
|
||
<div class="lz-unter">${sicher(f.grund)}</div>
|
||
</div>
|
||
<span class="marker ${gefahrKlasse[f.gefahr] || "neutral"}">${sicher(f.gefahr)}</span>
|
||
</div>`).join("")}</div>`;
|
||
}
|
||
|
||
function mitteilungsListe(mitteilungen) {
|
||
if (!mitteilungen.length) return `<div class="liste-leer">Keine Mitteilungen.</div>`;
|
||
|
||
return `<div class="liste">${mitteilungen.map((m) =>
|
||
`<div class="liste-zeile">
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(m.betreff || "Mitteilung")}</div>
|
||
<div class="lz-unter">${sicher((m.firstname || "") + " " + (m.lastname || ""))} · ${zeit(m.gesendet)}</div>
|
||
</div>
|
||
</div>`).join("")}</div>`;
|
||
}
|
||
|
||
/* ---- Aufbau ------------------------------------------------------ */
|
||
|
||
function zeichne(behaelter, d) {
|
||
const k = d.kennzahlen;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Übersicht</h2>
|
||
<p>Lage der Dienststelle — Stand ${new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })}</p>
|
||
</div>
|
||
<button class="knopf leise klein" id="db-aktualisieren">↻ Aktualisieren</button>
|
||
</div>
|
||
|
||
${warnungen(d.warnungen)}
|
||
|
||
<div class="raster k6" style="margin:14px 0 18px">
|
||
${kennzahl("Im Dienst", k.imDienst, `${k.verfuegbar} verfügbar`, k.imDienst > 0 ? "gut" : "")}
|
||
${kennzahl("Offene Einsätze", k.einsaetzeOffen, null, k.einsaetzeOffen > 0 ? "warn" : "")}
|
||
${kennzahl("Anzeigen (Entwurf)", k.anzeigenOffen, "warten auf Bestätigung")}
|
||
${kennzahl("Fahndungen", k.fahndungen)}
|
||
${kennzahl("In Gewahrsam", k.gefangene,
|
||
k.haftAbgelaufen > 0 ? `${k.haftAbgelaufen} Frist abgelaufen` : null,
|
||
k.haftAbgelaufen > 0 ? "hoch" : "")}
|
||
${kennzahl("Dienstfahrzeuge", `${k.fahrzeugeEinsatz}/${k.fahrzeugeGesamt}`, "im Einsatz")}
|
||
</div>
|
||
|
||
<div class="raster k2">
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Offene Einsätze</span>
|
||
<span class="marker neutral">${d.einsaetze.length}</span>
|
||
</div>
|
||
${einsatzListe(d.einsaetze)}
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Beamte im Dienst</span>
|
||
<span class="marker neutral">${d.beamte.length}</span>
|
||
</div>
|
||
${beamtenListe(d.beamte)}
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Gewahrsam</span>
|
||
<span class="marker neutral">${d.gewahrsam.length}</span>
|
||
</div>
|
||
${gewahrsamListe(d.gewahrsam)}
|
||
<div class="grundsatz">
|
||
<span>ℹ</span>
|
||
<div>Fristen dienen nur der <b>Erinnerung</b>. Eine Entlassung erfolgt
|
||
ausschließlich manuell durch einen Beamten.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Aktive Fahndungen</span>
|
||
<span class="marker neutral">${d.fahndungen.length}</span>
|
||
</div>
|
||
${fahndungsListe(d.fahndungen)}
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Meine offenen Berichte</span>
|
||
<span class="marker neutral">${d.berichte.length}</span>
|
||
</div>
|
||
${d.berichte.length
|
||
? `<div class="liste">${d.berichte.map((b) =>
|
||
`<div class="liste-zeile">
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.titel)}</div>
|
||
<div class="lz-unter">${sicher(b.nummer)} · ${zeit(b.erstellt_am)}</div>
|
||
</div>
|
||
<span class="marker warn">Entwurf</span>
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine offenen Berichte.</div>`}
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf"><span class="karte-titel">Neueste Mitteilungen</span></div>
|
||
${mitteilungsListe(d.mitteilungen)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Aktualisieren
|
||
App.$("db-aktualisieren").addEventListener("click", () => laden(behaelter));
|
||
|
||
// Route zu einem Einsatzort setzen
|
||
behaelter.querySelectorAll("[data-weg]").forEach((b) => {
|
||
b.addEventListener("click", () => {
|
||
const [x, y] = b.dataset.weg.split(",").map(Number);
|
||
anClient("wegpunkt", { x, y });
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Laden ------------------------------------------------------- */
|
||
|
||
async function laden(behaelter) {
|
||
try {
|
||
const daten = await aufruf("dashboard", "laden", {});
|
||
letzteDaten = daten;
|
||
App.zustand.hinweise = daten.hinweise || [];
|
||
zeichne(behaelter, daten);
|
||
} catch (fehler) {
|
||
behaelter.innerHTML =
|
||
`<div class="seiten-kopf"><div><h2>Übersicht</h2>
|
||
<p>Die Daten konnten nicht geladen werden: ${sicher(fehler.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
/* ---- Registrierung ----------------------------------------------- */
|
||
|
||
App.registriereAnsicht("dashboard", {
|
||
rendern: (behaelter) => laden(behaelter),
|
||
// Wird aufgerufen, wenn sich im Hintergrund etwas geändert hat
|
||
aktualisieren: () => {
|
||
const behaelter = App.$("inhalt");
|
||
if (App.zustand.aktivesModul === "dashboard" && behaelter) laden(behaelter);
|
||
},
|
||
});
|
||
})();
|