/* =====================================================================
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 = `
Berichte
Entwurf → Eingereicht → Freigegeben. Jede Stufe verlangt eine Entscheidung.
${d.rechte.erstellen ? `
+ Bericht anlegen ` : ""}
Meine offenen Entwürfe
${d.eigeneOffen}
warten auf Ausarbeitung
Zur Prüfung eingereicht
${d.zurPruefung}
${d.rechte.freigeben ? "warten auf deine Freigabe" : "in Bearbeitung"}
Alle Stufen
Entwürfe
Eingereicht
Freigegeben
${z.nurEigene ? "Alle anzeigen" : "Nur meine"}
${d.berichte.length ? `
${d.berichte.map((b) => `
${sicher(b.titel)}
${b.eigener ? 'eigener ' : ""}
${sicher(b.nummer)}
${b.einsatz_nummer ? " · Einsatz " + sicher(b.einsatz_nummer) : ""}
· ${sicher(b.dienstnummer || "")} ${sicher((b.firstname || "") + " " + (b.lastname || ""))}
· ${zeit(b.erstellt_am)}
${sicher(statusText[b.status] || b.status)}
${b.unterschriften.length
? `✍ ${b.unterschriften.length} ` : ""}
`).join("")}
`
: `
Keine Berichte gefunden.
`}
`;
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 = `
${sicher(b.titel)}
${sicher(b.nummer)}
${b.einsatz_nummer ? " · Einsatz " + sicher(b.einsatz_nummer) : ""}
${b.aktenzeichen ? " · Akte " + sicher(b.aktenzeichen) : ""}
‹ Zurück
${bearbeitbar ? `✎ Bearbeiten ` : ""}
${!d.habeUnterschrieben && b.status !== "freigegeben"
? `✍ Unterschreiben ` : ""}
${b.status === "entwurf" && bearbeitbar
? `Einreichen ` : ""}
${b.status === "eingereicht" && d.rechte.freigeben
? `Nachbessern
Freigeben ` : ""}
${b.status === "freigegeben" ? `
🔒
Dieser Bericht ist freigegeben und damit inhaltlich gesperrt —
auch für den Verfasser.
` : ""}
Sachverhalt
${sicher(statusText[b.status] || b.status)}
Verfasser ${sicher(b.dienstnummer || "")}
${sicher((b.firstname || "") + " " + (b.lastname || ""))}
Angelegt ${zeit(b.erstellt_am)}
${b.beschreibung
? sicher(b.beschreibung).replace(/\n/g, " ")
: "Noch keine Beschreibung erfasst. "}
Getroffene Maßnahmen
${b.massnahmen
? sicher(b.massnahmen).replace(/\n/g, " ")
: "Keine Maßnahmen dokumentiert. "}
Beteiligte
${bearbeitbar ? `+ ` : ""}
${b.beteiligte.length ? `
${b.beteiligte.map((p, i) => `
${p.typ === "beamter" ? (p.dienstnummer || "Beamter") : "Person"}
${sicher(p.name)}
${p.rolle ? `
${sicher(p.rolle)}
` : ""}
${bearbeitbar && p.charakterId !== b.erstellt_von
? `
✕ ` : ""}
`).join("")}
`
: `
Keine Beteiligten erfasst.
`}
Anlagen & Beweise
${bearbeitbar ? `+ ` : ""}
${b.anlagen.length ? `
${b.anlagen.map((a, i) => `
${a.typ === "beweis" ? sicher(a.kennung) : "Anlage"}
${sicher(a.titel)}
${a.adresse ? `
${sicher(a.adresse)}
` : ""}
${a.adresse
? `
Öffnen ` : ""}
${bearbeitbar ? `
✕ ` : ""}
`).join("")}
`
: `
Keine Anlagen.
`}
Unterschriften
${b.unterschriften.length}
${b.unterschriften.length ? `
${b.unterschriften.map((u) => `
${sicher(u.name)}
${sicher(u.dienstnummer || "")} · ${sicher(u.rolle || "")}
${zeit(u.am)}${u.freigabe ? " · Freigabe" : ""}
`).join("")}
`
: `
Noch nicht unterschrieben.
`}
ℹ
Eine Unterschrift ist verbindlich und wird nicht zurückgenommen.
Sie bestätigt, dass der Inhalt aus deiner Sicht zutrifft.
`;
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: `
Titel
Sachverhalt
Getroffene Maßnahmen
${unterschrieben ? `
` : ""}`,
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: `
✍
Du bestätigst, dass der Inhalt von ${sicher(b.nummer)} aus deiner
Sicht zutrifft. Die Unterschrift ist verbindlich und kann nicht
zurückgenommen werden.
`,
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: `
${sicher(b.nummer)} geht zur Prüfung an die Führung. Bis zur Freigabe
kannst du ihn weiterhin ändern — jede Änderung nach dem Unterschreiben wird
allerdings vermerkt.
`,
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: `
Mit der Freigabe geschieht Folgendes:
🔒
Der Bericht wird inhaltlich gesperrt — auch der Verfasser kann
ihn danach nicht mehr ändern.
✍
Du unterschreibst den Bericht damit zugleich als freigebender Beamter.
Vermerk
`,
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: `Was soll nachgebessert werden?
`,
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: `
Titel
Bezug zu einem Einsatz
— ohne Einsatzbezug —
${(bez.einsaetze || []).map((e) =>
`${sicher(e.nummer)} · ${sicher(e.titel)} `).join("")}
Sachverhalt
Getroffene Maßnahmen
`,
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: `
Art
Person (Zivilist)
Beamter
Name (frei)
Beteiligung
`,
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
? `${d.treffer.slice(0, 6).map((p) =>
`
${sicher(p.firstname + " " + p.lastname)}
geb. ${sicher(String(p.dob || "").slice(0, 10))}
`).join("")}
`
: `Keine Treffer.
`;
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 = `Ausgewählt: ${sicher(zeile.dataset.name)}
`;
}));
} 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: `
Art
Datei / Bild
Vorhandenes Beweismittel
`,
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
? `${d.beweise.map((b) =>
`
${sicher(b.kennung)}
${sicher(b.bezeichnung)}
${sicher(b.fundort || "")}
`).join("")}
`
: `Keine Treffer.
`;
ziel.querySelectorAll("[data-wahl]").forEach((zeile) =>
zeile.addEventListener("click", () => {
el.dataset.beweis = zeile.dataset.wahl;
ziel.innerHTML = `Ausgewählt.
`;
}));
} 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 = `Berichte
Konnte nicht geladen werden: ${sicher(f.message)}
`;
}
}
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 []; }
});
})();