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

756 lines
31 KiB
JavaScript
Raw Permalink 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: Blutbank
Ein Lagerbuch. Die Verträglichkeitstabelle ist eine Nachschlagehilfe
und keine Schranke: Weicht eine Kombination ab, wird das deutlich
angezeigt — ausgegeben werden kann sie trotzdem. Auch abgelaufene
Konserven verschwinden nicht von selbst.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen } = App;
const z = {
ordner: "vorraetig", // vorraetig | reserviert | archiv
gruppe: "", // Filter auf eine Blutgruppe
daten: null,
};
// Marker-Art je Status
const statusArt = {
vorraetig: "gut", reserviert: "warn",
ausgegeben: "info", verworfen: "neutral",
};
/* ---- Kleine Helfer ---------------------------------------------------- */
/** Anzeigename eines Status */
function statusName(status) {
const treffer = (z.daten.status || []).find((s) => s.id === status);
return treffer ? treffer.name : status;
}
/**
* Schlägt in der Verträglichkeitstabelle nach.
* @returns {boolean|null} null, wenn sich nichts sagen lässt
*/
function vertraeglich(empfaenger, konserve) {
if (!empfaenger) return null;
const erlaubt = (z.daten.vertraeglichkeit || {})[empfaenger];
if (!erlaubt) return null;
return erlaubt.includes(konserve);
}
/** Kurzer Hinweistext zum Verfallsdatum */
function verfallText(k) {
if (k.tage === null || k.tage === undefined) return "ohne Verfallsdatum";
if (k.tage <= 0) return `seit ${Math.abs(k.tage)} Tagen über dem Datum`;
return `noch ${k.tage} Tage`;
}
/* ---- Gerüst ------------------------------------------------------------ */
function reiterleiste(d) {
const eintraege = [
{ id: "vorraetig", text: "Vorrätig", zahl: d.zaehler.vorraetig },
{ id: "reserviert", text: "Reserviert", zahl: d.zaehler.reserviert },
{ id: "archiv", text: "Archiv", zahl: d.zaehler.ausgegebenHeute },
];
return `<div class="reiter">
${eintraege.map((e) => `
<button class="${z.ordner === e.id ? "aktiv" : ""}" data-reiter="${e.id}">
${sicher(e.text)}<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.vorraetig === 0 ? "hoch" : "gut"}">
<div class="kz-titel">Vorrätig</div>
<div class="kz-wert">${zk.vorraetig}</div>
<div class="kz-zusatz">frei verfügbare Konserven</div>
</div>
<div class="kennzahl">
<div class="kz-titel">Reserviert</div>
<div class="kz-wert">${zk.reserviert}</div>
<div class="kz-zusatz">für Patienten zurückgelegt</div>
</div>
<div class="kennzahl ${zk.baldAb > 0 ? "warn" : ""}">
<div class="kz-titel">Läuft bald ab</div>
<div class="kz-wert">${zk.baldAb}</div>
<div class="kz-zusatz">innerhalb von ${d.vorlaufTage} Tagen</div>
</div>
<div class="kennzahl ${zk.abgelaufen > 0 ? "hoch" : ""}">
<div class="kz-titel">Über dem Datum</div>
<div class="kz-wert">${zk.abgelaufen}</div>
<div class="kz-zusatz">liegt weiter im Lager</div>
</div>
</div>`;
}
/** Bestandskacheln je Blutgruppe — dienen zugleich als Filter */
function bestandsKacheln(d) {
return `<div class="raster k4 blut-bestand">
${d.bestand.map((b) => `
<div class="kennzahl klickbar ${z.gruppe === b.gruppe ? "gewaehlt" : ""}
${b.vorraetig === 0 ? "hoch" : b.knapp ? "warn" : ""}"
data-gruppe="${sicher(b.gruppe)}">
<div class="kz-titel">Blutgruppe ${sicher(b.gruppe)}</div>
<div class="kz-wert">${b.vorraetig}</div>
<div class="kz-zusatz">
vorrätig · ${b.reserviert} reserviert
${b.baldAb > 0 ? ` · ${b.baldAb} läuft ab` : ""}
</div>
</div>`).join("")}
</div>`;
}
/* ---- Übersicht ----------------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Blutbank</h2>
<p>Das System verwirft keine Konserve und wählt keine aus. Ein abgelaufenes
Datum ist eine Erinnerung — ob eine Konserve noch brauchbar ist, beurteilt
ein Mensch.</p>
</div>
<div class="ls-kopf-aktionen">
<button class="knopf leise" data-tabelle="1">Verträglichkeit nachschlagen</button>
${d.rechte.verwalten
? `<button class="knopf" data-einlagern="1">+ Konserven einlagern</button>` : ""}
</div>
</div>
${kennzahlen(d)}
${bestandsKacheln(d)}
${reiterleiste(d)}
<div class="filterleiste">
<select data-gruppenfilter="1">
<option value="">Alle Blutgruppen</option>
${d.gruppen.map((g) =>
`<option value="${sicher(g)}" ${g === z.gruppe ? "selected" : ""}>
${sicher(g)}</option>`).join("")}
</select>
<span class="feld-hinweis">${z.ordner === "vorraetig"
? "Die älteste Konserve steht oben — welche genommen wird, entscheidest du."
: "Klick auf eine Zeile öffnet die Konserve."}</span>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">${z.ordner === "vorraetig" ? "Bestand"
: z.ordner === "reserviert" ? "Zurückgelegte Konserven"
: "Ausgegeben und verworfen"}</span>
<span class="feld-hinweis">${d.liste.length} Einträge</span>
</div>
<div class="liste">
${d.liste.length ? d.liste.map(zeile).join("")
: `<div class="liste-leer">Keine Konserve in diesem Ordner.</div>`}
</div>
</div>
`;
verdrahteUebersicht(behaelter);
}
function zeile(k) {
return `<div class="liste-zeile" data-oeffne="${k.id}">
<span class="db-nummer">${sicher(k.konserve)}</span>
<span class="blut-gruppe">${sicher(k.blutgruppe)}</span>
<div class="lz-haupt">
<div class="lz-titel">${k.menge_ml} ml
${k.lagerort ? `<span class="pat-geburt">${sicher(k.lagerort)}</span>` : ""}</div>
<div class="lz-unter">
${k.empfaenger ? "für " + sicher(k.empfaenger)
: "eingelagert " + zeit(k.eingelagert_am)}
· ${sicher(verfallText(k))}
</div>
</div>
${k.vertraeglich === false
? `<span class="marker kritisch"
title="Weicht von der Verträglichkeitstabelle ab">Tabelle ✕</span>` : ""}
${k.abgelaufen ? `<span class="marker kritisch">abgelaufen</span>`
: k.laeuftBaldAb ? `<span class="marker warn">läuft ab</span>` : ""}
<span class="marker ${statusArt[k.status] || "neutral"}">
${sicher(statusName(k.status))}</span>
<span class="db-zeit">${k.status === "ausgegeben" ? zeit(k.ausgegeben_am)
: k.verfaellt_am ? sicher(String(k.verfaellt_am).slice(0, 10)) : "—"}</span>
</div>`;
}
function verdrahteUebersicht(behaelter) {
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
el.addEventListener("click", () => {
z.ordner = el.dataset.reiter;
rendern(behaelter);
});
});
behaelter.querySelectorAll("[data-gruppe]").forEach((el) => {
el.addEventListener("click", () => {
// Ein zweiter Klick hebt den Filter wieder auf
z.gruppe = z.gruppe === el.dataset.gruppe ? "" : el.dataset.gruppe;
rendern(behaelter);
});
});
behaelter.querySelector("[data-gruppenfilter]").addEventListener("change", (e) => {
z.gruppe = e.target.value;
rendern(behaelter);
});
behaelter.querySelectorAll("[data-oeffne]").forEach((el) => {
el.addEventListener("click", () => konserveFenster(behaelter, +el.dataset.oeffne));
});
behaelter.querySelector("[data-tabelle]")
.addEventListener("click", () => tabellenFenster());
const einlagern = behaelter.querySelector("[data-einlagern]");
if (einlagern) einlagern.addEventListener("click", () => einlagernFenster(behaelter));
}
/* ---- Einzelne Konserve ------------------------------------------------------ */
async function konserveFenster(behaelter, konserveId) {
let d;
try {
d = await aufruf("blutbank", "konserve", { id: konserveId });
} catch (f) { return meldung(f.message, "fehler"); }
const k = d.konserve;
const imLager = k.status === "vorraetig" || k.status === "reserviert";
const dialog = fenster({
titel: `Konserve ${k.kennung} · ${k.blutgruppe}`,
inhalt: `
${k.vertraeglich === false ? `
<div class="warnung kritisch">
<span class="w-symbol">⚠</span>
<div><b>Weicht von der Verträglichkeitstabelle ab.</b>
Konserve ${sicher(k.blutgruppe)} für einen Empfänger mit
${sicher(k.empfaengerGruppe)}. Das System sperrt nichts —
die Entscheidung liegt bei dir.</div>
</div>` : ""}
${k.abgelaufen ? `
<div class="warnung warn">
<span class="w-symbol">📅</span>
<div><b>Über dem Verfallsdatum</b> — ${sicher(verfallText(k))}.
Die Konserve bleibt im Bestand, bis jemand sie verwirft.</div>
</div>` : ""}
<div class="werte-liste">
<div><span>Status</span><b>${sicher(statusName(k.status))}</b></div>
<div><span>Blutgruppe</span><b>${sicher(k.blutgruppe)}</b></div>
<div><span>Menge</span><b>${k.menge} ml</b></div>
<div><span>Lagerort</span><b>${sicher(k.lagerort || "—")}</b></div>
<div><span>Eingelagert</span><b>${zeit(k.eingelagertAm)}</b></div>
<div><span>Eingelagert von</span><b>${sicher(k.eingelagertVon)}</b></div>
<div><span>Verfällt am</span>
<b>${k.verfaelltAm ? sicher(String(k.verfaelltAm).slice(0, 10))
: "kein Datum"} · ${sicher(verfallText(k))}</b></div>
${k.empfaengerName ? `
<div><span>Zurückgelegt für</span>
<b>${sicher(k.empfaengerName)}
${k.empfaengerGruppe ? ` (${sicher(k.empfaengerGruppe)})`
: " (Blutgruppe nicht bestimmt)"}</b></div>` : ""}
${k.reserviertVon
? `<div><span>Reserviert von</span><b>${sicher(k.reserviertVon)}</b></div>` : ""}
${k.ausgegebenAm ? `
<div><span>Ausgegeben</span><b>${zeit(k.ausgegebenAm)}</b></div>
<div><span>Ausgegeben von</span><b>${sicher(k.ausgegebenVon)}</b></div>` : ""}
</div>
${d.rechte.verwalten && imLager ? `
<div class="feld-reihe" style="margin-top:14px">
${k.status === "vorraetig"
? `<button class="knopf klein" data-reservieren="1">Für Patienten zurücklegen</button>`
: `<button class="knopf leise klein" data-loesen="1">Reservierung aufheben</button>`}
<button class="knopf klein" data-ausgeben="1">Ausgeben</button>
<button class="knopf leise klein" data-umlagern="1">Umlagern</button>
<button class="knopf gefahr klein" data-verwerfen="1">Verwerfen</button>
</div>` : ""}
<div class="karte-kopf" style="margin-top:14px">
<span class="karte-titel">Weg der Konserve</span>
<span class="feld-hinweis">aus dem Protokoll, unveränderlich</span>
</div>
<div class="liste">
${d.spur.length ? d.spur.map((s) => `
<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher(s.aktion)}</div>
<div class="lz-unter">${sicher(s.autor)}
${s.grund ? " · " + sicher(s.grund) : ""}</div>
</div>
<span class="db-zeit">${zeit(s.am)}</span>
</div>`).join("")
: `<div class="liste-leer">Noch kein Eintrag.</div>`}
</div>
`,
nachOeffnen: (hg) => {
const knopf = (name, funktion) => {
const el = hg.querySelector(`[data-${name}]`);
if (el) el.addEventListener("click", () => { dialog.schliessen(); funktion(); });
};
knopf("reservieren", () => reservierenFenster(behaelter, k));
knopf("loesen", () => loesenFenster(behaelter, k));
knopf("ausgeben", () => ausgebenFenster(behaelter, k));
knopf("umlagern", () => umlagernFenster(behaelter, k));
knopf("verwerfen", () => verwerfenFenster(behaelter, k));
},
});
}
/* ---- Einlagern --------------------------------------------------------------- */
function einlagernFenster(behaelter) {
const d = z.daten;
bestaetigen({
titel: "Konserven einlagern",
inhalt: `
<div class="feld-reihe">
<div class="feld"><label>Blutgruppe</label>
<select name="blutgruppe">
${d.gruppen.map((g) =>
`<option value="${sicher(g)}">${sicher(g)}</option>`).join("")}
</select></div>
<div class="feld"><label>Anzahl</label>
<input name="anzahl" type="number" min="1" max="20" value="1"></div>
<div class="feld"><label>Menge je Konserve (ml)</label>
<input name="menge" type="number" min="50" max="1000" step="50" value="500"></div>
</div>
<div class="feld"><label>Lagerort</label>
<select name="lagerort">
${(d.lagerorte || []).map((o) =>
`<option value="${sicher(o)}">${sicher(o)}</option>`).join("")}
</select></div>
<div class="feld"><label>Verfällt am (freiwillig)</label>
<input name="verfaellt" type="date">
<div class="feld-hinweis">Ohne Datum erinnert später nichts an einen Ablauf.
Mit Datum erinnert das System — verwerfen muss trotzdem jemand von Hand.</div></div>
<div class="feld"><label>Vermerk (freiwillig)</label>
<input name="vermerk" maxlength="255" placeholder="Herkunft, Spendeaktion…"></div>`,
bestaetigenText: "Einlagern",
bestaetigt: async (el, zu) => {
try {
const antwort = await aufruf("blutbank", "einlagern", {
blutgruppe: el.querySelector("[name=blutgruppe]").value,
anzahl: +el.querySelector("[name=anzahl]").value,
menge: +el.querySelector("[name=menge]").value,
lagerort: el.querySelector("[name=lagerort]").value,
verfaellt: el.querySelector("[name=verfaellt]").value,
vermerk: el.querySelector("[name=vermerk]").value.trim(),
});
zu();
meldung(`${antwort.anzahl} Konserve(n) eingelagert.`, "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Reservieren ---------------------------------------------------------------- */
function reservierenFenster(behaelter, k) {
let gewaehlt = null;
bestaetigen({
titel: `${k.kennung} zurücklegen`,
inhalt: `
<p>Konserve <b>${sicher(k.blutgruppe)}</b>, ${k.menge} ml.</p>
<div class="feld"><label>Patient suchen</label>
<input name="suche" placeholder="Name oder Geburtsdatum…"></div>
<div class="liste pat-trefferliste" data-treffer="1">
<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>
</div>
<div data-pruefung="1"></div>
<div class="feld"><label>Vermerk (freiwillig)</label>
<input name="vermerk" maxlength="255"
placeholder="Geplante OP, Notfallvorhalt…"></div>`,
bestaetigenText: "Zurücklegen",
bestaetigt: async (el, zu) => {
if (!gewaehlt) return meldung("Bitte zuerst einen Patienten wählen.", "fehler");
try {
const antwort = await aufruf("blutbank", "reservieren", {
id: k.id,
patient: gewaehlt.id,
vermerk: el.querySelector("[name=vermerk]").value.trim(),
});
zu();
meldung(antwort.vertraeglich === false
? "Zurückgelegt — die Abweichung wurde gemeldet."
: "Konserve zurückgelegt.", antwort.vertraeglich === false ? "" : "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
nachOeffnen: (hg) => {
const suchfeld = hg.querySelector("[name=suche]");
const liste = hg.querySelector("[data-treffer]");
const pruefung = hg.querySelector("[data-pruefung]");
let warten = null;
/** Zeigt, was die Nachschlagetabelle zu dieser Kombination sagt */
function zeigePruefung(person) {
const passt = vertraeglich(person.blutgruppe, k.blutgruppe);
pruefung.innerHTML = passt === null
? `<div class="warnung warn"><span class="w-symbol">?</span>
<div><b>Blutgruppe nicht bestimmt.</b> Zur Verträglichkeit lässt sich
nichts nachschlagen — das ist kein Hindernis, aber es steht so da.</div></div>`
: passt
? `<div class="warnung"><span class="w-symbol">✔</span>
<div>Laut Tabelle verträglich: Konserve ${sicher(k.blutgruppe)} für
Empfänger ${sicher(person.blutgruppe)}.</div></div>`
: `<div class="warnung kritisch"><span class="w-symbol">⚠</span>
<div><b>Laut Tabelle nicht verträglich:</b> Konserve
${sicher(k.blutgruppe)} für Empfänger ${sicher(person.blutgruppe)}.
Das System hält dich nicht auf — es meldet die Abweichung.</div></div>`;
}
suchfeld.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(async () => {
const begriff = suchfeld.value.trim();
if (begriff.length < 2) {
liste.innerHTML = `<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>`;
return;
}
try {
const antwort = await aufruf("blutbank", "patientSuche", { begriff });
const treffer = antwort.treffer || [];
liste.innerHTML = treffer.length ? treffer.map((t) => `
<div class="liste-zeile" data-person="${t.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(t.firstname)} ${sicher(t.lastname)}</div>
<div class="lz-unter">geb. ${sicher(t.dob)}</div>
</div>
${t.hinweis ? `<span class="marker kritisch">⚠</span>` : ""}
<span class="marker ${t.blutgruppe
? (vertraeglich(t.blutgruppe, k.blutgruppe) ? "gut" : "kritisch")
: "neutral"}">
${sicher(t.blutgruppe || "nicht bestimmt")}</span>
${t.laufend ? `<span class="marker info">${sicher(t.laufend)}</span>` : ""}
</div>`).join("")
: `<div class="liste-leer">Keine Person gefunden.</div>`;
liste.querySelectorAll("[data-person]").forEach((el) => {
el.addEventListener("click", () => {
gewaehlt = treffer.find((t) => t.id === +el.dataset.person);
liste.querySelectorAll("[data-person]")
.forEach((zeile) => zeile.classList.remove("gewaehlt"));
el.classList.add("gewaehlt");
zeigePruefung(gewaehlt);
});
});
} catch (f) { meldung(f.message, "fehler"); }
}, 260);
});
},
});
}
function loesenFenster(behaelter, k) {
bestaetigen({
titel: "Reservierung aufheben",
inhalt: `
<p>Konserve <b>${sicher(k.kennung)}</b> war für
${sicher(k.empfaengerName || "einen Patienten")} zurückgelegt und geht
zurück in den freien Bestand.</p>
<div class="feld"><label>Grund (Pflicht)</label>
<input name="grund" maxlength="255"
placeholder="OP verschoben, nicht mehr benötigt…"></div>`,
bestaetigenText: "Reservierung aufheben",
bestaetigt: async (el, zu) => {
const grund = el.querySelector("[name=grund]").value.trim();
if (!grund) return meldung("Bitte einen Grund angeben.", "fehler");
try {
await aufruf("blutbank", "reservierungLoesen", { id: k.id, grund });
zu();
meldung("Reservierung aufgehoben.", "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Ausgeben --------------------------------------------------------------------- */
function ausgebenFenster(behaelter, k) {
// Bei einer reservierten Konserve steht der Empfänger schon fest
let gewaehlt = k.empfaenger ? { id: k.empfaenger, blutgruppe: k.empfaengerGruppe } : null;
const passtSchon = k.empfaenger ? vertraeglich(k.empfaengerGruppe, k.blutgruppe) : null;
bestaetigen({
titel: `${k.kennung} ausgeben`,
gefahr: true,
inhalt: `
<p>Konserve <b>${sicher(k.blutgruppe)}</b>, ${k.menge} ml verlässt das Lager.</p>
<div class="warnung warn">
<span class="w-symbol">ℹ</span>
<div><b>Das System transfundiert nicht.</b> Es hält fest, dass die Konserve
herausgegeben wurde. Alles Weitere verantwortet der Arzt am Patienten.</div>
</div>
${k.abgelaufen ? `
<div class="warnung kritisch">
<span class="w-symbol">📅</span>
<div><b>Diese Konserve ist über dem Verfallsdatum</b> —
${sicher(verfallText(k))}. Die Ausgabe ist möglich und wird so vermerkt.</div>
</div>` : ""}
${k.empfaenger ? `
<div class="feld"><label>Empfänger</label>
<div class="beh-text">${sicher(k.empfaengerName)} ·
${sicher(k.empfaengerGruppe || "Blutgruppe nicht bestimmt")}</div></div>
${passtSchon === false ? `
<div class="warnung kritisch">
<span class="w-symbol">⚠</span>
<div><b>Laut Tabelle nicht verträglich.</b> Die Ausgabe wird trotzdem
dokumentiert und gemeldet.</div>
</div>` : ""}`
: `
<div class="feld"><label>Empfänger suchen (Pflicht)</label>
<input name="suche" placeholder="Name oder Geburtsdatum…"></div>
<div class="liste pat-trefferliste" data-treffer="1">
<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>
</div>
<div data-pruefung="1"></div>`}
<div class="feld"><label>Vermerk zur Ausgabe (Pflicht)</label>
<input name="vermerk" maxlength="255"
placeholder="An wen übergeben, wofür?"></div>`,
bestaetigenText: "Ausgabe dokumentieren",
bestaetigt: async (el, zu) => {
if (!gewaehlt) return meldung("Bitte einen Empfänger wählen.", "fehler");
const vermerk = el.querySelector("[name=vermerk]").value.trim();
if (!vermerk) return meldung("Bitte einen Vermerk hinterlassen.", "fehler");
try {
const antwort = await aufruf("blutbank", "ausgeben", {
id: k.id, patient: gewaehlt.id, vermerk,
});
zu();
meldung(antwort.vertraeglich === false
? "Ausgegeben — die Abweichung wurde gemeldet."
: "Ausgabe dokumentiert.", antwort.vertraeglich === false ? "" : "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
nachOeffnen: (hg) => {
const suchfeld = hg.querySelector("[name=suche]");
if (!suchfeld) return;
const liste = hg.querySelector("[data-treffer]");
const pruefung = hg.querySelector("[data-pruefung]");
let warten = null;
suchfeld.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(async () => {
const begriff = suchfeld.value.trim();
if (begriff.length < 2) return;
try {
const antwort = await aufruf("blutbank", "patientSuche", { begriff });
const treffer = antwort.treffer || [];
liste.innerHTML = treffer.length ? treffer.map((t) => `
<div class="liste-zeile" data-person="${t.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(t.firstname)} ${sicher(t.lastname)}</div>
<div class="lz-unter">geb. ${sicher(t.dob)}</div>
</div>
<span class="marker ${t.blutgruppe
? (vertraeglich(t.blutgruppe, k.blutgruppe) ? "gut" : "kritisch")
: "neutral"}">
${sicher(t.blutgruppe || "nicht bestimmt")}</span>
</div>`).join("")
: `<div class="liste-leer">Keine Person gefunden.</div>`;
liste.querySelectorAll("[data-person]").forEach((el) => {
el.addEventListener("click", () => {
gewaehlt = treffer.find((t) => t.id === +el.dataset.person);
liste.querySelectorAll("[data-person]")
.forEach((zeile) => zeile.classList.remove("gewaehlt"));
el.classList.add("gewaehlt");
const passt = vertraeglich(gewaehlt.blutgruppe, k.blutgruppe);
pruefung.innerHTML = passt === false
? `<div class="warnung kritisch"><span class="w-symbol">⚠</span>
<div><b>Laut Tabelle nicht verträglich.</b> Die Ausgabe bleibt
möglich und wird gemeldet.</div></div>`
: passt === null
? `<div class="warnung warn"><span class="w-symbol">?</span>
<div>Blutgruppe nicht bestimmt — nichts nachzuschlagen.</div></div>`
: "";
});
});
} catch (f) { meldung(f.message, "fehler"); }
}, 260);
});
},
});
}
/* ---- Verwerfen ---------------------------------------------------------------------- */
function verwerfenFenster(behaelter, k) {
bestaetigen({
titel: `${k.kennung} verwerfen`,
gefahr: true,
inhalt: `
<p>Konserve <b>${sicher(k.blutgruppe)}</b>, ${k.menge} ml wird als verworfen
festgehalten und verschwindet aus dem Bestand.</p>
${k.empfaengerName ? `
<div class="warnung warn">
<span class="w-symbol">🧍</span>
<div>Diese Konserve war für <b>${sicher(k.empfaengerName)}</b> zurückgelegt.
Das wird allen im Dienst gemeldet.</div>
</div>` : ""}
<div class="feld"><label>Begründung (Pflicht)</label>
<input name="grund" maxlength="255"
placeholder="Kühlkette unterbrochen, Beutel beschädigt, abgelaufen…"></div>
<div class="feld-hinweis">Das Verfallsdatum allein verwirft nichts. Ob eine
Konserve noch brauchbar ist, beurteilst du.</div>`,
bestaetigenText: "Verwerfen",
bestaetigt: async (el, zu) => {
const grund = el.querySelector("[name=grund]").value.trim();
if (grund.length < 3) return meldung("Bitte eine Begründung angeben.", "fehler");
try {
await aufruf("blutbank", "verwerfen", { id: k.id, grund });
zu();
meldung("Konserve verworfen.", "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Umlagern ------------------------------------------------------------------------ */
function umlagernFenster(behaelter, k) {
bestaetigen({
titel: `${k.kennung} umlagern`,
inhalt: `
<p>Aktueller Lagerort: <b>${sicher(k.lagerort || "—")}</b></p>
<div class="feld"><label>Neuer Lagerort</label>
<select name="lagerort">
${(z.daten.lagerorte || []).map((o) =>
`<option value="${sicher(o)}" ${o === k.lagerort ? "selected" : ""}>
${sicher(o)}</option>`).join("")}
</select></div>
<div class="feld"><label>Vermerk (freiwillig)</label>
<input name="vermerk" maxlength="255"></div>`,
bestaetigenText: "Umlagern",
bestaetigt: async (el, zu) => {
try {
await aufruf("blutbank", "umlagern", {
id: k.id,
lagerort: el.querySelector("[name=lagerort]").value,
vermerk: el.querySelector("[name=vermerk]").value.trim(),
});
zu();
meldung("Lagerort geändert.", "gut");
rendern(behaelter);
} catch (f) { meldung(f.message, "fehler"); }
},
});
}
/* ---- Verträglichkeitstabelle ------------------------------------------------------------ */
function tabellenFenster() {
const d = z.daten;
fenster({
titel: "Verträglichkeit nachschlagen",
inhalt: `
<div class="warnung">
<span class="w-symbol">ℹ</span>
<div><b>Eine Nachschlagehilfe, keine Schranke.</b> Das System sperrt keine
Konserve und wählt keine aus. Weicht eine Kombination ab, wird das
angezeigt und protokolliert — entschieden wird am Patienten.</div>
</div>
<div class="liste">
${d.gruppen.map((empfaenger) => {
const erlaubt = (d.vertraeglichkeit || {})[empfaenger] || [];
return `<div class="liste-zeile">
<span class="blut-gruppe">${sicher(empfaenger)}</span>
<div class="lz-haupt">
<div class="lz-unter">verträgt laut Tabelle</div>
<div class="blut-reihe">
${d.gruppen.map((spender) => `
<span class="blut-zelle ${erlaubt.includes(spender) ? "ja" : ""}">
${sicher(spender)}</span>`).join("")}
</div>
</div>
<span class="marker neutral">${erlaubt.length} von ${d.gruppen.length}</span>
</div>`;
}).join("")}
</div>`,
});
}
/* ---- Laden -------------------------------------------------------------------------- */
async function rendern(behaelter) {
try {
z.daten = await aufruf("blutbank", "uebersicht", {
ordner: z.ordner, gruppe: z.gruppe,
});
uebersicht(behaelter);
// Sprungziel aus einem Hinweis, z. B. „Blutkonserve läuft ab"
const ziel = App.zustand.modulZiel;
if (ziel && ziel.blut) {
App.zustand.modulZiel = null;
return konserveFenster(behaelter, ziel.blut);
}
} catch (f) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Blutbank</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("blutbank", {
rendern,
aktualisieren() {
// Kein offenes Fenster überschreiben
if (App.zustand.aktivesModul === "blutbank" && !document.querySelector(".fenster-hg")) {
rendern(App.$("inhalt"));
}
},
});
})();