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

369 lines
16 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: Beweisverwaltung
Es gibt keine automatische Spurenerkennung — jeder Beweis wird hier
von einem Beamten erfasst und beschrieben.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen, $ } = App;
const z = {
daten: null,
offen: null, // geöffneter Beweis
filterArt: "",
suche: "",
};
const artSymbol = {
foto: "📷", video: "🎬", fingerabdruck: "🖐", dna: "🧬",
huelse: "🔩", dokument: "📄", aussage: "🗣", gegenstand: "📦",
};
function artName(id) {
const treffer = (z.daten.arten || []).find((a) => a.id === id);
return treffer ? treffer.name : id;
}
/* ---- Übersicht ---------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Beweismittel</h2>
<p>Jeder Beweis wird von einem Beamten erfasst — es gibt keine automatische Spurenerkennung.</p>
</div>
${d.rechte.erfassen ? `<button class="knopf klein" id="bw-neu">+ Beweis erfassen</button>` : ""}
</div>
<div class="mdt-suchleiste" style="margin-bottom:14px">
<input id="bw-suche" type="search" placeholder="Kennung, Bezeichnung oder Fundort …"
value="${sicher(z.suche)}">
<select id="bw-art" class="katalog-suche">
<option value="">Alle Arten</option>
${(d.arten || []).map((a) =>
`<option value="${sicher(a.id)}" ${z.filterArt === a.id ? "selected" : ""}>${sicher(a.name)}</option>`).join("")}
</select>
</div>
<div class="karte">
${d.beweise.length ? `<div class="liste">${d.beweise.map((b) => `
<div class="liste-zeile treffer" data-beweis="${b.id}">
<span class="beweis-symbol">${artSymbol[b.art] || "•"}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(b.bezeichnung)}</div>
<div class="lz-unter">
${sicher(b.kennung)} · ${sicher(artName(b.art))}
${b.fundort ? " · Fundort: " + sicher(b.fundort) : ""}
${b.aktenzeichen ? " · " + sicher(b.aktenzeichen) : ""}
</div>
<div class="lz-unter" style="margin-top:2px">
erfasst von ${sicher(b.dienstnummer || "")}
${sicher((b.firstname || "") + " " + (b.lastname || ""))} · ${zeit(b.erfasst_am)}
</div>
</div>
${b.datei ? '<span class="marker info">Anlage</span>' : ""}
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Beweismittel gefunden.</div>`}
</div>`;
const neu = $("bw-neu");
if (neu) neu.addEventListener("click", () => erfassenDialog(behaelter));
const suchfeld = $("bw-suche");
suchfeld.addEventListener("keydown", (e) => {
if (e.key === "Enter") { z.suche = suchfeld.value.trim(); rendern(behaelter); }
});
$("bw-art").addEventListener("change", (e) => {
z.filterArt = e.target.value; rendern(behaelter);
});
behaelter.querySelectorAll("[data-beweis]").forEach((el) =>
el.addEventListener("click", () => oeffne(behaelter, +el.dataset.beweis)));
}
/* ---- Einzelansicht ------------------------------------------------ */
function einzelAnsicht(behaelter, d) {
const b = d.beweis;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>${artSymbol[b.art] || "•"} ${sicher(b.bezeichnung)}</h2>
<p>${sicher(b.kennung)} · ${sicher(artName(b.art))}</p>
</div>
<div style="display:flex;gap:8px">
<button class="knopf leise klein" id="bw-zurueck">‹ Zurück</button>
<button class="knopf leise klein" id="bw-bearbeiten">✎ Ändern</button>
${d.rechte.loeschen ? `<button class="knopf leise klein" id="bw-entfernen">Entfernen</button>` : ""}
</div>
</div>
<div class="raster k2">
<div class="karte" style="grid-column:1/-1">
<div class="karte-kopf"><span class="karte-titel">Beweismittel</span></div>
<div class="akte-zeilen">
<span>Kennung</span><b>${sicher(b.kennung)}</b>
<span>Art</span><b>${sicher(artName(b.art))}</b>
<span>Fundort</span><b>${sicher(b.fundort || "—")}</b>
<span>Aufgefunden</span><b>${zeit(b.gefunden_am)}</b>
<span>Erfasst von</span><b>${sicher(b.dienstnummer || "")}
${sicher((b.firstname || "") + " " + (b.lastname || ""))}</b>
<span>Erfasst am</span><b>${zeit(b.erfasst_am)}</b>
</div>
${b.beschreibung
? `<div class="akte-inhalt" style="margin-top:12px">${sicher(b.beschreibung).replace(/\n/g, "<br>")}</div>`
: ""}
${b.datei ? `<div style="margin-top:12px">
<a class="knopf leise klein" href="${sicher(b.datei)}" target="_blank"
style="display:inline-block;text-decoration:none">📎 Anlage öffnen</a>
</div>` : ""}
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Verknüpfte Akten</span>
<button class="knopf leise klein" id="bw-akte">+ Akte</button>
</div>
${d.akten.length ? `<div class="liste">${d.akten.map((a) => `
<div class="liste-zeile">
<span class="marker neutral">${sicher(a.typ)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.titel)}</div>
<div class="lz-unter">${sicher(a.aktenzeichen)}</div>
</div>
<span class="marker ${a.status === "geschlossen" ? "neutral" : "warn"}">${sicher(a.status)}</span>
</div>`).join("")}</div>`
: `<div class="liste-leer">Noch keiner Akte zugeordnet.</div>`}
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Zugehörige Asservate</span></div>
${d.asservate.length ? `<div class="liste">${d.asservate.map((a) => `
<div class="liste-zeile">
<span class="marker neutral">${sicher(a.kennung)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.bezeichnung)}</div>
<div class="lz-unter">${sicher(a.lagerort)}</div>
</div>
<span class="marker ${a.status === "eingelagert" ? "gut" : "neutral"}">${sicher(a.status)}</span>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Asservate hinterlegt.</div>`}
</div>
<div class="karte" style="grid-column:1/-1">
<div class="karte-kopf"><span class="karte-titel">Vorgangshistorie</span></div>
${d.historie.length ? `<div class="liste">${d.historie.map((h) => `
<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher(h.aktion)}</div>
<div class="lz-unter">${sicher(h.dienstnummer || "")}
${sicher((h.firstname || "") + " " + (h.lastname || ""))} · ${zeit(h.am)}
${h.grund ? " · " + sicher(h.grund) : ""}</div>
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Einträge.</div>`}
</div>
</div>`;
$("bw-zurueck").addEventListener("click", () => { z.offen = null; rendern(behaelter); });
$("bw-bearbeiten").addEventListener("click", () => {
fenster({
titel: "Beweis ändern",
inhalt: `
<div class="feld"><label>Bezeichnung</label>
<input id="be-bez" value="${sicher(b.bezeichnung)}"></div>
<div class="feld"><label>Fundort</label>
<input id="be-ort" value="${sicher(b.fundort || "")}"></div>
<div class="feld"><label>Beschreibung</label>
<textarea id="be-text">${sicher(b.beschreibung || "")}</textarea></div>
<div class="feld"><label>Anlage (Adresse)</label>
<input id="be-datei" value="${sicher(b.datei || "")}" placeholder="https://…"></div>
<div class="feld"><label>Grund der Änderung (erforderlich)</label>
<input id="be-grund" placeholder="Änderungen an Beweismitteln werden protokolliert">
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Speichern", klick: async (el, zu) => {
try {
await aufruf("beweis", "bearbeiten", {
id: b.id,
bezeichnung: el.querySelector("#be-bez").value.trim(),
fundort: el.querySelector("#be-ort").value.trim(),
beschreibung: el.querySelector("#be-text").value,
datei: el.querySelector("#be-datei").value.trim(),
grund: el.querySelector("#be-grund").value.trim(),
});
zu(); meldung("Gespeichert.", "gut"); oeffne(behaelter, b.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
const entfernen = $("bw-entfernen");
if (entfernen) entfernen.addEventListener("click", () => {
bestaetigen({
titel: "Beweismittel entfernen",
gefahr: true,
inhalt: `
<div class="folgen">
<div class="folge">
<span class="folge-symbol">⚠</span>
<div>Der Beweis <b>${sicher(b.kennung)}</b> wird aus dem Bestand entfernt.
Der Vorgang bleibt im Protokoll sichtbar und alle Beamten werden benachrichtigt.</div>
</div>
</div>
<div class="feld" style="margin-top:12px">
<label>Begründung (ausführlich, erforderlich)</label>
<textarea id="bl-grund" placeholder="Warum wird das Beweismittel entfernt?"></textarea>
</div>`,
bestaetigenText: "Entfernen",
bestaetigt: async (el, zu) => {
try {
await aufruf("beweis", "entfernen", {
id: b.id, grund: el.querySelector("#bl-grund").value.trim(),
});
zu(); meldung("Beweis entfernt."); z.offen = null; rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
$("bw-akte").addEventListener("click", () => akteVerknuepfenDialog(behaelter, b.id));
}
/* ---- Dialoge ------------------------------------------------------ */
function erfassenDialog(behaelter) {
const arten = (z.daten.arten || []);
fenster({
titel: "Beweis erfassen",
inhalt: `
<div class="raster k2" style="gap:10px">
<div class="feld" style="margin:0"><label>Art</label>
<select id="bn-art">
${arten.map((a) => `<option value="${sicher(a.id)}">${sicher(a.name)}</option>`).join("")}
</select></div>
<div class="feld" style="margin:0"><label>Fundort</label>
<input id="bn-ort" placeholder="Wo wurde es aufgefunden?"></div>
</div>
<div class="feld"><label>Bezeichnung</label>
<input id="bn-bez" placeholder="Kurze, eindeutige Benennung"></div>
<div class="feld"><label>Beschreibung</label>
<textarea id="bn-text" placeholder="Zustand, Auffälligkeiten, Umstände der Auffindung"></textarea></div>
<div class="feld"><label>Anlage (Bild-/Videoadresse)</label>
<input id="bn-datei" placeholder="https://… (optional)"></div>
<div class="grundsatz">
<span>ℹ</span>
<div>Der <b>Fundort</b> ist Pflicht — er gehört zur Beweiskette. Das System
erkennt keine Spuren; alles hier ist deine Dokumentation.</div>
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Erfassen", klick: async (el, zu) => {
try {
const ergebnis = await aufruf("beweis", "erfassen", {
art: el.querySelector("#bn-art").value,
bezeichnung: el.querySelector("#bn-bez").value.trim(),
beschreibung: el.querySelector("#bn-text").value,
fundort: el.querySelector("#bn-ort").value.trim(),
datei: el.querySelector("#bn-datei").value.trim(),
});
zu(); meldung("Beweis " + ergebnis.kennung + " erfasst.", "gut");
oeffne(behaelter, ergebnis.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
}
function akteVerknuepfenDialog(behaelter, beweisId) {
fenster({
titel: "Mit Akte verknüpfen",
inhalt: `
<div class="feld">
<label>Akte suchen</label>
<div class="person-waehler">
<input id="bv-suche" placeholder="Aktenzeichen oder Titel">
<button class="knopf leise klein" id="bv-suchen">Suchen</button>
</div>
</div>
<div id="bv-treffer"></div>`,
nachOeffnen: (el) => {
const suchen = async () => {
const begriff = el.querySelector("#bv-suche").value.trim();
if (begriff.length < 2) { meldung("Bitte mindestens zwei Zeichen.", "fehler"); return; }
try {
const d = await aufruf("beweis", "aktenSuche", { begriff });
const ziel = el.querySelector("#bv-treffer");
ziel.innerHTML = (d.akten || []).length
? `<div class="liste">${d.akten.map((a) =>
`<div class="liste-zeile treffer" data-wahl="${a.id}">
<span class="marker neutral">${sicher(a.typ)}</span>
<div class="lz-haupt"><div class="lz-titel">${sicher(a.titel)}</div>
<div class="lz-unter">${sicher(a.aktenzeichen)}</div></div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Treffer.</div>`;
ziel.querySelectorAll("[data-wahl]").forEach((zeile) =>
zeile.addEventListener("click", async () => {
try {
await aufruf("beweis", "akteVerknuepfen", {
beweisId, akteId: +zeile.dataset.wahl,
});
el.remove(); meldung("Verknüpft.", "gut"); oeffne(behaelter, beweisId);
} catch (f) { meldung(f.message, "fehler"); }
}));
} catch (f) { meldung(f.message, "fehler"); }
};
el.querySelector("#bv-suchen").addEventListener("click", suchen);
el.querySelector("#bv-suche").addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); suchen(); }
});
},
});
}
/* ---- Laden -------------------------------------------------------- */
async function oeffne(behaelter, beweisId) {
try {
const d = await aufruf("beweis", "oeffnen", { id: beweisId });
z.offen = beweisId;
einzelAnsicht(behaelter, d);
} catch (f) { meldung(f.message, "fehler"); }
}
async function rendern(behaelter) {
try {
z.daten = await aufruf("beweis", "liste", { art: z.filterArt, begriff: z.suche });
if (z.offen) await oeffne(behaelter, z.offen);
else uebersicht(behaelter);
} catch (f) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Beweise</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("beweis", { rendern });
/* Globale Suche bedienen */
App.registriereSuche(async (begriff) => {
try {
const d = await aufruf("beweis", "liste", { begriff });
return (d.beweise || []).slice(0, 5).map((b) => ({
bereich: "Beweis", titel: b.bezeichnung,
unter: b.kennung + " · " + (b.fundort || ""),
}));
} catch (e) { return []; }
});
})();