aether-framework/resources/[aether]/aether-medical/html/js/module/kommunikation.js
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

742 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
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 `<div class="reiter">
${eintraege.map((e) => `
<button class="${z.ordner === e.id ? "aktiv" : ""}" data-reiter="${e.id}">
${sicher(e.text)}${e.zahl > 0 ? `<span class="mdt-zahl">${e.zahl}</span>` : ""}
</button>`).join("")}
</div>`;
}
function kennzahlen(d) {
const zk = d.zaehler;
return `<div class="raster k4">
<div class="kennzahl ${zk.ungelesen > 0 ? "warn" : ""}">
<div class="kz-titel">Ungelesen</div>
<div class="kz-wert">${zk.ungelesen}</div>
<div class="kz-zusatz">im Posteingang</div>
</div>
<div class="kennzahl ${zk.offeneKenntnis > 0 ? "hoch" : "gut"}">
<div class="kz-titel">Kenntnisnahme offen</div>
<div class="kz-wert">${zk.offeneKenntnis}</div>
<div class="kz-zusatz">von dir noch nicht bestätigt</div>
</div>
<div class="kennzahl">
<div class="kz-titel">Aushänge</div>
<div class="kz-wert">${zk.aushaenge}</div>
<div class="kz-zusatz">hängen gerade</div>
</div>
<div class="kennzahl">
<div class="kz-titel">Von dir gesendet</div>
<div class="kz-wert">${zk.gesendet}</div>
<div class="kz-zusatz">bleiben unverändert stehen</div>
</div>
</div>`;
}
/* ---- Übersicht ---------------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
const amBrett = z.ordner === "aushang" || z.ordner === "archiv";
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Nachrichten und Aushänge</h2>
<p>Was hier steht, hat jemand geschrieben und unterschrieben. Eine gesendete
Nachricht lässt sich nicht mehr ändern, ein Aushang wird archiviert statt
gelöscht.</p>
</div>
<div class="ls-kopf-aktionen">
${d.rechte.senden
? `<button class="knopf leise" data-schreiben="1">✉ Nachricht schreiben</button>` : ""}
${d.rechte.schreiben
? `<button class="knopf" data-neueraushang="1">+ Aushang veröffentlichen</button>` : ""}
</div>
</div>
${kennzahlen(d)}
${reiterleiste(d)}
${amBrett ? brettAnsicht(d) : postAnsicht(d)}
`;
verdrahteUebersicht(behaelter);
}
/* ---- Postfach ------------------------------------------------------------- */
function postAnsicht(d) {
const gesendet = z.ordner === "gesendet";
return `<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">${gesendet ? "Von dir gesendet" : "Posteingang"}</span>
<span class="feld-hinweis">${gesendet
? "Gelesen heißt: jemand hat sie geöffnet"
: "Als gelesen gilt, was du geöffnet hast"}</span>
</div>
<div class="liste">
${d.liste.length ? d.liste.map(gesendet ? gesendetZeile : postZeile).join("")
: `<div class="liste-leer">${gesendet
? "Du hast noch nichts geschrieben." : "Keine Nachrichten."}</div>`}
</div>
</div>`;
}
function postZeile(n) {
return `<div class="liste-zeile ${n.gelesen ? "" : "nk-ungelesen"}"
data-nachricht="${n.id}">
<span class="nk-punkt ${n.gelesen ? "" : "offen"}"></span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(n.betreff || "(ohne Betreff)")}</div>
<div class="lz-unter">${sicher(n.absenderName)} · ${sicher(n.vorschau)}</div>
</div>
<span class="marker ${typArt[n.empf_typ] || "neutral"}">
${sicher(typName[n.empf_typ] || n.empf_typ)}</span>
<span class="db-zeit">${zeit(n.gesendet)}</span>
</div>`;
}
function gesendetZeile(n) {
return `<div class="liste-zeile" data-nachricht="${n.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(n.betreff || "(ohne Betreff)")}</div>
<div class="lz-unter">an ${sicher(n.empfaengerName)} · ${sicher(n.vorschau)}</div>
</div>
<span class="marker ${typArt[n.empf_typ] || "neutral"}">
${sicher(typName[n.empf_typ] || n.empf_typ)}</span>
<span class="marker neutral">${n.gelesenVon} gelesen</span>
<span class="db-zeit">${zeit(n.gesendet)}</span>
</div>`;
}
/* ---- Schwarzes Brett ------------------------------------------------------- */
function brettAnsicht(d) {
const archiv = z.ordner === "archiv";
if (!d.liste.length) {
return `<div class="karte"><div class="liste-leer">${archiv
? "Nichts archiviert." : "Am Brett hängt nichts."}</div></div>`;
}
return `<div class="ah-raster">${d.liste.map((a) => `
<div class="ah-karte ${archiv ? "archiviert" : ""}
${a.angeheftet ? "angeheftet" : ""}" data-aushang="${a.id}"
style="border-left-color:${sicher(kategorieFarbe(a.kategorie))}">
<div class="ah-kopf">
<div class="ah-titel">
${a.angeheftet ? `<span class="ah-nadel">📌</span>` : ""}
${sicher(a.titel)}</div>
<span class="marker neutral">${sicher(kategorieName(a.kategorie))}</span>
</div>
<div class="ah-text">${sicher(a.vorschau)}</div>
<div class="ah-fuss">
<span>${sicher(a.autor)} · ${zeit(a.erstellt_am)}</span>
<span class="ah-marker">
${a.station ? `<span class="marker info">${sicher(a.stationName)}</span>` : ""}
${a.kenntnispflicht
? (a.bestaetigt
? `<span class="marker gut">gelesen</span>`
: `<span class="marker kritisch">Kenntnis offen</span>`)
: ""}
${a.kenntnispflicht
? `<span class="marker neutral">${a.kenntnisse} bestätigt</span>` : ""}
${fristText(a)
? `<span class="marker ${a.fristAbgelaufen ? "kritisch" : "warn"}">
${sicher(fristText(a))}</span>` : ""}
</span>
</div>
${archiv && a.archiviertVon
? `<div class="ah-archiv">Abgenommen von ${sicher(a.archiviertVon)} ·
${zeit(a.archiviert_am)}</div>` : ""}
</div>`).join("")}</div>`;
}
/* ---- 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: `
<div class="werte-liste">
<div><span>Von</span><b>${sicher(n.absenderName)}</b></div>
<div><span>An</span><b>${sicher(n.empfaengerName)}</b></div>
<div><span>Gesendet</span><b>${zeit(n.gesendet)}</b></div>
</div>
<div class="beh-text" style="margin-top:12px; white-space:pre-wrap">${sicher(n.text)}</div>
${n.vonMir ? `
<div class="karte-kopf" style="margin-top:14px">
<span class="karte-titel">Gelesen von</span>
<span class="feld-hinweis">${d.leser.length} Person(en)</span>
</div>
<div class="liste">
${d.leser.length ? d.leser.map((l) => `
<div class="liste-zeile">
<div class="lz-haupt"><div class="lz-titel">${sicher(l.name)}</div></div>
<span class="db-zeit">${zeit(l.gelesen_am)}</span>
</div>`).join("")
: `<div class="liste-leer">Noch niemand hat sie geöffnet.</div>`}
</div>` : ""}
${!n.vonMir && d.rechte.senden ? `
<div class="feld-reihe" style="margin-top:14px">
<button class="knopf klein" data-antworten="1">Antworten</button>
</div>` : ""}
`,
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: `
<div class="warnung">
<span class="w-symbol">ℹ</span>
<div>Eine abgeschickte Nachricht lässt sich weder ändern noch löschen.
Sie bleibt mit deinem Namen stehen.</div>
</div>
<div class="feld"><label>Empfänger</label>
<div class="reiter nk-typwahl">
<button class="${typ === "person" ? "aktiv" : ""}" data-typ="person">Person</button>
<button class="${typ === "station" ? "aktiv" : ""}" data-typ="station">Station</button>
${e.rechte.rundmeldung
? `<button class="${typ === "rundmeldung" ? "aktiv" : ""}"
data-typ="rundmeldung">Alle im Haus</button>` : ""}
</div>
</div>
<div data-wahl="person" class="feld" ${typ === "person" ? "" : 'style="display:none"'}>
<input name="suche" placeholder="Nach Name oder Personalnummer filtern…">
<div class="liste pat-trefferliste" data-personen="1" style="margin-top:8px">
${e.personal.map((p) => `
<div class="liste-zeile ${p.charakterId === ziel ? "gewaehlt" : ""}"
data-person="${p.charakterId}"
data-suchtext="${sicher((p.name + " " + (p.personalnummer || "")).toLowerCase())}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(p.name)}
${p.selbst ? `<span class="marker info">du</span>` : ""}</div>
<div class="lz-unter">${sicher(p.rolleName)} ·
${sicher(p.personalnummer || "")} · ${sicher(p.stationName)}</div>
</div>
${p.imDienst ? `<span class="marker gut">im Dienst</span>`
: `<span class="marker neutral">außer Dienst</span>`}
</div>`).join("")}
</div>
<div class="feld-hinweis">Wer nicht im Dienst ist, bekommt die Nachricht
beim nächsten Öffnen des Systems.</div>
</div>
<div data-wahl="station" class="feld" ${typ === "station" ? "" : 'style="display:none"'}>
<label>Station</label>
<select name="station">
${e.stationen.map((s) =>
`<option value="${sicher(s.id)}">${sicher(s.name)}</option>`).join("")}
</select>
</div>
<div data-wahl="rundmeldung" class="warnung warn"
${typ === "rundmeldung" ? "" : 'style="display:none"'}>
<span class="w-symbol">📣</span>
<div>Diese Nachricht geht an <b>jeden</b> im Haus und wird allen im Dienst
sofort gemeldet.</div>
</div>
<div class="feld"><label>Betreff</label>
<input name="betreff" maxlength="160"
value="${vorgabe && vorgabe.betreff ? sicher(vorgabe.betreff) : ""}"></div>
<div class="feld"><label>Text</label>
<textarea name="text" rows="6" maxlength="4000"
placeholder="Was möchtest du mitteilen?"></textarea></div>`,
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 ? "" : `
<div class="warnung">
<span class="w-symbol">📁</span>
<div><b>Archiviert</b> — abgenommen von ${sicher(a.archiviertVon || "unbekannt")}
am ${zeit(a.archiviertAm)}. Der Wortlaut bleibt nachlesbar.</div>
</div>`}
${a.kenntnispflicht && !a.bestaetigt && a.aktiv ? `
<div class="warnung warn">
<span class="w-symbol">✋</span>
<div><b>Kenntnisnahme steht aus.</b> Bestätigen kannst nur du selbst —
niemand kann das für dich tun.</div>
</div>` : ""}
${fristText(a) ? `
<div class="warnung ${a.fristAbgelaufen ? "kritisch" : ""}">
<span class="w-symbol">🕓</span>
<div>${sicher(fristText(a))}. Eine abgelaufene Frist erinnert —
sie hat keine weitere Folge.</div>
</div>` : ""}
<div class="werte-liste">
<div><span>Kategorie</span><b>${sicher(kategorieName(a.kategorie))}</b></div>
<div><span>Gilt für</span><b>${sicher(a.stationName)}</b></div>
<div><span>Veröffentlicht</span><b>${zeit(a.erstelltAm)}</b></div>
<div><span>Von</span><b>${sicher(a.autor)}</b></div>
</div>
<div class="beh-text" style="margin-top:12px; white-space:pre-wrap">${sicher(a.text)}</div>
<div class="feld-reihe" style="margin-top:14px">
${a.aktiv && a.kenntnispflicht && !a.bestaetigt
? `<button class="knopf klein" data-kenntnis="1">Zur Kenntnis genommen</button>` : ""}
${a.aktiv && d.rechte.schreiben
? `<button class="knopf leise klein" data-bearbeiten="1">Bearbeiten</button>
<button class="knopf leise klein" data-anheften="1">
${a.angeheftet ? "Lösen" : "Anheften"}</button>` : ""}
${a.aktiv && d.rechte.archivieren
? `<button class="knopf gefahr klein" data-archivieren="1">Abnehmen</button>` : ""}
</div>
${a.textGesperrt && a.aktiv ? `
<div class="feld-hinweis" style="margin-top:8px">
${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.</div>` : ""}
${a.kenntnispflicht ? `
<div class="db-spalten" style="margin-top:14px">
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Zur Kenntnis genommen</span>
<span class="feld-hinweis">${d.kenntnis.length}</span>
</div>
<div class="liste">
${d.kenntnis.length ? d.kenntnis.map((k) => `
<div class="liste-zeile">
<span class="db-nummer">${sicher(k.personalnummer || "—")}</span>
<div class="lz-haupt"><div class="lz-titel">${sicher(k.name)}</div></div>
<span class="db-zeit">${zeit(k.bestaetigt_am)}</span>
</div>`).join("")
: `<div class="liste-leer">Noch niemand.</div>`}
</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Steht noch aus</span>
<span class="feld-hinweis">${d.offen.length}</span>
</div>
<div class="liste">
${d.offen.length ? d.offen.map((o) => `
<div class="liste-zeile">
<span class="db-nummer">${sicher(o.personalnummer || "—")}</span>
<div class="lz-haupt"><div class="lz-titel">${sicher(o.name)}</div></div>
</div>`).join("")
: `<div class="liste-leer">Alle haben bestätigt.</div>`}
</div>
</div>
</div>` : ""}
`,
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 ? `
<div class="warnung warn">
<span class="w-symbol">✋</span>
<div><b>Titel und Text sind gesperrt.</b> Es haben bereits Personen diesen
Wortlaut zur Kenntnis genommen. Für eine inhaltliche Änderung den Aushang
abnehmen und neu veröffentlichen.</div>
</div>` : ""}
<div class="feld"><label>Titel</label>
<input name="titel" maxlength="160" ${!neu && a.textGesperrt ? "disabled" : ""}
value="${neu ? "" : sicher(a.titel)}"></div>
<div class="feld-reihe">
<div class="feld"><label>Kategorie</label>
<select name="kategorie">
${d.kategorien.map((k) =>
`<option value="${sicher(k.id)}"
${!neu && k.id === a.kategorie ? "selected" : ""}>
${sicher(k.name)}</option>`).join("")}
</select></div>
<div class="feld"><label>Gilt für</label>
<select name="station">
<option value="">gesamte Klinik</option>
${d.stationen.map((s) =>
`<option value="${sicher(s.id)}"
${!neu && s.id === a.station ? "selected" : ""}>
${sicher(s.name)}</option>`).join("")}
</select></div>
</div>
<div class="feld"><label>Text</label>
<textarea name="text" rows="7" maxlength="8000"
${!neu && a.textGesperrt ? "disabled" : ""}
placeholder="Wortlaut des Aushangs…">${neu ? "" : sicher(a.text)}</textarea></div>
<div class="feld"><label>
<input type="checkbox" class="haken" name="angeheftet"
${!neu && a.angeheftet ? "checked" : ""}>
Oben anheften</label></div>
<div class="feld"><label>
<input type="checkbox" class="haken" name="kenntnispflicht"
${!neu && a.kenntnispflicht ? "checked" : ""}>
Kenntnisnahme erforderlich</label>
<div class="feld-hinweis">Das System erinnert die Betroffenen. Bestätigen kann
jeder nur für sich — erzwungen wird nichts.</div></div>
<div class="feld"><label>Frist (freiwillig)</label>
<input name="frist" type="datetime-local"
value="${neu || !a.frist ? "" : sicher(String(a.frist).replace(" ", "T").slice(0, 16))}">
<div class="feld-hinweis">Läuft die Frist ab, geschieht nichts weiter —
es wird nur deutlicher erinnert.</div></div>`,
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: `
<p>„<b>${sicher(a.titel)}</b>" wird vom Brett genommen und wandert ins Archiv.
Gelöscht wird nichts — der Wortlaut und alle Kenntnisnahmen bleiben.</p>
${a.kenntnispflicht ? `
<div class="warnung warn">
<span class="w-symbol">ℹ</span>
<div>Offene Kenntnisnahmen halten das Abnehmen nicht auf. Wie viele noch
ausstanden, wird im Protokoll festgehalten.</div>
</div>` : ""}
<div class="feld"><label>Grund (Pflicht)</label>
<input name="grund" maxlength="255"
placeholder="Erledigt, ersetzt, überholt…"></div>`,
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 = `<div class="seiten-kopf"><div><h2>Nachrichten und Aushänge</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("kommunikation", {
rendern,
aktualisieren() {
if (App.zustand.aktivesModul === "kommunikation"
&& !document.querySelector(".fenster-hg")) {
rendern(App.$("inhalt"));
}
},
});
})();