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