aether-framework/resources/[aether]/aether-medical/html/js/module/vorlage.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

273 lines
9.9 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: 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"));
}
},
});
})();