aether-framework/resources/[aether]/aether-police/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

247 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
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);
},
});
})();