/* =====================================================================
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) => `
${sicher(e.text)}
${e.zaehler ? `${e.zaehler} ` : ""}
`).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
? `
✉ Nachricht verfassen ` : ""}
${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: `
Empfänger
Beamter
Abteilung
${empfaengerDaten.rundmeldung
? `Alle Einheiten ` : ""}
Betreff
Nachricht
`,
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 = `
— Abteilung wählen —
${empfaengerDaten.abteilungen.map((a) =>
`${sicher(a.name)} `).join("")}
`;
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
? `
+ Aushang verfassen ` : ""}
${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
? `
Kenntnis bestätigen ` : "")) : ""}
${a.kenntnispflicht && d.rechte.schreiben
? `
${a.kenntnisse}/${a.betroffen} bestätigt${offen > 0 ? " · " + offen + " offen" : ""}
` : ""}
${d.rechte.schreiben && a.aktiv === 1
? `
${a.angeheftet ? "Loslösen" : "Anheften"} ` : ""}
${d.rechte.archivieren && a.aktiv === 1
? `Archivieren ` : ""}
`;
}
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)}
${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: `
Titel
Art
${Object.keys(d.kategorien).map((k) =>
`${sicher(d.kategorien[k])} `).join("")}
Gilt für
Gesamte Behörde
${d.abteilungen.map((a) =>
`${sicher(a.name)} `).join("")}
Inhalt
Oben anheften
Frist für die Kenntnisnahme (Minuten)
`,
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.
Grund (freiwillig)
`,
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 });
})();