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

496 lines
19 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: Administration
Module an- und abschalten, Strafenkatalog pflegen, Protokoll lesen.
Die Reiter richten sich nach den Rechten des Beamten — wer nur das
Protokoll lesen darf, sieht auch nur dieses.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, euro, aufruf, meldung, fenster, bestaetigen } = App;
const z = {
bereiche: null, // welche Reiter erlaubt sind
reiter: null,
daten: null,
katalogSuche: "",
katalogKategorie: "",
katalogStillgelegt: false,
protokollModul: "",
protokollNummer: "",
};
/* ---- Reiter ---------------------------------------------------------- */
function reiterleiste() {
const moeglich = [
{ id: "module", text: "Module", erlaubt: z.bereiche.module },
{ id: "katalog", text: "Strafenkatalog", erlaubt: z.bereiche.katalog },
{ id: "protokoll", text: "Protokoll", erlaubt: z.bereiche.protokoll },
].filter((e) => e.erlaubt);
return `<div class="mdt-reiter">
${moeglich.map((e) => `
<button class="${z.reiter === e.id ? "aktiv" : ""}" data-reiter="${e.id}">
${sicher(e.text)}</button>`).join("")}
</div>`;
}
function verdrahteReiter(behaelter) {
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
el.addEventListener("click", () => {
z.reiter = el.dataset.reiter;
rendern(behaelter);
});
});
}
/* ---- Module ----------------------------------------------------------- */
function moduleAnsicht(behaelter) {
const d = z.daten;
const aktiv = d.module.filter((m) => m.aktiv).length;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Module</h2>
<p>Jeder Baustein lässt sich einzeln abschalten. Abgeschaltete Module
verschwinden aus der Seitenleiste — ihre Daten bleiben unangetastet.</p>
</div>
</div>
${reiterleiste()}
<div class="raster raster-3">
<div class="kennzahl"><span class="kz-zahl">${aktiv}/${d.module.length}</span>
<span class="kz-text">Module aktiv</span></div>
<div class="kennzahl"><span class="kz-zahl">${d.beamteImDienst}</span>
<span class="kz-text">Beamte im Dienst</span></div>
<div class="kennzahl">
<span class="kz-zahl">${d.module.reduce((s, m) => s + m.aktionen, 0)}</span>
<span class="kz-text">registrierte Aktionen</span></div>
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Registrierte Module</span></div>
<div class="liste">
${d.module.map(modulZeile).join("")}
</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Konfiguration</span>
<span class="feld-hinweis">Steht in config.lua — Änderung erfordert Neustart</span>
</div>
<div class="st-werte">
${d.konfiguration.map((k) =>
`<div><span>${sicher(k.name)}</span><b>${sicher(k.wert)}</b></div>`).join("")}
</div>
</div>
`;
verdrahteModule(behaelter);
}
function modulZeile(m) {
return `<div class="liste-zeile adm-modul ${m.aktiv ? "" : "aus"}">
<span class="adm-icon">${sicher(m.icon)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(m.name)}</div>
<div class="lz-unter">
${sicher(m.beschreibung || "")}
${m.rechtName ? ` · Recht: ${sicher(m.rechtName)}`
: m.recht ? ` · Recht: ${sicher(String(m.recht))}` : " · für alle sichtbar"}
</div>
</div>
<span class="marker neutral">${m.aktionen} Aktionen</span>
${m.unverzichtbar
? `<span class="marker info" title="Ohne dieses Modul wäre das Tablet nicht mehr bedienbar">
unverzichtbar</span>`
: `<button class="knopf ${m.aktiv ? "leise" : ""} klein"
data-schalten="${sicher(m.id)}" data-wert="${m.aktiv ? "0" : "1"}">
${m.aktiv ? "Abschalten" : "Einschalten"}</button>`}
</div>`;
}
function verdrahteModule(behaelter) {
verdrahteReiter(behaelter);
behaelter.querySelectorAll("[data-schalten]").forEach((el) => {
el.addEventListener("click", () => {
const id = el.dataset.schalten;
const einschalten = el.dataset.wert === "1";
const modul = z.daten.module.find((m) => m.id === id);
bestaetigen({
titel: einschalten ? "Modul einschalten" : "Modul abschalten",
gefahr: !einschalten,
inhalt: `<p><b>${sicher(modul.name)}</b> wird
${einschalten ? "wieder in der Seitenleiste erscheinen"
: "für alle Beamten aus der Seitenleiste verschwinden"}.</p>
${einschalten ? "" : `<p class="feld-hinweis">Vorhandene Daten bleiben erhalten
und sind nach dem Wiedereinschalten unverändert da. Beamte, die das Tablet
gerade offen haben, sehen die Änderung beim nächsten Öffnen.</p>`}
<div class="feld"><label>Grund (freiwillig)</label>
<input name="grund" placeholder="Warum wird das Modul geschaltet?"></div>`,
bestaetigenText: einschalten ? "Einschalten" : "Abschalten",
bestaetigt: async (dialog, zu) => {
try {
await aufruf("admin", "modulSchalten", {
id, aktiv: einschalten,
grund: dialog.querySelector("[name=grund]").value.trim(),
});
zu();
meldung(einschalten ? "Modul eingeschaltet." : "Modul abgeschaltet.", "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
});
}
/* ---- Strafenkatalog ---------------------------------------------------- */
function katalogAnsicht(behaelter) {
const d = z.daten;
// Delikte nach Kategorie gruppieren
const gruppen = {};
d.delikte.forEach((delikt) => {
(gruppen[delikt.kategorie] = gruppen[delikt.kategorie] || []).push(delikt);
});
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Strafenkatalog</h2>
<p>Die Werte hier sind <b>Vorschläge</b>. Sie werden nie automatisch verhängt —
ein Beamter kann jederzeit begründet davon abweichen.</p>
</div>
<button class="knopf" data-neu="1">+ Delikt anlegen</button>
</div>
${reiterleiste()}
<div class="mdt-suchleiste">
<input class="feld" data-suche="1" placeholder="Kennung, Bezeichnung oder Paragraf…"
value="${sicher(z.katalogSuche)}">
<select class="feld" data-kategorie="1">
<option value="">Alle Kategorien</option>
${d.kategorien.map((k) =>
`<option value="${sicher(k.kategorie)}"
${k.kategorie === z.katalogKategorie ? "selected" : ""}>
${sicher(k.kategorie)}</option>`).join("")}
</select>
<button class="knopf ${z.katalogStillgelegt ? "" : "leise"}" data-stillgelegt="1">
${z.katalogStillgelegt ? "Auch stillgelegte" : "Nur aktive"}
</button>
</div>
${Object.keys(gruppen).length
? Object.keys(gruppen).map((kategorie) => `
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">${sicher(kategorie)}</span>
<span class="feld-hinweis">${gruppen[kategorie].length} Delikte</span>
</div>
<div class="liste">${gruppen[kategorie].map(deliktZeile).join("")}</div>
</div>`).join("")
: `<div class="liste-leer">Kein Delikt gefunden.</div>`}
`;
verdrahteKatalog(behaelter);
}
function deliktZeile(delikt) {
return `<div class="liste-zeile adm-delikt ${delikt.aktiv ? "" : "aus"}">
<span class="pv-nummer">${sicher(delikt.kennung)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(delikt.bezeichnung)}</div>
<div class="lz-unter">
${sicher(delikt.paragraph || "ohne Paragraf")}
${delikt.hinweis ? " · " + sicher(delikt.hinweis) : ""}
</div>
</div>
<span class="adm-vorschlag" title="Vorgeschlagene Haftzeit">
${delikt.haft || 0} Min</span>
<span class="adm-vorschlag" title="Vorgeschlagene Geldstrafe">
${euro(delikt.geld || 0)}</span>
${delikt.aktiv ? "" : `<span class="marker neutral">stillgelegt</span>`}
<button class="knopf leise klein" data-bearbeiten="${delikt.id}">Bearbeiten</button>
<button class="knopf ${delikt.aktiv ? "leise" : ""} klein"
data-stilllegen="${delikt.id}" data-wert="${delikt.aktiv ? "0" : "1"}">
${delikt.aktiv ? "Stilllegen" : "Freigeben"}</button>
</div>`;
}
function verdrahteKatalog(behaelter) {
verdrahteReiter(behaelter);
const suchfeld = behaelter.querySelector("[data-suche]");
let warten = null;
suchfeld.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(() => {
z.katalogSuche = suchfeld.value.trim();
rendern(behaelter);
}, 260);
});
behaelter.querySelector("[data-kategorie]").addEventListener("change", (e) => {
z.katalogKategorie = e.target.value;
rendern(behaelter);
});
behaelter.querySelector("[data-stillgelegt]").addEventListener("click", () => {
z.katalogStillgelegt = !z.katalogStillgelegt;
rendern(behaelter);
});
behaelter.querySelector("[data-neu]").addEventListener("click", () =>
deliktFenster(behaelter, null));
behaelter.querySelectorAll("[data-bearbeiten]").forEach((el) => {
el.addEventListener("click", () => {
const delikt = z.daten.delikte.find((d) => d.id === +el.dataset.bearbeiten);
deliktFenster(behaelter, delikt);
});
});
behaelter.querySelectorAll("[data-stilllegen]").forEach((el) => {
el.addEventListener("click", () => {
const delikt = z.daten.delikte.find((d) => d.id === +el.dataset.stilllegen);
const freigeben = el.dataset.wert === "1";
bestaetigen({
titel: freigeben ? "Delikt freigeben" : "Delikt stilllegen",
gefahr: !freigeben,
inhalt: `<p><b>${sicher(delikt.kennung)} — ${sicher(delikt.bezeichnung)}</b></p>
<p>${freigeben
? "Das Delikt steht wieder zur Auswahl."
: "Das Delikt lässt sich für neue Anzeigen nicht mehr auswählen."}</p>
<p class="feld-hinweis">Gelöscht wird nichts — bestehende Anzeigen behalten
ihren Eintrag und bleiben nachvollziehbar.</p>
<div class="feld"><label>Grund (freiwillig)</label>
<input name="grund" placeholder="Warum?"></div>`,
bestaetigenText: freigeben ? "Freigeben" : "Stilllegen",
bestaetigt: async (dialog, zu) => {
try {
await aufruf("admin", "katalogSchalten", {
id: delikt.id, aktiv: freigeben,
grund: dialog.querySelector("[name=grund]").value.trim(),
});
zu();
meldung(freigeben ? "Delikt freigegeben." : "Delikt stillgelegt.", "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
});
}
function deliktFenster(behaelter, delikt) {
const neu = !delikt;
delikt = delikt || { kennung: "", kategorie: "", bezeichnung: "", paragraph: "",
haft: 0, geld: 0, hinweis: "", sortierung: 0 };
fenster({
titel: neu ? "Delikt anlegen" : "Delikt bearbeiten",
inhalt: `
<div class="adm-formular">
<div class="feld"><label>Kennung</label>
<input name="kennung" maxlength="24" value="${sicher(delikt.kennung)}"
placeholder="z. B. STGB-242"></div>
<div class="feld"><label>Kategorie</label>
<input name="kategorie" maxlength="48" value="${sicher(delikt.kategorie)}"
list="adm-kategorien" placeholder="z. B. Eigentumsdelikte">
<datalist id="adm-kategorien">
${z.daten.kategorien.map((k) =>
`<option value="${sicher(k.kategorie)}">`).join("")}
</datalist></div>
</div>
<div class="feld"><label>Bezeichnung</label>
<input name="bezeichnung" maxlength="160" value="${sicher(delikt.bezeichnung)}"
placeholder="Klartext des Delikts"></div>
<div class="feld"><label>Paragraf</label>
<input name="paragraph" maxlength="48" value="${sicher(delikt.paragraph || "")}"
placeholder="z. B. § 242 StGB"></div>
<div class="adm-formular">
<div class="feld"><label>Vorgeschlagene Haft (Minuten)</label>
<input name="haft" type="number" min="0" value="${delikt.haft || 0}"></div>
<div class="feld"><label>Vorgeschlagene Geldstrafe</label>
<input name="geld" type="number" min="0" value="${delikt.geld || 0}"></div>
</div>
<div class="feld"><label>Hinweis für den Beamten</label>
<input name="hinweis" maxlength="255" value="${sicher(delikt.hinweis || "")}"
placeholder="z. B. bei Wiederholung höher ansetzen"></div>
<div class="feld"><label>Sortierung</label>
<input name="sortierung" type="number" value="${delikt.sortierung || 0}"></div>
<div class="grundsatz">
<span>ℹ</span>
<div>Diese Werte sind <b>Vorschläge</b>. Das System verhängt sie nie —
es rechnet sie nur zusammen und legt sie dem Beamten vor.</div>
</div>`,
knoepfe: [
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
{ text: neu ? "Anlegen" : "Speichern", klick: async (el, zu) => {
const feld = (name) => el.querySelector(`[name=${name}]`).value.trim();
try {
await aufruf("admin", "katalogSpeichern", {
id: neu ? null : delikt.id,
kennung: feld("kennung"),
kategorie: feld("kategorie"),
bezeichnung: feld("bezeichnung"),
paragraph: feld("paragraph"),
haft: +feld("haft"),
geld: +feld("geld"),
hinweis: feld("hinweis"),
sortierung: +feld("sortierung"),
});
zu();
meldung(neu ? "Delikt angelegt." : "Delikt gespeichert.", "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
} },
],
});
}
/* ---- Protokoll ---------------------------------------------------------- */
function protokollAnsicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Protokoll</h2>
<p>Jede Entscheidung im System steht hier mit Beamtem, Zeit und Begründung.
Einträge lassen sich weder ändern noch entfernen.</p>
</div>
<span class="marker neutral">${d.gesamt.toLocaleString("de-DE")} Einträge gesamt</span>
</div>
${reiterleiste()}
<div class="mdt-suchleiste">
<select class="feld" data-modul="1">
<option value="">Alle Module</option>
${d.module.map((m) =>
`<option value="${sicher(m.modul)}" ${m.modul === z.protokollModul ? "selected" : ""}>
${sicher(m.modul)}</option>`).join("")}
</select>
<input class="feld" data-nummer="1" placeholder="Dienstnummer, z. B. LSPD-0001"
value="${sicher(z.protokollNummer)}">
<button class="knopf leise" data-zuruecksetzen="1">Filter zurücksetzen</button>
</div>
<div class="liste">
${d.eintraege.length
? d.eintraege.map(protokollZeile).join("")
: `<div class="liste-leer">${d.unbekannteNummer
? "Diese Dienstnummer gibt es nicht."
: "Kein Eintrag gefunden."}</div>`}
</div>
`;
verdrahteProtokoll(behaelter);
}
function protokollZeile(e) {
// Details als kurze Schlüssel-Wert-Kette
const details = e.details && typeof e.details === "object"
? Object.keys(e.details).map((k) =>
`${sicher(k)}: ${sicher(String(e.details[k]))}`).join(" · ")
: "";
return `<div class="liste-zeile adm-protokoll">
<span class="marker neutral">${sicher(e.modul)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(e.aktion)}</div>
<div class="lz-unter">
${sicher(e.name)}${e.dienstnummer ? " (" + sicher(e.dienstnummer) + ")" : ""}
${e.ziel_typ ? ` · ${sicher(e.ziel_typ)} ${sicher(e.ziel_id || "")}` : ""}
${details ? ` · ${details}` : ""}
</div>
${e.grund ? `<div class="adm-grund">„${sicher(e.grund)}"</div>` : ""}
</div>
<span class="komm-zeit">${zeit(e.am || e.erstellt_am)}</span>
</div>`;
}
function verdrahteProtokoll(behaelter) {
verdrahteReiter(behaelter);
behaelter.querySelector("[data-modul]").addEventListener("change", (e) => {
z.protokollModul = e.target.value;
rendern(behaelter);
});
const nummer = behaelter.querySelector("[data-nummer]");
let warten = null;
nummer.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(() => {
z.protokollNummer = nummer.value.trim();
rendern(behaelter);
}, 320);
});
behaelter.querySelector("[data-zuruecksetzen]").addEventListener("click", () => {
z.protokollModul = "";
z.protokollNummer = "";
rendern(behaelter);
});
}
/* ---- Laden -------------------------------------------------------------- */
async function rendern(behaelter) {
try {
// Erlaubte Bereiche einmal holen und den Startreiter danach wählen
if (!z.bereiche) {
z.bereiche = await aufruf("admin", "bereiche", {});
z.reiter = z.bereiche.module ? "module"
: z.bereiche.katalog ? "katalog"
: z.bereiche.protokoll ? "protokoll" : null;
}
if (!z.reiter) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Administration</h2>
<p>Für keinen Bereich der Administration hast du eine Berechtigung.</p></div></div>`;
return;
}
if (z.reiter === "katalog") {
z.daten = await aufruf("admin", "katalog", {
begriff: z.katalogSuche, kategorie: z.katalogKategorie,
stillgelegt: z.katalogStillgelegt,
});
katalogAnsicht(behaelter);
} else if (z.reiter === "protokoll") {
z.daten = await aufruf("admin", "protokoll", {
modul: z.protokollModul, dienstnummer: z.protokollNummer, limit: 200,
});
protokollAnsicht(behaelter);
} else {
z.daten = await aufruf("admin", "module", {});
moduleAnsicht(behaelter);
}
} catch (f) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Administration</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("admin", { rendern });
})();