/* =====================================================================
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) => `
${sicher(e.text)}${e.zahl}
`).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.
Verträglichkeit nachschlagen
${d.rechte.verwalten
? `+ Konserven einlagern ` : ""}
${kennzahlen(d)}
${bestandsKacheln(d)}
${reiterleiste(d)}
Alle Blutgruppen
${d.gruppen.map((g) =>
`
${sicher(g)} `).join("")}
${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"
? `Für Patienten zurücklegen `
: `Reservierung aufheben `}
Ausgeben
Umlagern
Verwerfen
` : ""}
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: `
Lagerort
${(d.lagerorte || []).map((o) =>
`${sicher(o)} `).join("")}
Vermerk (freiwillig)
`,
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.
Patient suchen
Mindestens zwei Zeichen eingeben.
Vermerk (freiwillig)
`,
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.
Grund (Pflicht)
`,
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 ? `
Empfänger
${sicher(k.empfaengerName)} ·
${sicher(k.empfaengerGruppe || "Blutgruppe nicht bestimmt")}
${passtSchon === false ? `
⚠
Laut Tabelle nicht verträglich. Die Ausgabe wird trotzdem
dokumentiert und gemeldet.
` : ""}`
: `
Empfänger suchen (Pflicht)
Mindestens zwei Zeichen eingeben.
`}
Vermerk zur Ausgabe (Pflicht)
`,
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.
` : ""}
Begründung (Pflicht)
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 || "—")}
Neuer Lagerort
${(z.daten.lagerorte || []).map((o) =>
`
${sicher(o)} `).join("")}
Vermerk (freiwillig)
`,
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"));
}
},
});
})();