- 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>
742 lines
29 KiB
JavaScript
742 lines
29 KiB
JavaScript
/* =====================================================================
|
||
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"));
|
||
}
|
||
},
|
||
});
|
||
})();
|