- 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>
273 lines
9.9 KiB
JavaScript
273 lines
9.9 KiB
JavaScript
/* =====================================================================
|
||
aether-medical / Ansicht: Bausteine
|
||
|
||
Pflege der Textvorlagen für Behandlungs- und Berichtsbogen. Ein
|
||
Baustein ist vorbereiteter Text — er wird nie von selbst eingesetzt
|
||
und schreibt nichts in eine Dokumentation.
|
||
===================================================================== */
|
||
"use strict";
|
||
|
||
(() => {
|
||
const { sicher, aufruf, meldung, fenster, bestaetigen } = App;
|
||
|
||
const z = {
|
||
bereich: "behandlung", // gewählter Bogen
|
||
daten: null,
|
||
};
|
||
|
||
/* ---- Gerüst ------------------------------------------------------------ */
|
||
|
||
function reiterleiste(d) {
|
||
return `<div class="reiter">
|
||
${d.bereiche.map((b) => `
|
||
<button class="${z.bereich === b.id ? "aktiv" : ""}" data-reiter="${sicher(b.id)}">
|
||
${sicher(b.name)}<span class="mdt-zahl">${b.anzahl}</span>
|
||
</button>`).join("")}
|
||
</div>`;
|
||
}
|
||
|
||
/* ---- Übersicht ---------------------------------------------------------- */
|
||
|
||
function uebersicht(behaelter) {
|
||
const d = z.daten;
|
||
|
||
behaelter.innerHTML = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Bausteine</h2>
|
||
<p>Vorformulierter Text für die Bögen. Ein Baustein füllt beim Einsetzen nur
|
||
das Formular — gespeichert wird danach von Hand, und vorhandener Text
|
||
bleibt stehen.</p>
|
||
</div>
|
||
<button class="knopf" data-neu="1">+ Baustein anlegen</button>
|
||
</div>
|
||
|
||
${reiterleiste(d)}
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Bausteine für diesen Bogen</span>
|
||
<span class="feld-hinweis">Die Reihenfolge bestimmt nur die Anzeige</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.liste.length ? d.liste.map(zeile).join("")
|
||
: `<div class="liste-leer">Für diesen Bogen ist noch nichts hinterlegt.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte" style="margin-top:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Felder dieses Bogens</span>
|
||
</div>
|
||
<div class="fz-ausstattung">
|
||
${d.felder.map((f) => `<span class="fz-teil">${sicher(f.name)}</span>`).join("")}
|
||
</div>
|
||
<div class="feld-hinweis" style="margin-top:8px">
|
||
Ein Baustein kann nur füllen, was es im Bogen auch gibt. Felder, für die
|
||
jemandem das Recht fehlt, bleiben beim Einsetzen unberührt.
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
verdrahteUebersicht(behaelter);
|
||
}
|
||
|
||
function zeile(b) {
|
||
return `<div class="liste-zeile ${b.aktiv ? "" : "med-aus"}">
|
||
<span class="db-nummer">${sicher(b.kennung)}</span>
|
||
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.name)}</div>
|
||
<div class="lz-unter">${b.felder.length
|
||
? sicher(b.felder.join(" · ")) : "füllt kein Feld"}</div>
|
||
</div>
|
||
|
||
<span class="marker neutral">Reihenfolge ${b.sortierung}</span>
|
||
<span class="marker ${b.aktiv ? "gut" : "neutral"}">
|
||
${b.aktiv ? "einsetzbar" : "abgeschaltet"}</span>
|
||
|
||
<button class="knopf leise klein" data-ansehen="${b.id}">Ansehen</button>
|
||
<button class="knopf leise klein" data-bearbeiten="${b.id}">Bearbeiten</button>
|
||
<button class="knopf leise klein" data-aktiv="${b.id}"
|
||
data-ziel="${b.aktiv ? "0" : "1"}">
|
||
${b.aktiv ? "Abschalten" : "Einschalten"}</button>
|
||
<button class="knopf gefahr klein" data-entfernen="${b.id}">Entfernen</button>
|
||
</div>`;
|
||
}
|
||
|
||
function verdrahteUebersicht(behaelter) {
|
||
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
z.bereich = el.dataset.reiter;
|
||
rendern(behaelter);
|
||
});
|
||
});
|
||
|
||
const neu = behaelter.querySelector("[data-neu]");
|
||
if (neu) neu.addEventListener("click", () => bearbeitenFenster(behaelter, null));
|
||
|
||
behaelter.querySelectorAll("[data-ansehen]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
const b = z.daten.liste.find((e) => e.id === +el.dataset.ansehen);
|
||
if (b) ansehenFenster(b);
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-bearbeiten]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
const b = z.daten.liste.find((e) => e.id === +el.dataset.bearbeiten);
|
||
if (b) bearbeitenFenster(behaelter, b);
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-aktiv]").forEach((el) => {
|
||
el.addEventListener("click", async () => {
|
||
try {
|
||
await aufruf("vorlage", "aktiv", {
|
||
id: +el.dataset.aktiv, aktiv: el.dataset.ziel === "1",
|
||
});
|
||
meldung(el.dataset.ziel === "1" ? "Baustein einsetzbar." : "Baustein abgeschaltet.", "gut");
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
});
|
||
});
|
||
|
||
behaelter.querySelectorAll("[data-entfernen]").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
const b = z.daten.liste.find((e) => e.id === +el.dataset.entfernen);
|
||
if (b) entfernenFenster(behaelter, b);
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- Ansehen ------------------------------------------------------------- */
|
||
|
||
function ansehenFenster(b) {
|
||
const d = z.daten;
|
||
|
||
fenster({
|
||
titel: `${b.kennung} · ${b.name}`,
|
||
inhalt: `
|
||
<p class="feld-hinweis">So sieht der Text aus, der beim Einsetzen in die
|
||
Felder wandert. Vorhandener Text im Bogen bleibt erhalten.</p>
|
||
${d.felder.filter((f) => b.inhalt[f.id]).map((f) => `
|
||
<div class="bs-feld">
|
||
<label>${sicher(f.name)}</label>
|
||
<div class="beh-text">${sicher(b.inhalt[f.id]).replace(/\n/g, "<br>")}</div>
|
||
</div>`).join("") || `<div class="liste-leer">Dieser Baustein ist leer.</div>`}`,
|
||
});
|
||
}
|
||
|
||
/* ---- Anlegen und bearbeiten ------------------------------------------------ */
|
||
|
||
function bearbeitenFenster(behaelter, b) {
|
||
const d = z.daten;
|
||
const neu = !b;
|
||
|
||
bestaetigen({
|
||
titel: neu ? `Baustein für den ${d.bereiche.find((e) => e.id === z.bereich).name}`
|
||
: `„${b.name}" bearbeiten`,
|
||
inhalt: `
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Name</label>
|
||
<input name="name" maxlength="120" placeholder="z. B. Wundversorgung"
|
||
value="${neu ? "" : sicher(b.name)}"></div>
|
||
<div class="feld"><label>Reihenfolge</label>
|
||
<input name="sortierung" type="number" min="0" max="999"
|
||
value="${neu ? 0 : b.sortierung}"></div>
|
||
</div>
|
||
|
||
<div class="feld-hinweis">Leer gelassene Felder bleiben beim Einsetzen
|
||
unberührt. Ein Baustein muss mindestens ein Feld füllen.</div>
|
||
|
||
${d.felder.map((f) => `
|
||
<div class="feld"><label>${sicher(f.name)}</label>
|
||
<textarea name="${sicher(f.id)}" rows="4" maxlength="4000"
|
||
placeholder="Gerüst mit Doppelpunkten, das jemand ausfüllt…">${
|
||
neu ? "" : sicher(b.inhalt[f.id] || "")}</textarea></div>`).join("")}
|
||
|
||
${neu ? "" : `<div class="feld-hinweis">Der Bogen eines Bausteins lässt sich
|
||
nicht wechseln — die Felder des einen passen nicht auf den anderen.</div>`}`,
|
||
bestaetigenText: neu ? "Anlegen" : "Speichern",
|
||
bestaetigt: async (el, zu) => {
|
||
const name = el.querySelector("[name=name]").value.trim();
|
||
if (!name) return meldung("Bitte einen Namen angeben.", "fehler");
|
||
|
||
// Nur gefüllte Felder wandern in den Baustein
|
||
const inhalt = {};
|
||
d.felder.forEach((f) => {
|
||
const wert = el.querySelector(`[name=${f.id}]`).value.trim();
|
||
if (wert) inhalt[f.id] = wert;
|
||
});
|
||
|
||
if (!Object.keys(inhalt).length) {
|
||
return meldung("Bitte mindestens ein Feld ausfüllen.", "fehler");
|
||
}
|
||
|
||
try {
|
||
await aufruf("vorlage", "speichern", {
|
||
id: neu ? null : b.id,
|
||
bereich: z.bereich,
|
||
name,
|
||
sortierung: +el.querySelector("[name=sortierung]").value,
|
||
inhalt,
|
||
});
|
||
zu();
|
||
meldung(neu ? "Baustein angelegt." : "Baustein gespeichert.", "gut");
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
}
|
||
|
||
function entfernenFenster(behaelter, b) {
|
||
bestaetigen({
|
||
titel: `„${b.name}" entfernen`,
|
||
gefahr: true,
|
||
inhalt: `
|
||
<p>Der Baustein verschwindet aus der Auswahl.</p>
|
||
<div class="warnung">
|
||
<span class="w-symbol">ℹ</span>
|
||
<div>Was damit bereits geschrieben wurde, steht in den Bögen und bleibt
|
||
unberührt. Soll der Baustein nur vorübergehend verschwinden, ist
|
||
<b>Abschalten</b> der sanftere Weg.</div>
|
||
</div>
|
||
<div class="feld"><label>Grund (Pflicht)</label>
|
||
<input name="grund" maxlength="255"
|
||
placeholder="Überholt, ersetzt, fehlerhaft…"></div>`,
|
||
bestaetigenText: "Entfernen",
|
||
bestaetigt: async (el, zu) => {
|
||
const grund = el.querySelector("[name=grund]").value.trim();
|
||
if (!grund) return meldung("Bitte einen Grund angeben.", "fehler");
|
||
try {
|
||
await aufruf("vorlage", "entfernen", { id: b.id, grund });
|
||
zu();
|
||
meldung("Baustein entfernt.", "gut");
|
||
rendern(behaelter);
|
||
} catch (f) { meldung(f.message, "fehler"); }
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ---- Laden -------------------------------------------------------------------- */
|
||
|
||
async function rendern(behaelter) {
|
||
try {
|
||
z.daten = await aufruf("vorlage", "uebersicht", { bereich: z.bereich });
|
||
uebersicht(behaelter);
|
||
} catch (f) {
|
||
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Bausteine</h2>
|
||
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
App.registriereAnsicht("vorlage", {
|
||
rendern,
|
||
aktualisieren() {
|
||
if (App.zustand.aktivesModul === "vorlage" && !document.querySelector(".fenster-hg")) {
|
||
rendern(App.$("inhalt"));
|
||
}
|
||
},
|
||
});
|
||
})();
|