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

395 lines
17 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: Asservatenkammer
Jedes Objekt trägt eine lückenlose Verwahrkette: wer hat es
eingelagert, ausgegeben, zurückgenommen oder vernichtet.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen, $ } = App;
const z = {
daten: null,
offen: null,
filterKategorie: "",
filterStatus: "",
suche: "",
};
const katSymbol = {
waffe: "🔫", drogen: "💊", bargeld: "💵", schmuck: "💍",
datentraeger: "💾", dokument: "📄", sonstiges: "📦",
};
const statusArt = {
eingelagert: "gut", ausgegeben: "warn",
vernichtet: "kritisch", zurueckgegeben: "neutral",
};
const statusText = {
eingelagert: "Eingelagert", ausgegeben: "Ausgegeben",
vernichtet: "Vernichtet", zurueckgegeben: "Zurückgegeben",
};
function katName(id) {
const treffer = (z.daten.kategorien || []).find((k) => k.id === id);
return treffer ? treffer.name : id;
}
/* ---- Übersicht ---------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Asservatenkammer</h2>
<p>Jeder Schritt wird mit Namen festgehalten — die Verwahrkette bleibt lückenlos.</p>
</div>
${d.rechte.einlagern ? `<button class="knopf klein" id="as-neu">+ Einlagern</button>` : ""}
</div>
<div class="raster k4" style="margin-bottom:14px">
<div class="kennzahl gut"><div class="kz-titel">Eingelagert</div>
<div class="kz-wert">${d.bestand.eingelagert || 0}</div></div>
<div class="kennzahl warn"><div class="kz-titel">Ausgegeben</div>
<div class="kz-wert">${d.bestand.ausgegeben || 0}</div></div>
<div class="kennzahl"><div class="kz-titel">Zurückgegeben</div>
<div class="kz-wert">${d.bestand.zurueckgegeben || 0}</div></div>
<div class="kennzahl"><div class="kz-titel">Vernichtet</div>
<div class="kz-wert">${d.bestand.vernichtet || 0}</div></div>
</div>
<div class="mdt-suchleiste" style="margin-bottom:14px">
<input id="as-suche" type="search" placeholder="Kennung, Bezeichnung oder Lagerort …"
value="${sicher(z.suche)}">
<select id="as-kategorie" class="katalog-suche">
<option value="">Alle Kategorien</option>
${(d.kategorien || []).map((k) =>
`<option value="${sicher(k.id)}" ${z.filterKategorie === k.id ? "selected" : ""}>${sicher(k.name)}</option>`).join("")}
</select>
<select id="as-status" class="katalog-suche">
<option value="">Alle Status</option>
${["eingelagert", "ausgegeben", "zurueckgegeben", "vernichtet"].map((s) =>
`<option value="${s}" ${z.filterStatus === s ? "selected" : ""}>${statusText[s]}</option>`).join("")}
</select>
</div>
<div class="karte">
${d.asservate.length ? `<div class="liste">${d.asservate.map((a) => `
<div class="liste-zeile treffer" data-asservat="${a.id}">
<span class="beweis-symbol">${katSymbol[a.kategorie] || "📦"}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.bezeichnung)}
${a.menge ? `<span class="marker neutral">${sicher(a.menge)}</span>` : ""}</div>
<div class="lz-unter">
${sicher(a.kennung)} · ${sicher(katName(a.kategorie))} · ${sicher(a.lagerort)}
${a.aktenzeichen ? " · " + sicher(a.aktenzeichen) : ""}
</div>
<div class="lz-unter" style="margin-top:2px">
verantwortlich: ${sicher(a.dienstnummer || "")}
${sicher((a.firstname || "") + " " + (a.lastname || ""))} · ${zeit(a.eingelagert_am)}
</div>
</div>
<span class="marker ${statusArt[a.status]}">${sicher(statusText[a.status] || a.status)}</span>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Asservate gefunden.</div>`}
</div>`;
const neu = $("as-neu");
if (neu) neu.addEventListener("click", () => einlagernDialog(behaelter));
const suchfeld = $("as-suche");
suchfeld.addEventListener("keydown", (e) => {
if (e.key === "Enter") { z.suche = suchfeld.value.trim(); rendern(behaelter); }
});
$("as-kategorie").addEventListener("change", (e) => {
z.filterKategorie = e.target.value; rendern(behaelter);
});
$("as-status").addEventListener("change", (e) => {
z.filterStatus = e.target.value; rendern(behaelter);
});
behaelter.querySelectorAll("[data-asservat]").forEach((el) =>
el.addEventListener("click", () => oeffne(behaelter, +el.dataset.asservat)));
}
/* ---- Einzelansicht ------------------------------------------------ */
function einzelAnsicht(behaelter, d) {
const a = d.asservat;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>${katSymbol[a.kategorie] || "📦"} ${sicher(a.bezeichnung)}</h2>
<p>${sicher(a.kennung)} · ${sicher(katName(a.kategorie))}</p>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="knopf leise klein" id="as-zurueck">‹ Zurück</button>
${a.status === "eingelagert" && d.rechte.ausgeben
? `<button class="knopf leise klein" id="as-ausgeben">Ausgeben</button>` : ""}
${a.status === "ausgegeben" && d.rechte.ausgeben
? `<button class="knopf leise klein" id="as-zurueckgeben">Zurücknehmen</button>` : ""}
${a.status === "eingelagert"
? `<button class="knopf leise klein" id="as-umlagern">Umlagern</button>` : ""}
${a.status !== "vernichtet" && d.rechte.vernichten
? `<button class="knopf leise klein" id="as-vernichten">Vernichten</button>` : ""}
</div>
</div>
<div class="raster k2">
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Objekt</span>
<span class="marker ${statusArt[a.status]}">${sicher(statusText[a.status] || a.status)}</span>
</div>
<div class="akte-zeilen" style="grid-template-columns:auto 1fr">
<span>Kennung</span><b>${sicher(a.kennung)}</b>
<span>Kategorie</span><b>${sicher(katName(a.kategorie))}</b>
<span>Menge</span><b>${sicher(a.menge || "—")}</b>
<span>Lagerort</span><b>${sicher(a.lagerort)}</b>
<span>Verantwortlich</span><b>${sicher(a.dienstnummer || "")}
${sicher((a.firstname || "") + " " + (a.lastname || ""))}</b>
<span>Eingelagert</span><b>${zeit(a.eingelagert_am)}</b>
</div>
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Zuordnung</span></div>
<div class="liste">
${a.aktenzeichen ? `<div class="liste-zeile">
<span class="marker neutral">Akte</span>
<div class="lz-haupt"><div class="lz-titel">${sicher(a.akte_titel || "")}</div>
<div class="lz-unter">${sicher(a.aktenzeichen)}</div></div></div>` : ""}
${a.beweis_kennung ? `<div class="liste-zeile">
<span class="marker neutral">Beweis</span>
<div class="lz-haupt"><div class="lz-titel">${sicher(a.beweis_bezeichnung || "")}</div>
<div class="lz-unter">${sicher(a.beweis_kennung)}</div></div></div>` : ""}
${!a.aktenzeichen && !a.beweis_kennung
? `<div class="liste-leer">Keine Zuordnung hinterlegt.</div>` : ""}
</div>
</div>
<div class="karte" style="grid-column:1/-1">
<div class="karte-kopf">
<span class="karte-titel">Verwahrkette</span>
<span class="marker neutral">${d.historie.length} Schritte</span>
</div>
<div class="kette">${d.historie.map((h, i) => `
<div class="kette-schritt">
<div class="kette-punkt">${i + 1}</div>
<div class="kette-inhalt">
<div class="kette-aktion">${sicher(h.aktion)}</div>
<div class="kette-meta">
${sicher(h.dienstnummer || "")}
${sicher((h.firstname || "") + " " + (h.lastname || ""))} · ${zeit(h.am)}
</div>
${h.notiz ? `<div class="kette-notiz">${sicher(h.notiz)}</div>` : ""}
</div>
</div>`).join("")}</div>
</div>
</div>`;
$("as-zurueck").addEventListener("click", () => { z.offen = null; rendern(behaelter); });
const ausgeben = $("as-ausgeben");
if (ausgeben) ausgeben.addEventListener("click", () => {
bestaetigen({
titel: "Asservat ausgeben",
inhalt: `
<p style="margin:0 0 12px;font-size:13px;line-height:1.6">
<b>${sicher(a.kennung)} — ${sicher(a.bezeichnung)}</b></p>
<div class="feld"><label>Empfänger</label>
<input id="aa-empf" placeholder="Name oder Dienststelle"></div>
<div class="feld"><label>Grund</label>
<input id="aa-grund" placeholder="Wozu wird es benötigt?"></div>`,
bestaetigenText: "Ausgeben",
bestaetigt: async (el, zu) => {
try {
await aufruf("asservat", "ausgeben", {
id: a.id,
empfaenger: el.querySelector("#aa-empf").value.trim(),
grund: el.querySelector("#aa-grund").value.trim(),
});
zu(); meldung("Ausgegeben.", "gut"); oeffne(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
const zurueckgeben = $("as-zurueckgeben");
if (zurueckgeben) zurueckgeben.addEventListener("click", () => {
fenster({
titel: "Zurücknehmen",
inhalt: `
<div class="feld"><label>Lagerort</label>
<input id="az-ort" value="${sicher(a.lagerort)}"></div>
<div class="feld"><label>Vermerk</label>
<input id="az-notiz" placeholder="Zustand bei Rückgabe (optional)"></div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Zurücknehmen", klick: async (el, zu) => {
try {
await aufruf("asservat", "zurueckgeben", {
id: a.id,
lagerort: el.querySelector("#az-ort").value.trim(),
notiz: el.querySelector("#az-notiz").value.trim(),
});
zu(); meldung("Zurückgenommen.", "gut"); oeffne(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
const umlagern = $("as-umlagern");
if (umlagern) umlagern.addEventListener("click", () => {
fenster({
titel: "Umlagern",
inhalt: `<div class="feld"><label>Neuer Lagerort</label>
<input id="au-ort" value="${sicher(a.lagerort)}"></div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Umlagern", klick: async (el, zu) => {
try {
await aufruf("asservat", "umlagern", {
id: a.id, lagerort: el.querySelector("#au-ort").value.trim(),
});
zu(); meldung("Umgelagert.", "gut"); oeffne(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
const vernichten = $("as-vernichten");
if (vernichten) vernichten.addEventListener("click", () => {
bestaetigen({
titel: "Asservat vernichten",
gefahr: true,
inhalt: `
<div class="folgen">
<div class="folgen-titel">Endgültige Maßnahme</div>
<div class="folge">
<span class="folge-symbol">🔥</span>
<div><b>${sicher(a.kennung)} — ${sicher(a.bezeichnung)}</b> wird als vernichtet
gekennzeichnet.</div>
</div>
<div class="folge">
<span class="folge-symbol">📄</span>
<div>Der <b>Datensatz bleibt bestehen</b> — vernichtet wird der Gegenstand,
nicht seine Dokumentation. Alle Beamten werden benachrichtigt.</div>
</div>
</div>
<div class="feld" style="margin-top:12px"><label>Zweiter Beamter als Zeuge</label>
<input id="av-zeuge" placeholder="Dienstnummer und Name"></div>
<div class="feld"><label>Begründung (ausführlich)</label>
<textarea id="av-grund" placeholder="Rechtsgrundlage und Anlass"></textarea></div>`,
bestaetigenText: "Vernichtung bestätigen",
bestaetigt: async (el, zu) => {
try {
await aufruf("asservat", "vernichten", {
id: a.id,
zeuge: el.querySelector("#av-zeuge").value.trim(),
grund: el.querySelector("#av-grund").value.trim(),
});
zu(); meldung("Vernichtung dokumentiert."); oeffne(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
}
/* ---- Einlagern ---------------------------------------------------- */
function einlagernDialog(behaelter) {
fenster({
titel: "Asservat einlagern",
inhalt: `
<div class="raster k2" style="gap:10px">
<div class="feld" style="margin:0"><label>Kategorie</label>
<select id="an-kat">
${(z.daten.kategorien || []).map((k) =>
`<option value="${sicher(k.id)}">${sicher(k.name)}</option>`).join("")}
</select></div>
<div class="feld" style="margin:0"><label>Menge / Umfang</label>
<input id="an-menge" placeholder="z. B. 3 Stück, 12,5 g"></div>
</div>
<div class="feld"><label>Bezeichnung</label>
<input id="an-bez" placeholder="Was wird eingelagert?"></div>
<div class="feld"><label>Lagerort</label>
<input id="an-ort" value="Asservatenkammer A" placeholder="Regal, Fach, Schrank"></div>
<div class="raster k2" style="gap:10px">
<div class="feld" style="margin:0"><label>Akten-ID (optional)</label>
<input id="an-akte" type="number" placeholder="z. B. 12"></div>
<div class="feld" style="margin:0"><label>Beweis-ID (optional)</label>
<input id="an-beweis" type="number" placeholder="z. B. 5"></div>
</div>
<div class="feld"><label>Vermerk</label>
<input id="an-notiz" placeholder="Zustand bei Einlagerung (optional)"></div>
<div class="grundsatz">
<span>ℹ</span>
<div>Du wirst als <b>verantwortlich</b> eingetragen. Jede weitere Bewegung
des Objekts wird in der Verwahrkette festgehalten.</div>
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Einlagern", klick: async (el, zu) => {
try {
const ergebnis = await aufruf("asservat", "einlagern", {
kategorie: el.querySelector("#an-kat").value,
bezeichnung: el.querySelector("#an-bez").value.trim(),
menge: el.querySelector("#an-menge").value.trim(),
lagerort: el.querySelector("#an-ort").value.trim(),
akte: +el.querySelector("#an-akte").value || null,
beweis: +el.querySelector("#an-beweis").value || null,
notiz: el.querySelector("#an-notiz").value.trim(),
});
zu(); meldung("Eingelagert als " + ergebnis.kennung + ".", "gut");
oeffne(behaelter, ergebnis.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
}
/* ---- Laden -------------------------------------------------------- */
async function oeffne(behaelter, asservatId) {
try {
const d = await aufruf("asservat", "oeffnen", { id: asservatId });
z.offen = asservatId;
// Kategorien für die Benennung mitführen
if (!z.daten) z.daten = { kategorien: [] };
einzelAnsicht(behaelter, d);
} catch (f) { meldung(f.message, "fehler"); }
}
async function rendern(behaelter) {
try {
z.daten = await aufruf("asservat", "liste", {
kategorie: z.filterKategorie, status: z.filterStatus, begriff: z.suche,
});
if (z.offen) await oeffne(behaelter, z.offen);
else uebersicht(behaelter);
} catch (f) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Asservate</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("asservat", { rendern });
App.registriereSuche(async (begriff) => {
try {
const d = await aufruf("asservat", "liste", { begriff });
return (d.asservate || []).slice(0, 5).map((a) => ({
bereich: "Asservat", titel: a.bezeichnung,
unter: a.kennung + " · " + a.lagerort,
}));
} catch (e) { return []; }
});
})();