- 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>
395 lines
17 KiB
JavaScript
395 lines
17 KiB
JavaScript
/* =====================================================================
|
||
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 []; }
|
||
});
|
||
})();
|