/* ===================================================================== aether-medical / Ansicht: Nachrichten und Aushänge Postfach und Schwarzes Brett. Abgeschickt ist abgeschickt — es gibt hier bewusst kein Bearbeiten und kein Löschen von Nachrichten. Ein Aushang wird archiviert, nie entfernt. Und eine Kenntnisnahme setzt jeder nur für sich selbst. ===================================================================== */ "use strict"; (() => { const { sicher, zeit, minuten, aufruf, meldung, fenster, bestaetigen } = App; const z = { ordner: "posteingang", // posteingang | gesendet | aushang | archiv daten: null, }; // Marker-Art je Empfängerart const typArt = { rundmeldung: "kritisch", station: "warn", person: "info" }; const typName = { rundmeldung: "Rundmeldung", station: "Station", person: "Persönlich" }; /* ---- Kleine Helfer ---------------------------------------------------- */ /** Anzeigename einer Aushangkategorie */ function kategorieName(kennung) { const treffer = (z.daten.kategorien || []).find((k) => k.id === kennung); return treffer ? treffer.name : kennung; } /** Farbe einer Aushangkategorie */ function kategorieFarbe(kennung) { const treffer = (z.daten.kategorien || []).find((k) => k.id === kennung); return treffer ? treffer.farbe : "#8697a4"; } /** Beschreibt eine Frist in Worten */ function fristText(a) { if (a.restMinuten === null || a.restMinuten === undefined) return null; if (a.restMinuten <= 0) return `Frist seit ${minuten(-a.restMinuten)} abgelaufen`; return `noch ${minuten(a.restMinuten)}`; } /* ---- Gerüst ------------------------------------------------------------ */ function reiterleiste(d) { const eintraege = [ { id: "posteingang", text: "Posteingang", zahl: d.zaehler.ungelesen }, { id: "gesendet", text: "Gesendet", zahl: d.zaehler.gesendet }, { id: "aushang", text: "Schwarzes Brett", zahl: d.zaehler.aushaenge }, { id: "archiv", text: "Archiv", zahl: 0 }, ]; return `
${eintraege.map((e) => ` `).join("")}
`; } function kennzahlen(d) { const zk = d.zaehler; return `
Ungelesen
${zk.ungelesen}
im Posteingang
Kenntnisnahme offen
${zk.offeneKenntnis}
von dir noch nicht bestätigt
Aushänge
${zk.aushaenge}
hängen gerade
Von dir gesendet
${zk.gesendet}
bleiben unverändert stehen
`; } /* ---- Übersicht ---------------------------------------------------------- */ function uebersicht(behaelter) { const d = z.daten; const amBrett = z.ordner === "aushang" || z.ordner === "archiv"; behaelter.innerHTML = `

Nachrichten und Aushänge

Was hier steht, hat jemand geschrieben und unterschrieben. Eine gesendete Nachricht lässt sich nicht mehr ändern, ein Aushang wird archiviert statt gelöscht.

${d.rechte.senden ? `` : ""} ${d.rechte.schreiben ? `` : ""}
${kennzahlen(d)} ${reiterleiste(d)} ${amBrett ? brettAnsicht(d) : postAnsicht(d)} `; verdrahteUebersicht(behaelter); } /* ---- Postfach ------------------------------------------------------------- */ function postAnsicht(d) { const gesendet = z.ordner === "gesendet"; return `
${gesendet ? "Von dir gesendet" : "Posteingang"} ${gesendet ? "Gelesen heißt: jemand hat sie geöffnet" : "Als gelesen gilt, was du geöffnet hast"}
${d.liste.length ? d.liste.map(gesendet ? gesendetZeile : postZeile).join("") : `
${gesendet ? "Du hast noch nichts geschrieben." : "Keine Nachrichten."}
`}
`; } function postZeile(n) { return `
${sicher(n.betreff || "(ohne Betreff)")}
${sicher(n.absenderName)} · ${sicher(n.vorschau)}
${sicher(typName[n.empf_typ] || n.empf_typ)} ${zeit(n.gesendet)}
`; } function gesendetZeile(n) { return `
${sicher(n.betreff || "(ohne Betreff)")}
an ${sicher(n.empfaengerName)} · ${sicher(n.vorschau)}
${sicher(typName[n.empf_typ] || n.empf_typ)} ${n.gelesenVon} gelesen ${zeit(n.gesendet)}
`; } /* ---- Schwarzes Brett ------------------------------------------------------- */ function brettAnsicht(d) { const archiv = z.ordner === "archiv"; if (!d.liste.length) { return `
${archiv ? "Nichts archiviert." : "Am Brett hängt nichts."}
`; } return `
${d.liste.map((a) => `
${a.angeheftet ? `📌` : ""} ${sicher(a.titel)}
${sicher(kategorieName(a.kategorie))}
${sicher(a.vorschau)}
${sicher(a.autor)} · ${zeit(a.erstellt_am)} ${a.station ? `${sicher(a.stationName)}` : ""} ${a.kenntnispflicht ? (a.bestaetigt ? `gelesen` : `Kenntnis offen`) : ""} ${a.kenntnispflicht ? `${a.kenntnisse} bestätigt` : ""} ${fristText(a) ? ` ${sicher(fristText(a))}` : ""}
${archiv && a.archiviertVon ? `
Abgenommen von ${sicher(a.archiviertVon)} · ${zeit(a.archiviert_am)}
` : ""}
`).join("")}
`; } /* ---- Verdrahtung ------------------------------------------------------------ */ function verdrahteUebersicht(behaelter) { behaelter.querySelectorAll("[data-reiter]").forEach((el) => { el.addEventListener("click", () => { z.ordner = el.dataset.reiter; rendern(behaelter); }); }); behaelter.querySelectorAll("[data-nachricht]").forEach((el) => { el.addEventListener("click", () => nachrichtFenster(behaelter, +el.dataset.nachricht)); }); behaelter.querySelectorAll("[data-aushang]").forEach((el) => { el.addEventListener("click", () => aushangFenster(behaelter, +el.dataset.aushang)); }); const schreiben = behaelter.querySelector("[data-schreiben]"); if (schreiben) schreiben.addEventListener("click", () => schreibenFenster(behaelter, null)); const neuerAushang = behaelter.querySelector("[data-neueraushang]"); if (neuerAushang) { neuerAushang.addEventListener("click", () => aushangSchreibenFenster(behaelter, null)); } } /* ---- Nachricht lesen ---------------------------------------------------------- */ async function nachrichtFenster(behaelter, nachrichtId) { let d; try { d = await aufruf("kommunikation", "nachricht", { id: nachrichtId }); } catch (f) { return meldung(f.message, "fehler"); } const n = d.nachricht; const dialog = fenster({ titel: n.betreff || "(ohne Betreff)", inhalt: `
Von${sicher(n.absenderName)}
An${sicher(n.empfaengerName)}
Gesendet${zeit(n.gesendet)}
${sicher(n.text)}
${n.vonMir ? `
Gelesen von ${d.leser.length} Person(en)
${d.leser.length ? d.leser.map((l) => `
${sicher(l.name)}
${zeit(l.gelesen_am)}
`).join("") : `
Noch niemand hat sie geöffnet.
`}
` : ""} ${!n.vonMir && d.rechte.senden ? `
` : ""} `, nachOeffnen: (hg) => { const antworten = hg.querySelector("[data-antworten]"); if (antworten) { antworten.addEventListener("click", () => { dialog.schliessen(); schreibenFenster(behaelter, { typ: "person", ziel: n.absender, zielName: n.absenderName, betreff: n.betreff && !/^Re:/i.test(n.betreff) ? "Re: " + n.betreff : (n.betreff || ""), }); }); } }, }); // Der Posteingang zeigt sie danach als gelesen rendernStill(behaelter); } /* ---- Nachricht schreiben -------------------------------------------------------- */ async function schreibenFenster(behaelter, vorgabe) { let e; try { e = await aufruf("kommunikation", "empfaenger", {}); } catch (f) { return meldung(f.message, "fehler"); } let typ = (vorgabe && vorgabe.typ) || "person"; let ziel = (vorgabe && vorgabe.ziel) || null; bestaetigen({ titel: "Nachricht schreiben", inhalt: `
ℹ
Eine abgeschickte Nachricht lässt sich weder ändern noch löschen. Sie bleibt mit deinem Namen stehen.
${e.rechte.rundmeldung ? `` : ""}
${e.personal.map((p) => `
${sicher(p.name)} ${p.selbst ? `du` : ""}
${sicher(p.rolleName)} · ${sicher(p.personalnummer || "")} · ${sicher(p.stationName)}
${p.imDienst ? `im Dienst` : `außer Dienst`}
`).join("")}
Wer nicht im Dienst ist, bekommt die Nachricht beim nächsten Öffnen des Systems.
📣
Diese Nachricht geht an jeden im Haus und wird allen im Dienst sofort gemeldet.
`, bestaetigenText: "Abschicken", bestaetigt: async (el, zu) => { const text = el.querySelector("[name=text]").value.trim(); if (text.length < 2) return meldung("Bitte einen Text schreiben.", "fehler"); if (typ === "person" && !ziel) { return meldung("Bitte einen Empfänger wählen.", "fehler"); } const zielWert = typ === "station" ? el.querySelector("[name=station]").value : (typ === "person" ? ziel : null); try { const antwort = await aufruf("kommunikation", "senden", { typ, ziel: zielWert, betreff: el.querySelector("[name=betreff]").value.trim(), text, }); zu(); meldung(`Nachricht an ${antwort.empfaenger} abgeschickt.`, "gut"); z.ordner = "gesendet"; rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, nachOeffnen: (hg) => { // Empfängerart umschalten hg.querySelectorAll("[data-typ]").forEach((el) => { el.addEventListener("click", () => { typ = el.dataset.typ; hg.querySelectorAll("[data-typ]").forEach((k) => k.classList.remove("aktiv")); el.classList.add("aktiv"); hg.querySelectorAll("[data-wahl]").forEach((block) => { block.style.display = block.dataset.wahl === typ ? "" : "none"; }); }); }); // Person wählen hg.querySelectorAll("[data-person]").forEach((el) => { el.addEventListener("click", () => { ziel = +el.dataset.person; hg.querySelectorAll("[data-person]") .forEach((zeile) => zeile.classList.remove("gewaehlt")); el.classList.add("gewaehlt"); }); }); // Personenliste filtern const suche = hg.querySelector("[name=suche]"); suche.addEventListener("input", () => { const begriff = suche.value.trim().toLowerCase(); hg.querySelectorAll("[data-person]").forEach((el) => { el.style.display = !begriff || el.dataset.suchtext.includes(begriff) ? "" : "none"; }); }); }, }); } /* ---- Aushang lesen -------------------------------------------------------------- */ async function aushangFenster(behaelter, aushangId) { let d; try { d = await aufruf("kommunikation", "aushang", { id: aushangId }); } catch (f) { return meldung(f.message, "fehler"); } const a = d.aushang; const dialog = fenster({ titel: a.titel, inhalt: ` ${a.aktiv ? "" : `
📁
Archiviert — abgenommen von ${sicher(a.archiviertVon || "unbekannt")} am ${zeit(a.archiviertAm)}. Der Wortlaut bleibt nachlesbar.
`} ${a.kenntnispflicht && !a.bestaetigt && a.aktiv ? `
✋
Kenntnisnahme steht aus. Bestätigen kannst nur du selbst — niemand kann das für dich tun.
` : ""} ${fristText(a) ? `
🕓
${sicher(fristText(a))}. Eine abgelaufene Frist erinnert — sie hat keine weitere Folge.
` : ""}
Kategorie${sicher(kategorieName(a.kategorie))}
Gilt für${sicher(a.stationName)}
Veröffentlicht${zeit(a.erstelltAm)}
Von${sicher(a.autor)}
${sicher(a.text)}
${a.aktiv && a.kenntnispflicht && !a.bestaetigt ? `` : ""} ${a.aktiv && d.rechte.schreiben ? ` ` : ""} ${a.aktiv && d.rechte.archivieren ? `` : ""}
${a.textGesperrt && a.aktiv ? `
${d.kenntnis.length} Person(en) haben diesen Wortlaut gelesen — Titel und Text lassen sich deshalb nicht mehr ändern. Für eine Änderung: abnehmen und neu veröffentlichen.
` : ""} ${a.kenntnispflicht ? `
Zur Kenntnis genommen ${d.kenntnis.length}
${d.kenntnis.length ? d.kenntnis.map((k) => `
${sicher(k.personalnummer || "—")}
${sicher(k.name)}
${zeit(k.bestaetigt_am)}
`).join("") : `
Noch niemand.
`}
Steht noch aus ${d.offen.length}
${d.offen.length ? d.offen.map((o) => `
${sicher(o.personalnummer || "—")}
${sicher(o.name)}
`).join("") : `
Alle haben bestätigt.
`}
` : ""} `, nachOeffnen: (hg) => { const kenntnis = hg.querySelector("[data-kenntnis]"); if (kenntnis) { kenntnis.addEventListener("click", async () => { try { await aufruf("kommunikation", "kenntnis", { id: a.id }); dialog.schliessen(); meldung("Kenntnisnahme bestätigt.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }); } const anheften = hg.querySelector("[data-anheften]"); if (anheften) { anheften.addEventListener("click", async () => { try { await aufruf("kommunikation", "anheften", { id: a.id, angeheftet: !a.angeheftet, }); dialog.schliessen(); meldung(a.angeheftet ? "Aushang gelöst." : "Aushang angeheftet.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }); } const bearbeiten = hg.querySelector("[data-bearbeiten]"); if (bearbeiten) { bearbeiten.addEventListener("click", () => { dialog.schliessen(); aushangSchreibenFenster(behaelter, a); }); } const archivieren = hg.querySelector("[data-archivieren]"); if (archivieren) { archivieren.addEventListener("click", () => { dialog.schliessen(); archivierenFenster(behaelter, a); }); } }, }); } /* ---- Aushang veröffentlichen ------------------------------------------------------ */ function aushangSchreibenFenster(behaelter, a) { const d = z.daten; const neu = !a; bestaetigen({ titel: neu ? "Aushang veröffentlichen" : `„${a.titel}" bearbeiten`, inhalt: ` ${!neu && a.textGesperrt ? `
✋
Titel und Text sind gesperrt. Es haben bereits Personen diesen Wortlaut zur Kenntnis genommen. Für eine inhaltliche Änderung den Aushang abnehmen und neu veröffentlichen.
` : ""}
Das System erinnert die Betroffenen. Bestätigen kann jeder nur für sich — erzwungen wird nichts.
Läuft die Frist ab, geschieht nichts weiter — es wird nur deutlicher erinnert.
`, bestaetigenText: neu ? "Veröffentlichen" : "Speichern", bestaetigt: async (el, zu) => { const gesperrt = !neu && a.textGesperrt; const titel = gesperrt ? a.titel : el.querySelector("[name=titel]").value.trim(); const text = gesperrt ? a.text : el.querySelector("[name=text]").value.trim(); if (!titel) return meldung("Bitte einen Titel angeben.", "fehler"); if (text.length < 5) return meldung("Bitte einen Text schreiben.", "fehler"); try { await aufruf("kommunikation", "aushangSchreiben", { id: neu ? null : a.id, titel, text, kategorie: el.querySelector("[name=kategorie]").value, station: el.querySelector("[name=station]").value, angeheftet: el.querySelector("[name=angeheftet]").checked, kenntnispflicht: el.querySelector("[name=kenntnispflicht]").checked, frist: el.querySelector("[name=frist]").value, }); zu(); meldung(neu ? "Aushang hängt." : "Aushang gespeichert.", "gut"); z.ordner = "aushang"; rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, }); } function archivierenFenster(behaelter, a) { bestaetigen({ titel: "Aushang abnehmen", gefahr: true, inhalt: `

„${sicher(a.titel)}" wird vom Brett genommen und wandert ins Archiv. Gelöscht wird nichts — der Wortlaut und alle Kenntnisnahmen bleiben.

${a.kenntnispflicht ? `
ℹ
Offene Kenntnisnahmen halten das Abnehmen nicht auf. Wie viele noch ausstanden, wird im Protokoll festgehalten.
` : ""}
`, bestaetigenText: "Abnehmen", bestaetigt: async (el, zu) => { const grund = el.querySelector("[name=grund]").value.trim(); if (!grund) return meldung("Bitte einen Grund angeben.", "fehler"); try { const antwort = await aufruf("kommunikation", "aushangArchivieren", { id: a.id, grund }); zu(); meldung(antwort.offen > 0 ? `Abgenommen — ${antwort.offen} Kenntnisnahme(n) standen noch aus.` : "Aushang abgenommen.", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, }); } /* ---- Laden ------------------------------------------------------------------------ */ /** Frischt die Liste im Hintergrund auf, ohne ein offenes Fenster zu stören */ async function rendernStill(behaelter) { try { z.daten = await aufruf("kommunikation", "uebersicht", { ordner: z.ordner }); if (!document.querySelector(".fenster-hg")) uebersicht(behaelter); } catch (f) { /* eine stille Auffrischung darf folgenlos scheitern */ } } async function rendern(behaelter) { try { z.daten = await aufruf("kommunikation", "uebersicht", { ordner: z.ordner }); uebersicht(behaelter); // Sprungziel aus einem Hinweis const ziel = App.zustand.modulZiel; if (ziel && ziel.nachricht) { App.zustand.modulZiel = null; return nachrichtFenster(behaelter, ziel.nachricht); } if (ziel && ziel.aushang) { App.zustand.modulZiel = null; return aushangFenster(behaelter, ziel.aushang); } } catch (f) { behaelter.innerHTML = `

Nachrichten und Aushänge

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

`; } } App.registriereAnsicht("kommunikation", { rendern, aktualisieren() { if (App.zustand.aktivesModul === "kommunikation" && !document.querySelector(".fenster-hg")) { rendern(App.$("inhalt")); } }, }); })();