- 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>
603 lines
26 KiB
JavaScript
603 lines
26 KiB
JavaScript
/* =====================================================================
|
||
aether-police / Ansicht: Berichtswesen
|
||
|
||
Drei Stufen: Entwurf → Eingereicht → Freigegeben. Keine davon wird
|
||
automatisch erreicht. Unterschriften sind verbindlich.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
(() => {
|
||
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen, $ } = App;
|
||
|
||
const z = {
|
||
daten: null,
|
||
offen: null,
|
||
filterStatus: "",
|
||
nurEigene: false,
|
||
suche: "",
|
||
};
|
||
|
||
const statusArt = { entwurf: "warn", eingereicht: "info", freigegeben: "gut" };
|
||
const statusText = { entwurf: "Entwurf", eingereicht: "Eingereicht", freigegeben: "Freigegeben" };
|
||
|
||
/* ---- Übersicht ---------------------------------------------------- */
|
||
|
||
function uebersicht(behaelter) {
|
||
const d = z.daten;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Berichte</h2>
|
||
<p>Entwurf → Eingereicht → Freigegeben. Jede Stufe verlangt eine Entscheidung.</p>
|
||
</div>
|
||
${d.rechte.erstellen ? `<button class="knopf klein" id="br-neu">+ Bericht anlegen</button>` : ""}
|
||
</div>
|
||
|
||
<div class="raster k2" style="margin-bottom:14px">
|
||
<div class="kennzahl ${d.eigeneOffen > 0 ? "warn" : ""}">
|
||
<div class="kz-titel">Meine offenen Entwürfe</div>
|
||
<div class="kz-wert">${d.eigeneOffen}</div>
|
||
<div class="kz-zusatz">warten auf Ausarbeitung</div>
|
||
</div>
|
||
<div class="kennzahl ${d.zurPruefung > 0 ? "info" : ""}">
|
||
<div class="kz-titel">Zur Prüfung eingereicht</div>
|
||
<div class="kz-wert">${d.zurPruefung}</div>
|
||
<div class="kz-zusatz">${d.rechte.freigeben ? "warten auf deine Freigabe" : "in Bearbeitung"}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mdt-suchleiste" style="margin-bottom:14px">
|
||
<input id="br-suche" type="search" placeholder="Nummer oder Titel …" value="${sicher(z.suche)}">
|
||
<select id="br-status" class="katalog-suche">
|
||
<option value="">Alle Stufen</option>
|
||
<option value="entwurf" ${z.filterStatus === "entwurf" ? "selected" : ""}>Entwürfe</option>
|
||
<option value="eingereicht" ${z.filterStatus === "eingereicht" ? "selected" : ""}>Eingereicht</option>
|
||
<option value="freigegeben" ${z.filterStatus === "freigegeben" ? "selected" : ""}>Freigegeben</option>
|
||
</select>
|
||
<button class="knopf leise klein" id="br-eigene">
|
||
${z.nurEigene ? "Alle anzeigen" : "Nur meine"}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
${d.berichte.length ? `<div class="liste">${d.berichte.map((b) => `
|
||
<div class="liste-zeile treffer" data-bericht="${b.id}">
|
||
<span class="prio ${b.status === "entwurf" ? "p2" : b.status === "eingereicht" ? "p3" : "p4"}"></span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.titel)}
|
||
${b.eigener ? '<span class="marker neutral">eigener</span>' : ""}</div>
|
||
<div class="lz-unter">
|
||
${sicher(b.nummer)}
|
||
${b.einsatz_nummer ? " · Einsatz " + sicher(b.einsatz_nummer) : ""}
|
||
· ${sicher(b.dienstnummer || "")} ${sicher((b.firstname || "") + " " + (b.lastname || ""))}
|
||
· ${zeit(b.erstellt_am)}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
|
||
<span class="marker ${statusArt[b.status]}">${sicher(statusText[b.status] || b.status)}</span>
|
||
${b.unterschriften.length
|
||
? `<span class="lz-unter">✍ ${b.unterschriften.length}</span>` : ""}
|
||
</div>
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine Berichte gefunden.</div>`}
|
||
</div>`;
|
||
|
||
const neu = $("br-neu");
|
||
if (neu) neu.addEventListener("click", () => anlegenDialog(behaelter));
|
||
|
||
const suchfeld = $("br-suche");
|
||
suchfeld.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") { z.suche = suchfeld.value.trim(); rendern(behaelter); }
|
||
});
|
||
$("br-status").addEventListener("change", (e) => {
|
||
z.filterStatus = e.target.value; rendern(behaelter);
|
||
});
|
||
$("br-eigene").addEventListener("click", () => {
|
||
z.nurEigene = !z.nurEigene; rendern(behaelter);
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-bericht]").forEach((el) =>
|
||
el.addEventListener("click", () => oeffne(behaelter, +el.dataset.bericht)));
|
||
}
|
||
|
||
/* ---- Einzelansicht ------------------------------------------------ */
|
||
|
||
function einzelAnsicht(behaelter, d) {
|
||
const b = d.bericht;
|
||
const bearbeitbar = d.rechte.bearbeiten;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>${sicher(b.titel)}</h2>
|
||
<p>${sicher(b.nummer)}
|
||
${b.einsatz_nummer ? " · Einsatz " + sicher(b.einsatz_nummer) : ""}
|
||
${b.aktenzeichen ? " · Akte " + sicher(b.aktenzeichen) : ""}</p>
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="knopf leise klein" id="br-zurueck">‹ Zurück</button>
|
||
${bearbeitbar ? `<button class="knopf leise klein" id="br-bearbeiten">✎ Bearbeiten</button>` : ""}
|
||
${!d.habeUnterschrieben && b.status !== "freigegeben"
|
||
? `<button class="knopf leise klein" id="br-unterschreiben">✍ Unterschreiben</button>` : ""}
|
||
${b.status === "entwurf" && bearbeitbar
|
||
? `<button class="knopf klein" id="br-einreichen">Einreichen</button>` : ""}
|
||
${b.status === "eingereicht" && d.rechte.freigeben
|
||
? `<button class="knopf leise klein" id="br-zurueckweisen">Nachbessern</button>
|
||
<button class="knopf klein" id="br-freigeben">Freigeben</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
${b.status === "freigegeben" ? `
|
||
<div class="warnung niedrig">
|
||
<span class="w-symbol">🔒</span>
|
||
<span>Dieser Bericht ist <b>freigegeben</b> und damit inhaltlich gesperrt —
|
||
auch für den Verfasser.</span>
|
||
</div>` : ""}
|
||
|
||
<div class="raster k2" style="margin-top:14px">
|
||
<div class="karte" style="grid-column:1/-1">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Sachverhalt</span>
|
||
<span class="marker ${statusArt[b.status]}">${sicher(statusText[b.status] || b.status)}</span>
|
||
</div>
|
||
<div class="akte-zeilen" style="grid-template-columns:auto 1fr;margin-bottom:12px">
|
||
<span>Verfasser</span><b>${sicher(b.dienstnummer || "")}
|
||
${sicher((b.firstname || "") + " " + (b.lastname || ""))}</b>
|
||
<span>Angelegt</span><b>${zeit(b.erstellt_am)}</b>
|
||
</div>
|
||
<div class="akte-inhalt">${b.beschreibung
|
||
? sicher(b.beschreibung).replace(/\n/g, "<br>")
|
||
: "<i>Noch keine Beschreibung erfasst.</i>"}</div>
|
||
|
||
<div class="karte-titel" style="margin-top:18px">Getroffene Maßnahmen</div>
|
||
<div class="akte-inhalt" style="margin-top:8px">${b.massnahmen
|
||
? sicher(b.massnahmen).replace(/\n/g, "<br>")
|
||
: "<i>Keine Maßnahmen dokumentiert.</i>"}</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Beteiligte</span>
|
||
${bearbeitbar ? `<button class="knopf leise klein" id="br-beteiligt">+</button>` : ""}
|
||
</div>
|
||
${b.beteiligte.length ? `<div class="liste">${b.beteiligte.map((p, i) => `
|
||
<div class="liste-zeile">
|
||
<span class="marker ${p.typ === "beamter" ? "info" : "neutral"}">
|
||
${p.typ === "beamter" ? (p.dienstnummer || "Beamter") : "Person"}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(p.name)}</div>
|
||
${p.rolle ? `<div class="lz-unter">${sicher(p.rolle)}</div>` : ""}
|
||
</div>
|
||
${bearbeitbar && p.charakterId !== b.erstellt_von
|
||
? `<button class="knopf leise klein" data-bet-weg="${i + 1}">✕</button>` : ""}
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine Beteiligten erfasst.</div>`}
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Anlagen & Beweise</span>
|
||
${bearbeitbar ? `<button class="knopf leise klein" id="br-anlage">+</button>` : ""}
|
||
</div>
|
||
${b.anlagen.length ? `<div class="liste">${b.anlagen.map((a, i) => `
|
||
<div class="liste-zeile">
|
||
<span class="marker ${a.typ === "beweis" ? "info" : "neutral"}">
|
||
${a.typ === "beweis" ? sicher(a.kennung) : "Anlage"}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(a.titel)}</div>
|
||
${a.adresse ? `<div class="lz-unter">${sicher(a.adresse)}</div>` : ""}
|
||
</div>
|
||
${a.adresse
|
||
? `<a class="knopf leise klein" href="${sicher(a.adresse)}" target="_blank"
|
||
style="text-decoration:none">Öffnen</a>` : ""}
|
||
${bearbeitbar ? `<button class="knopf leise klein" data-anl-weg="${i + 1}">✕</button>` : ""}
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine Anlagen.</div>`}
|
||
</div>
|
||
|
||
<div class="karte" style="grid-column:1/-1">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Unterschriften</span>
|
||
<span class="marker neutral">${b.unterschriften.length}</span>
|
||
</div>
|
||
${b.unterschriften.length ? `<div class="unterschriften">${b.unterschriften.map((u) => `
|
||
<div class="unterschrift">
|
||
<div class="us-name">${sicher(u.name)}</div>
|
||
<div class="us-meta">${sicher(u.dienstnummer || "")} · ${sicher(u.rolle || "")}</div>
|
||
<div class="us-zeit">${zeit(u.am)}${u.freigabe ? " · Freigabe" : ""}</div>
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Noch nicht unterschrieben.</div>`}
|
||
<div class="grundsatz">
|
||
<span>ℹ</span>
|
||
<div>Eine Unterschrift ist <b>verbindlich</b> und wird nicht zurückgenommen.
|
||
Sie bestätigt, dass der Inhalt aus deiner Sicht zutrifft.</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
verdrahteEinzel(behaelter, d);
|
||
}
|
||
|
||
function verdrahteEinzel(behaelter, d) {
|
||
const b = d.bericht;
|
||
|
||
$("br-zurueck").addEventListener("click", () => { z.offen = null; rendern(behaelter); });
|
||
|
||
const bearbeiten = $("br-bearbeiten");
|
||
if (bearbeiten) bearbeiten.addEventListener("click", () => {
|
||
const unterschrieben = b.unterschriften.length > 0;
|
||
fenster({
|
||
titel: "Bericht bearbeiten",
|
||
inhalt: `
|
||
<div class="feld"><label>Titel</label>
|
||
<input id="be-titel" value="${sicher(b.titel)}"></div>
|
||
<div class="feld"><label>Sachverhalt</label>
|
||
<textarea id="be-text" style="min-height:170px">${sicher(b.beschreibung || "")}</textarea></div>
|
||
<div class="feld"><label>Getroffene Maßnahmen</label>
|
||
<textarea id="be-mass" style="min-height:110px">${sicher(b.massnahmen || "")}</textarea></div>
|
||
${unterschrieben ? `
|
||
<div class="abweichung">
|
||
<div class="abweichung-kopf">⚠ Der Bericht wurde bereits unterschrieben</div>
|
||
<div class="feld" style="margin:10px 0 0">
|
||
<label>Grund der Änderung (erforderlich)</label>
|
||
<input id="be-grund" placeholder="Warum wird nachträglich geändert?">
|
||
</div>
|
||
</div>` : ""}`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", klick: (_, zu) => zu() },
|
||
{ text: "Speichern", klick: async (el, zu) => {
|
||
try {
|
||
await aufruf("bericht", "speichern", {
|
||
id: b.id,
|
||
titel: el.querySelector("#be-titel").value.trim(),
|
||
beschreibung: el.querySelector("#be-text").value,
|
||
massnahmen: el.querySelector("#be-mass").value,
|
||
grund: (el.querySelector("#be-grund") || {}).value || "",
|
||
});
|
||
zu(); meldung("Gespeichert.", "gut"); oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}},
|
||
],
|
||
});
|
||
});
|
||
|
||
const beteiligt = $("br-beteiligt");
|
||
if (beteiligt) beteiligt.addEventListener("click", () => beteiligtDialog(behaelter, b.id));
|
||
|
||
behaelter.querySelectorAll("[data-bet-weg]").forEach((el) =>
|
||
el.addEventListener("click", async () => {
|
||
try {
|
||
await aufruf("bericht", "beteiligtEntfernen", { id: b.id, index: +el.dataset.betWeg });
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}));
|
||
|
||
const anlage = $("br-anlage");
|
||
if (anlage) anlage.addEventListener("click", () => anlageDialog(behaelter, b.id));
|
||
|
||
behaelter.querySelectorAll("[data-anl-weg]").forEach((el) =>
|
||
el.addEventListener("click", async () => {
|
||
try {
|
||
await aufruf("bericht", "anlageEntfernen", { id: b.id, index: +el.dataset.anlWeg });
|
||
oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}));
|
||
|
||
const unterschreiben = $("br-unterschreiben");
|
||
if (unterschreiben) unterschreiben.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Bericht unterschreiben",
|
||
inhalt: `<div class="folgen">
|
||
<div class="folge">
|
||
<span class="folge-symbol">✍</span>
|
||
<div>Du bestätigst, dass der Inhalt von <b>${sicher(b.nummer)}</b> aus deiner
|
||
Sicht zutrifft. Die Unterschrift ist <b>verbindlich</b> und kann nicht
|
||
zurückgenommen werden.</div>
|
||
</div>
|
||
</div>`,
|
||
bestaetigenText: "Unterschreiben",
|
||
bestaetigt: async (_, zu) => {
|
||
try {
|
||
await aufruf("bericht", "unterschreiben", { id: b.id });
|
||
zu(); meldung("Unterschrieben.", "gut"); oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
|
||
const einreichen = $("br-einreichen");
|
||
if (einreichen) einreichen.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Bericht einreichen",
|
||
inhalt: `<p style="margin:0;font-size:13px;line-height:1.6">
|
||
<b>${sicher(b.nummer)}</b> geht zur Prüfung an die Führung. Bis zur Freigabe
|
||
kannst du ihn weiterhin ändern — jede Änderung nach dem Unterschreiben wird
|
||
allerdings vermerkt.</p>`,
|
||
bestaetigenText: "Einreichen",
|
||
bestaetigt: async (_, zu) => {
|
||
try {
|
||
await aufruf("bericht", "einreichen", { id: b.id });
|
||
zu(); meldung("Eingereicht.", "gut"); oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
|
||
const freigeben = $("br-freigeben");
|
||
if (freigeben) freigeben.addEventListener("click", () => {
|
||
bestaetigen({
|
||
titel: "Bericht freigeben",
|
||
inhalt: `
|
||
<div class="folgen">
|
||
<div class="folgen-titel">Mit der Freigabe geschieht Folgendes:</div>
|
||
<div class="folge">
|
||
<span class="folge-symbol">🔒</span>
|
||
<div>Der Bericht wird <b>inhaltlich gesperrt</b> — auch der Verfasser kann
|
||
ihn danach nicht mehr ändern.</div>
|
||
</div>
|
||
<div class="folge">
|
||
<span class="folge-symbol">✍</span>
|
||
<div>Du unterschreibst den Bericht damit zugleich als freigebender Beamter.</div>
|
||
</div>
|
||
</div>
|
||
<div class="feld" style="margin-top:12px"><label>Vermerk</label>
|
||
<input id="bf-vermerk" placeholder="optional"></div>`,
|
||
bestaetigenText: "Freigeben",
|
||
bestaetigt: async (el, zu) => {
|
||
try {
|
||
await aufruf("bericht", "freigeben", {
|
||
id: b.id, vermerk: el.querySelector("#bf-vermerk").value.trim(),
|
||
});
|
||
zu(); meldung("Freigegeben.", "gut"); oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
});
|
||
|
||
const zurueckweisen = $("br-zurueckweisen");
|
||
if (zurueckweisen) zurueckweisen.addEventListener("click", () => {
|
||
fenster({
|
||
titel: "Zur Nachbesserung zurückgeben",
|
||
inhalt: `<div class="feld"><label>Was soll nachgebessert werden?</label>
|
||
<textarea id="bz-grund" placeholder="Der Verfasser erhält diesen Hinweis."></textarea></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", klick: (_, zu) => zu() },
|
||
{ text: "Zurückweisen", art: "gefahr", klick: async (el, zu) => {
|
||
try {
|
||
await aufruf("bericht", "zurueckweisen", {
|
||
id: b.id, grund: el.querySelector("#bz-grund").value.trim(),
|
||
});
|
||
zu(); meldung("Zurückgewiesen."); oeffne(behaelter, b.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}},
|
||
],
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Dialoge ------------------------------------------------------ */
|
||
|
||
function anlegenDialog(behaelter) {
|
||
aufruf("bericht", "bezuege", {}).then((bez) => {
|
||
fenster({
|
||
titel: "Bericht anlegen",
|
||
inhalt: `
|
||
<div class="feld"><label>Titel</label>
|
||
<input id="bn-titel" placeholder="Worum geht es?"></div>
|
||
<div class="feld"><label>Bezug zu einem Einsatz</label>
|
||
<select id="bn-einsatz">
|
||
<option value="">— ohne Einsatzbezug —</option>
|
||
${(bez.einsaetze || []).map((e) =>
|
||
`<option value="${e.id}">${sicher(e.nummer)} · ${sicher(e.titel)}</option>`).join("")}
|
||
</select></div>
|
||
<div class="feld"><label>Sachverhalt</label>
|
||
<textarea id="bn-text" style="min-height:140px"
|
||
placeholder="Was ist geschehen? Datum, Uhrzeit, Ablauf."></textarea></div>
|
||
<div class="feld"><label>Getroffene Maßnahmen</label>
|
||
<textarea id="bn-mass" placeholder="Welche Maßnahmen wurden ergriffen?"></textarea></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", klick: (_, zu) => zu() },
|
||
{ text: "Anlegen", klick: async (el, zu) => {
|
||
try {
|
||
const ergebnis = await aufruf("bericht", "erstellen", {
|
||
titel: el.querySelector("#bn-titel").value.trim(),
|
||
einsatz: +el.querySelector("#bn-einsatz").value || null,
|
||
beschreibung: el.querySelector("#bn-text").value,
|
||
massnahmen: el.querySelector("#bn-mass").value,
|
||
});
|
||
zu(); meldung("Bericht " + ergebnis.nummer + " angelegt.", "gut");
|
||
oeffne(behaelter, ergebnis.id);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}},
|
||
],
|
||
});
|
||
}).catch((f) => meldung(f.message, "fehler"));
|
||
}
|
||
|
||
function beteiligtDialog(behaelter, berichtId) {
|
||
fenster({
|
||
titel: "Beteiligten hinzufügen",
|
||
inhalt: `
|
||
<div class="feld"><label>Art</label>
|
||
<select id="bb-typ">
|
||
<option value="person">Person (Zivilist)</option>
|
||
<option value="beamter">Beamter</option>
|
||
</select></div>
|
||
<div class="feld">
|
||
<label>Suchen</label>
|
||
<div class="person-waehler">
|
||
<input id="bb-suche" placeholder="Name eingeben">
|
||
<button class="knopf leise klein" id="bb-suchen">Suchen</button>
|
||
</div>
|
||
<div class="feld-hinweis">Oder unten einen Namen frei eintragen.</div>
|
||
</div>
|
||
<div id="bb-treffer"></div>
|
||
<div class="feld"><label>Name (frei)</label>
|
||
<input id="bb-name" placeholder="Falls die Person nicht im System ist"></div>
|
||
<div class="feld"><label>Beteiligung</label>
|
||
<input id="bb-rolle" placeholder="z. B. Zeuge, Geschädigter, Beschuldigter"></div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", klick: (_, zu) => zu() },
|
||
{ text: "Hinzufügen", klick: async (el, zu) => {
|
||
try {
|
||
await aufruf("bericht", "beteiligtHinzu", {
|
||
id: berichtId,
|
||
typ: el.querySelector("#bb-typ").value,
|
||
name: el.querySelector("#bb-name").value.trim(),
|
||
rolle: el.querySelector("#bb-rolle").value.trim(),
|
||
charakterId: el.dataset.gewaehlt ? +el.dataset.gewaehlt : null,
|
||
});
|
||
zu(); oeffne(behaelter, berichtId);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}},
|
||
],
|
||
nachOeffnen: (el) => {
|
||
const suchen = async () => {
|
||
const begriff = el.querySelector("#bb-suche").value.trim();
|
||
if (begriff.length < 2) { meldung("Bitte mindestens zwei Zeichen.", "fehler"); return; }
|
||
try {
|
||
const d = await aufruf("mdt", "personSuchen", { begriff });
|
||
const ziel = el.querySelector("#bb-treffer");
|
||
ziel.innerHTML = (d.treffer || []).length
|
||
? `<div class="liste">${d.treffer.slice(0, 6).map((p) =>
|
||
`<div class="liste-zeile treffer" data-wahl="${p.id}"
|
||
data-name="${sicher(p.firstname + " " + p.lastname)}">
|
||
<div class="lz-haupt"><div class="lz-titel">${sicher(p.firstname + " " + p.lastname)}</div>
|
||
<div class="lz-unter">geb. ${sicher(String(p.dob || "").slice(0, 10))}</div></div>
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine Treffer.</div>`;
|
||
|
||
ziel.querySelectorAll("[data-wahl]").forEach((zeile) =>
|
||
zeile.addEventListener("click", () => {
|
||
el.dataset.gewaehlt = zeile.dataset.wahl;
|
||
el.querySelector("#bb-name").value = zeile.dataset.name;
|
||
ziel.innerHTML = `<div class="feld-hinweis">Ausgewählt: <b>${sicher(zeile.dataset.name)}</b></div>`;
|
||
}));
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
};
|
||
el.querySelector("#bb-suchen").addEventListener("click", suchen);
|
||
el.querySelector("#bb-suche").addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") { e.preventDefault(); suchen(); }
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
function anlageDialog(behaelter, berichtId) {
|
||
fenster({
|
||
titel: "Anlage hinzufügen",
|
||
inhalt: `
|
||
<div class="feld"><label>Art</label>
|
||
<select id="ba-typ">
|
||
<option value="datei">Datei / Bild</option>
|
||
<option value="beweis">Vorhandenes Beweismittel</option>
|
||
</select></div>
|
||
|
||
<div id="ba-datei">
|
||
<div class="feld"><label>Bezeichnung</label>
|
||
<input id="ba-titel" placeholder="Was zeigt die Anlage?"></div>
|
||
<div class="feld"><label>Adresse</label>
|
||
<input id="ba-adresse" placeholder="https://…"></div>
|
||
</div>
|
||
|
||
<div id="ba-beweis" hidden>
|
||
<div class="feld">
|
||
<label>Beweismittel suchen</label>
|
||
<div class="person-waehler">
|
||
<input id="ba-suche" placeholder="Kennung oder Bezeichnung">
|
||
<button class="knopf leise klein" id="ba-suchen">Suchen</button>
|
||
</div>
|
||
</div>
|
||
<div id="ba-treffer"></div>
|
||
</div>`,
|
||
knoepfe: [
|
||
{ text: "Abbrechen", klick: (_, zu) => zu() },
|
||
{ text: "Hinzufügen", klick: async (el, zu) => {
|
||
const typ = el.querySelector("#ba-typ").value;
|
||
try {
|
||
await aufruf("bericht", "anlageHinzu", {
|
||
id: berichtId, typ,
|
||
titel: (el.querySelector("#ba-titel") || {}).value || "",
|
||
adresse: (el.querySelector("#ba-adresse") || {}).value || "",
|
||
beweisId: el.dataset.beweis ? +el.dataset.beweis : null,
|
||
});
|
||
zu(); oeffne(behaelter, berichtId);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}},
|
||
],
|
||
nachOeffnen: (el) => {
|
||
el.querySelector("#ba-typ").addEventListener("change", (e) => {
|
||
const istBeweis = e.target.value === "beweis";
|
||
el.querySelector("#ba-datei").hidden = istBeweis;
|
||
el.querySelector("#ba-beweis").hidden = !istBeweis;
|
||
});
|
||
|
||
const suchen = async () => {
|
||
const begriff = el.querySelector("#ba-suche").value.trim();
|
||
if (begriff.length < 2) { meldung("Bitte mindestens zwei Zeichen.", "fehler"); return; }
|
||
try {
|
||
const d = await aufruf("bericht", "beweisSuche", { begriff });
|
||
const ziel = el.querySelector("#ba-treffer");
|
||
ziel.innerHTML = (d.beweise || []).length
|
||
? `<div class="liste">${d.beweise.map((b) =>
|
||
`<div class="liste-zeile treffer" data-wahl="${b.id}">
|
||
<span class="marker neutral">${sicher(b.kennung)}</span>
|
||
<div class="lz-haupt"><div class="lz-titel">${sicher(b.bezeichnung)}</div>
|
||
<div class="lz-unter">${sicher(b.fundort || "")}</div></div>
|
||
</div>`).join("")}</div>`
|
||
: `<div class="liste-leer">Keine Treffer.</div>`;
|
||
|
||
ziel.querySelectorAll("[data-wahl]").forEach((zeile) =>
|
||
zeile.addEventListener("click", () => {
|
||
el.dataset.beweis = zeile.dataset.wahl;
|
||
ziel.innerHTML = `<div class="feld-hinweis">Ausgewählt.</div>`;
|
||
}));
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
};
|
||
el.querySelector("#ba-suchen").addEventListener("click", suchen);
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ---- Laden -------------------------------------------------------- */
|
||
|
||
async function oeffne(behaelter, berichtId) {
|
||
try {
|
||
const d = await aufruf("bericht", "oeffnen", { id: berichtId });
|
||
z.offen = berichtId;
|
||
einzelAnsicht(behaelter, d);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
}
|
||
|
||
async function rendern(behaelter) {
|
||
try {
|
||
z.daten = await aufruf("bericht", "liste", {
|
||
status: z.filterStatus, nurEigene: z.nurEigene, begriff: z.suche,
|
||
});
|
||
if (z.offen) await oeffne(behaelter, z.offen);
|
||
else uebersicht(behaelter);
|
||
} catch (f) {
|
||
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Berichte</h2>
|
||
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
App.registriereAnsicht("bericht", {
|
||
rendern,
|
||
aktualisieren: () => {
|
||
if (App.zustand.aktivesModul === "bericht") rendern($("inhalt"));
|
||
},
|
||
});
|
||
|
||
App.registriereSuche(async (begriff) => {
|
||
try {
|
||
const d = await aufruf("bericht", "liste", { begriff });
|
||
return (d.berichte || []).slice(0, 5).map((b) => ({
|
||
bereich: "Bericht", titel: b.titel,
|
||
unter: b.nummer + " · " + (statusText[b.status] || b.status),
|
||
}));
|
||
} catch (e) { return []; }
|
||
});
|
||
})();
|