/* ===================================================================== aether-police / Ansicht: Interne Kommunikation Posteingang und Aushänge. Der Posteingang merkt sich, was du gelesen hast — das ist Anzeige. Eine Kenntnisnahme ist dagegen eine Erklärung und wird nur durch deinen ausdrücklichen Klick gesetzt. ===================================================================== */ "use strict"; (() => { const { sicher, zeit, aufruf, meldung, fenster, bestaetigen } = App; const z = { reiter: "eingang", // eingang | gesendet | aushang | archiv daten: null, aushangDaten: null, offeneNachricht: null, offenerAushang: null, }; // Farbgebung der Aushangkategorien const kategorieArt = { anweisung: "info", information: "neutral", warnung: "kritisch", }; const kategorieSymbol = { anweisung: "📋", information: "ℹ", warnung: "⚠", }; /* ---- Gerüst -------------------------------------------------------- */ function reiterleiste(d) { const eintraege = [ { id: "eingang", text: "Posteingang", zaehler: d.ungelesen }, { id: "gesendet", text: "Gesendet" }, { id: "aushang", text: "Aushänge", zaehler: d.offeneKenntnis }, { id: "archiv", text: "Archiv" }, ]; return `
${eintraege.map((e) => ` `).join("")}
`; } /* ---- Posteingang --------------------------------------------------- */ function postliste(behaelter) { const d = z.daten; const gesendet = z.reiter === "gesendet"; behaelter.innerHTML = `

Kommunikation

Nachrichten und Aushänge der Behörde. Zugestellt wird automatisch — gelesen, beantwortet und zur Kenntnis genommen wird von Hand.

${d.rechte.senden ? `` : ""}
${reiterleiste(d)}
${(d.nachrichten || []).length ? d.nachrichten.map((n) => postzeile(n, gesendet)).join("") : `
${gesendet ? "Du hast noch nichts gesendet." : "Keine Nachrichten."}
`}
`; verdrahtePost(behaelter); } function postzeile(n, gesendet) { const typMarker = n.empf_typ === "rundmeldung" ? `Rundmeldung` : n.empf_typ === "abteilung" ? `Abteilung` : ""; return `
${sicher(n.betreff || "(kein Betreff)")}
${gesendet ? "an " + sicher(n.empfaengerName) : "von " + sicher(n.absenderName) + (n.dienstnummer ? " · " + sicher(n.dienstnummer) : "")} — ${sicher(n.vorschau)}
${typMarker} ${zeit(n.gesendet)}
`; } function verdrahtePost(behaelter) { verdrahteReiter(behaelter); behaelter.querySelectorAll("[data-nachricht]").forEach((el) => { el.addEventListener("click", () => nachrichtOeffnen(behaelter, +el.dataset.nachricht)); }); const neu = behaelter.querySelector("[data-neu]"); if (neu) neu.addEventListener("click", () => verfassen(behaelter)); } function verdrahteReiter(behaelter) { behaelter.querySelectorAll("[data-reiter]").forEach((el) => { el.addEventListener("click", () => { z.reiter = el.dataset.reiter; rendern(behaelter); }); }); } /* ---- Nachricht lesen ------------------------------------------------ */ async function nachrichtOeffnen(behaelter, id) { try { const n = await aufruf("kommunikation", "nachrichtOeffnen", { id }); fenster({ titel: n.betreff || "(kein Betreff)", inhalt: `
${sicher(n.absenderName)} ${sicher(n.dienstnummer || "")} ${n.rolleName ? "· " + sicher(n.rolleName) : ""}
${zeit(n.gesendet)}
${n.empfaengerName ? `

Gerichtet an: ${sicher(n.empfaengerName)}

` : ""}
${sicher(n.text).replace(/\n/g, "
")}
`, knoepfe: n.antwortAn ? [{ text: "Antworten", klick: (_, zu) => { zu(); verfassen(behaelter, { empfTyp: "person", empfaenger: n.antwortAn, empfaengerName: n.absenderName, betreff: n.betreff && n.betreff.startsWith("Re:") ? n.betreff : "Re: " + (n.betreff || ""), }); } }, { text: "Schließen", art: "leise", klick: (_, zu) => zu() }] : [{ text: "Schließen", art: "leise", klick: (_, zu) => zu() }], }); // Der Lesestatus hat sich geändert — Liste auffrischen rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } } /* ---- Nachricht verfassen -------------------------------------------- */ async function verfassen(behaelter, vorgabe) { vorgabe = vorgabe || {}; let empfaengerDaten; try { empfaengerDaten = await aufruf("kommunikation", "empfaenger", {}); } catch (f) { return meldung(f.message, "fehler"); } let gewaehlt = vorgabe.empfaenger ? { typ: vorgabe.empfTyp, id: vorgabe.empfaenger, name: vorgabe.empfaengerName } : null; fenster({ titel: "Nachricht verfassen", inhalt: `
${empfaengerDaten.rundmeldung ? `` : ""}
`, knoepfe: [ { text: "Abbrechen", art: "leise", klick: (_, zu) => zu() }, { text: "Senden", klick: async (el, zu) => { if (!gewaehlt) return meldung("Bitte einen Empfänger wählen.", "fehler"); try { await aufruf("kommunikation", "senden", { empfTyp: gewaehlt.typ, empfaenger: gewaehlt.id, betreff: el.querySelector("[name=betreff]").value.trim(), text: el.querySelector("[name=text]").value.trim(), }); zu(); meldung("Nachricht gesendet.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } } }, ], nachOeffnen: (hg) => { const bereich = hg.querySelector("[data-empfbereich]"); /** Zeichnet den Empfängerbereich passend zur gewählten Art */ function zeichne(art) { hg.querySelectorAll("[data-art]").forEach((b) => b.classList.toggle("gewaehlt", b.dataset.art === art)); if (art === "rundmeldung") { gewaehlt = { typ: "rundmeldung", id: "alle", name: "Alle Einheiten" }; bereich.innerHTML = `
📣
Diese Nachricht erreicht jeden Beamten der Behörde.
`; return; } if (art === "abteilung") { gewaehlt = null; bereich.innerHTML = ``; bereich.querySelector("[name=abt]").addEventListener("change", (e) => { gewaehlt = e.target.value ? { typ: "abteilung", id: e.target.value, name: e.target.value } : null; }); return; } // Einzelner Beamter: Liste mit Suchfeld bereich.innerHTML = `
`; const liste = bereich.querySelector("[data-liste]"); const suchfeld = bereich.querySelector("[name=suche]"); function zeichneListe(begriff) { const gefiltert = empfaengerDaten.beamte.filter((b) => { if (!begriff) return true; const text = (b.firstname + " " + b.lastname + " " + b.dienstnummer).toLowerCase(); return text.includes(begriff.toLowerCase()); }).slice(0, 40); liste.innerHTML = gefiltert.length ? gefiltert.map((b) => `
${sicher(b.dienstnummer)}
${sicher(b.firstname)} ${sicher(b.lastname)}
${sicher(b.rolleName)}
`).join("") : `
Kein Beamter gefunden.
`; liste.querySelectorAll("[data-beamter]").forEach((el) => { el.addEventListener("click", () => { const b = empfaengerDaten.beamte.find((x) => x.character === +el.dataset.beamter); gewaehlt = { typ: "person", id: b.character, name: b.firstname + " " + b.lastname }; liste.querySelectorAll("[data-beamter]") .forEach((zeile) => zeile.classList.remove("gewaehlt")); el.classList.add("gewaehlt"); }); }); } zeichneListe(gewaehlt && gewaehlt.name ? gewaehlt.name : ""); suchfeld.addEventListener("input", () => zeichneListe(suchfeld.value.trim())); } hg.querySelectorAll("[data-art]").forEach((b) => b.addEventListener("click", () => zeichne(b.dataset.art))); zeichne(gewaehlt ? gewaehlt.typ : "person"); }, }); } /* ---- Aushänge -------------------------------------------------------- */ function aushangListe(behaelter) { const d = z.aushangDaten; behaelter.innerHTML = `

${d.archiv ? "Archivierte Aushänge" : "Aushänge"}

Dienstanweisungen und Mitteilungen der Leitung. Eine Kenntnisnahme bestätigst ausschließlich du selbst.

${d.rechte.schreiben && !d.archiv ? `` : ""}
${reiterleiste(z.daten || { ungelesen: 0, offeneKenntnis: 0 })}
${(d.aushaenge || []).length ? d.aushaenge.map((a) => aushangKarte(a, d)).join("") : `
${d.archiv ? "Das Archiv ist leer." : "Zurzeit hängt nichts aus."}
`}
`; verdrahteAushaenge(behaelter); } function aushangKarte(a, d) { const art = kategorieArt[a.kategorie] || "neutral"; const offen = a.betroffen - a.kenntnisse; return `
${kategorieSymbol[a.kategorie] || ""} ${sicher(a.titel)}
${a.angeheftet ? `angeheftet` : ""} ${sicher(a.kategorieName)} ${a.abteilungName ? `${sicher(a.abteilungName)}` : ""}
${sicher(a.text).replace(/\n/g, "
")}
${sicher(a.autor)}${a.dienstnummer ? " (" + sicher(a.dienstnummer) + ")" : ""} · ${zeit(a.erstellt_am)} ${a.frist ? ` · Kenntnisnahme bis ${zeit(a.frist)}` : ""}
${a.kenntnispflicht ? (a.bestaetigt ? `✓ zur Kenntnis genommen` : (a.aktiv === 1 ? `` : "")) : ""} ${a.kenntnispflicht && d.rechte.schreiben ? `` : ""} ${d.rechte.schreiben && a.aktiv === 1 ? `` : ""} ${d.rechte.archivieren && a.aktiv === 1 ? `` : ""}
`; } function verdrahteAushaenge(behaelter) { verdrahteReiter(behaelter); const neu = behaelter.querySelector("[data-neu-aushang]"); if (neu) neu.addEventListener("click", () => aushangVerfassen(behaelter)); behaelter.querySelectorAll("[data-kenntnis]").forEach((el) => { el.addEventListener("click", () => kenntnisBestaetigen(behaelter, +el.dataset.kenntnis)); }); behaelter.querySelectorAll("[data-stand]").forEach((el) => { el.addEventListener("click", () => kenntnisStand(+el.dataset.stand)); }); behaelter.querySelectorAll("[data-anheften]").forEach((el) => { el.addEventListener("click", async () => { try { await aufruf("kommunikation", "anheften", { id: +el.dataset.anheften, angeheftet: el.dataset.wert === "1", }); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }); }); behaelter.querySelectorAll("[data-archiv]").forEach((el) => { el.addEventListener("click", () => aushangArchivieren(behaelter, +el.dataset.archiv)); }); } /* ---- Kenntnisnahme ---------------------------------------------------- */ function kenntnisBestaetigen(behaelter, id) { // Bewusst über den Bestätigungsdialog: das ist eine Erklärung, kein Klick bestaetigen({ titel: "Kenntnisnahme bestätigen", inhalt: `

Du erklärst damit, den Aushang gelesen und verstanden zu haben. Die Bestätigung wird mit deinem Namen und der Uhrzeit vermerkt.

`, bestaetigenText: "Zur Kenntnis genommen", bestaetigt: async (_, zu) => { try { await aufruf("kommunikation", "kenntnisnahme", { id }); zu(); meldung("Kenntnisnahme vermerkt.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, }); } async function kenntnisStand(id) { try { const d = await aufruf("kommunikation", "kenntnisStand", { id }); fenster({ titel: "Stand der Kenntnisnahmen", inhalt: `

${sicher(d.titel)}

${d.gesamt - d.offen} von ${d.gesamt} Beamten haben bestätigt. ${d.frist ? "Frist: " + zeit(d.frist) + "." : ""} Das System erinnert nur — es bestätigt für niemanden.

${d.beamte.map((b) => `
${sicher(b.dienstnummer)}
${sicher(b.name)}
${b.bestaetigt_am ? `${zeit(b.bestaetigt_am)}` : `offen`}
`).join("")}
`, }); } catch (f) { meldung(f.message, "fehler"); } } /* ---- Aushang verfassen und archivieren -------------------------------- */ function aushangVerfassen(behaelter) { const d = z.aushangDaten; bestaetigen({ titel: "Aushang verfassen", inhalt: `
Beamte bestätigen selbst. Das System erinnert nur an offene Bestätigungen und setzt nie eine.
`, bestaetigenText: "Veröffentlichen", bestaetigt: async (el, zu) => { const kenntnispflicht = el.querySelector("[name=kenntnispflicht]").checked; const frist = +el.querySelector("[name=frist]").value; try { await aufruf("kommunikation", "aushangErstellen", { titel: el.querySelector("[name=titel]").value.trim(), text: el.querySelector("[name=text]").value.trim(), kategorie: el.querySelector("[name=kategorie]").value, abteilung: el.querySelector("[name=abteilung]").value, angeheftet: el.querySelector("[name=angeheftet]").checked, kenntnispflicht, fristMinuten: kenntnispflicht && frist > 0 ? frist : null, }); zu(); meldung("Aushang veröffentlicht.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, nachOeffnen: (hg) => { // Das Fristfeld ist nur sinnvoll, wenn eine Kenntnisnahme verlangt wird const haken = hg.querySelector("[name=kenntnispflicht]"); const fristfeld = hg.querySelector("[data-fristfeld]"); haken.addEventListener("change", () => { fristfeld.style.display = haken.checked ? "" : "none"; }); }, }); } function aushangArchivieren(behaelter, id) { bestaetigen({ titel: "Aushang archivieren", gefahr: true, inhalt: `

Der Aushang verschwindet vom Brett und wandert ins Archiv. Bereits bestätigte Kenntnisnahmen bleiben erhalten.

`, bestaetigenText: "Archivieren", bestaetigt: async (el, zu) => { try { await aufruf("kommunikation", "aushangArchivieren", { id, grund: el.querySelector("[name=grund]").value.trim(), }); zu(); meldung("Aushang archiviert.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, }); } /* ---- Laden ------------------------------------------------------------- */ async function rendern(behaelter) { try { // Die Zähler der Reiterleiste kommen immer aus der Übersicht z.daten = await aufruf("kommunikation", "uebersicht", { ordner: z.reiter === "gesendet" ? "gesendet" : "eingang", }); // Sprungziel aus einem Hinweis, z. B. „Neuer Aushang" const ziel = App.zustand.modulZiel; if (ziel && ziel.aushang) { App.zustand.modulZiel = null; z.reiter = "aushang"; } else if (ziel && ziel.nachricht) { App.zustand.modulZiel = null; z.reiter = "eingang"; postliste(behaelter); return nachrichtOeffnen(behaelter, ziel.nachricht); } if (z.reiter === "aushang" || z.reiter === "archiv") { z.aushangDaten = await aufruf("kommunikation", "aushaenge", { archiv: z.reiter === "archiv", }); aushangListe(behaelter); } else { postliste(behaelter); } } catch (f) { behaelter.innerHTML = `

Kommunikation

Konnte nicht geladen werden: ${sicher(f.message)}

`; } } App.registriereAnsicht("kommunikation", { rendern }); })();