/* ===================================================================== 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 `
${eintraege.map((e) => ` `).join("")}
`; } function kennzahlen(d) { const zk = d.zaehler; return `
Vorrätig
${zk.vorraetig}
frei verfügbare Konserven
Reserviert
${zk.reserviert}
für Patienten zurückgelegt
Läuft bald ab
${zk.baldAb}
innerhalb von ${d.vorlaufTage} Tagen
Über dem Datum
${zk.abgelaufen}
liegt weiter im Lager
`; } /** Bestandskacheln je Blutgruppe — dienen zugleich als Filter */ function bestandsKacheln(d) { return `
${d.bestand.map((b) => `
Blutgruppe ${sicher(b.gruppe)}
${b.vorraetig}
vorrätig · ${b.reserviert} reserviert ${b.baldAb > 0 ? ` · ${b.baldAb} läuft ab` : ""}
`).join("")}
`; } /* ---- Übersicht ----------------------------------------------------------- */ function uebersicht(behaelter) { const d = z.daten; behaelter.innerHTML = `

Blutbank

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.

${d.rechte.verwalten ? `` : ""}
${kennzahlen(d)} ${bestandsKacheln(d)} ${reiterleiste(d)}
${z.ordner === "vorraetig" ? "Die älteste Konserve steht oben — welche genommen wird, entscheidest du." : "Klick auf eine Zeile öffnet die Konserve."}
${z.ordner === "vorraetig" ? "Bestand" : z.ordner === "reserviert" ? "Zurückgelegte Konserven" : "Ausgegeben und verworfen"} ${d.liste.length} Einträge
${d.liste.length ? d.liste.map(zeile).join("") : `
Keine Konserve in diesem Ordner.
`}
`; verdrahteUebersicht(behaelter); } function zeile(k) { return `
${sicher(k.konserve)} ${sicher(k.blutgruppe)}
${k.menge_ml} ml ${k.lagerort ? `${sicher(k.lagerort)}` : ""}
${k.empfaenger ? "für " + sicher(k.empfaenger) : "eingelagert " + zeit(k.eingelagert_am)} · ${sicher(verfallText(k))}
${k.vertraeglich === false ? `Tabelle ✕` : ""} ${k.abgelaufen ? `abgelaufen` : k.laeuftBaldAb ? `läuft ab` : ""} ${sicher(statusName(k.status))} ${k.status === "ausgegeben" ? zeit(k.ausgegeben_am) : k.verfaellt_am ? sicher(String(k.verfaellt_am).slice(0, 10)) : "—"}
`; } 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 ? `
⚠
Weicht von der Verträglichkeitstabelle ab. Konserve ${sicher(k.blutgruppe)} für einen Empfänger mit ${sicher(k.empfaengerGruppe)}. Das System sperrt nichts — die Entscheidung liegt bei dir.
` : ""} ${k.abgelaufen ? `
📅
Über dem Verfallsdatum — ${sicher(verfallText(k))}. Die Konserve bleibt im Bestand, bis jemand sie verwirft.
` : ""}
Status${sicher(statusName(k.status))}
Blutgruppe${sicher(k.blutgruppe)}
Menge${k.menge} ml
Lagerort${sicher(k.lagerort || "—")}
Eingelagert${zeit(k.eingelagertAm)}
Eingelagert von${sicher(k.eingelagertVon)}
Verfällt am ${k.verfaelltAm ? sicher(String(k.verfaelltAm).slice(0, 10)) : "kein Datum"} · ${sicher(verfallText(k))}
${k.empfaengerName ? `
Zurückgelegt für ${sicher(k.empfaengerName)} ${k.empfaengerGruppe ? ` (${sicher(k.empfaengerGruppe)})` : " (Blutgruppe nicht bestimmt)"}
` : ""} ${k.reserviertVon ? `
Reserviert von${sicher(k.reserviertVon)}
` : ""} ${k.ausgegebenAm ? `
Ausgegeben${zeit(k.ausgegebenAm)}
Ausgegeben von${sicher(k.ausgegebenVon)}
` : ""}
${d.rechte.verwalten && imLager ? `
${k.status === "vorraetig" ? `` : ``}
` : ""}
Weg der Konserve aus dem Protokoll, unveränderlich
${d.spur.length ? d.spur.map((s) => `
${sicher(s.aktion)}
${sicher(s.autor)} ${s.grund ? " · " + sicher(s.grund) : ""}
${zeit(s.am)}
`).join("") : `
Noch kein Eintrag.
`}
`, 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: `
Ohne Datum erinnert später nichts an einen Ablauf. Mit Datum erinnert das System — verwerfen muss trotzdem jemand von Hand.
`, 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: `

Konserve ${sicher(k.blutgruppe)}, ${k.menge} ml.

Mindestens zwei Zeichen eingeben.
`, 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 ? `
?
Blutgruppe nicht bestimmt. Zur Verträglichkeit lässt sich nichts nachschlagen — das ist kein Hindernis, aber es steht so da.
` : passt ? `
✔
Laut Tabelle verträglich: Konserve ${sicher(k.blutgruppe)} für Empfänger ${sicher(person.blutgruppe)}.
` : `
⚠
Laut Tabelle nicht verträglich: Konserve ${sicher(k.blutgruppe)} für Empfänger ${sicher(person.blutgruppe)}. Das System hält dich nicht auf — es meldet die Abweichung.
`; } suchfeld.addEventListener("input", () => { clearTimeout(warten); warten = setTimeout(async () => { const begriff = suchfeld.value.trim(); if (begriff.length < 2) { liste.innerHTML = `
Mindestens zwei Zeichen eingeben.
`; return; } try { const antwort = await aufruf("blutbank", "patientSuche", { begriff }); const treffer = antwort.treffer || []; liste.innerHTML = treffer.length ? treffer.map((t) => `
${sicher(t.firstname)} ${sicher(t.lastname)}
geb. ${sicher(t.dob)}
${t.hinweis ? `⚠` : ""} ${sicher(t.blutgruppe || "nicht bestimmt")} ${t.laufend ? `${sicher(t.laufend)}` : ""}
`).join("") : `
Keine Person gefunden.
`; 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: `

Konserve ${sicher(k.kennung)} war für ${sicher(k.empfaengerName || "einen Patienten")} zurückgelegt und geht zurück in den freien Bestand.

`, 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: `

Konserve ${sicher(k.blutgruppe)}, ${k.menge} ml verlässt das Lager.

ℹ
Das System transfundiert nicht. Es hält fest, dass die Konserve herausgegeben wurde. Alles Weitere verantwortet der Arzt am Patienten.
${k.abgelaufen ? `
📅
Diese Konserve ist über dem Verfallsdatum — ${sicher(verfallText(k))}. Die Ausgabe ist möglich und wird so vermerkt.
` : ""} ${k.empfaenger ? `
${sicher(k.empfaengerName)} · ${sicher(k.empfaengerGruppe || "Blutgruppe nicht bestimmt")}
${passtSchon === false ? `
⚠
Laut Tabelle nicht verträglich. Die Ausgabe wird trotzdem dokumentiert und gemeldet.
` : ""}` : `
Mindestens zwei Zeichen eingeben.
`}
`, 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) => `
${sicher(t.firstname)} ${sicher(t.lastname)}
geb. ${sicher(t.dob)}
${sicher(t.blutgruppe || "nicht bestimmt")}
`).join("") : `
Keine Person gefunden.
`; 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 ? `
⚠
Laut Tabelle nicht verträglich. Die Ausgabe bleibt möglich und wird gemeldet.
` : passt === null ? `
?
Blutgruppe nicht bestimmt — nichts nachzuschlagen.
` : ""; }); }); } catch (f) { meldung(f.message, "fehler"); } }, 260); }); }, }); } /* ---- Verwerfen ---------------------------------------------------------------------- */ function verwerfenFenster(behaelter, k) { bestaetigen({ titel: `${k.kennung} verwerfen`, gefahr: true, inhalt: `

Konserve ${sicher(k.blutgruppe)}, ${k.menge} ml wird als verworfen festgehalten und verschwindet aus dem Bestand.

${k.empfaengerName ? `
🧍
Diese Konserve war für ${sicher(k.empfaengerName)} zurückgelegt. Das wird allen im Dienst gemeldet.
` : ""}
Das Verfallsdatum allein verwirft nichts. Ob eine Konserve noch brauchbar ist, beurteilst du.
`, 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: `

Aktueller Lagerort: ${sicher(k.lagerort || "—")}

`, 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: `
ℹ
Eine Nachschlagehilfe, keine Schranke. Das System sperrt keine Konserve und wählt keine aus. Weicht eine Kombination ab, wird das angezeigt und protokolliert — entschieden wird am Patienten.
${d.gruppen.map((empfaenger) => { const erlaubt = (d.vertraeglichkeit || {})[empfaenger] || []; return `
${sicher(empfaenger)}
verträgt laut Tabelle
${d.gruppen.map((spender) => ` ${sicher(spender)}`).join("")}
${erlaubt.length} von ${d.gruppen.length}
`; }).join("")}
`, }); } /* ---- 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 = `

Blutbank

Konnte nicht geladen werden: ${sicher(f.message)}

`; } } App.registriereAnsicht("blutbank", { rendern, aktualisieren() { // Kein offenes Fenster überschreiben if (App.zustand.aktivesModul === "blutbank" && !document.querySelector(".fenster-hg")) { rendern(App.$("inhalt")); } }, }); })();