/* =====================================================================
aether-medical / Ansicht: Medikamentenverwaltung
Bestand, Buchungen und BTM-Buch. Das System zählt und erinnert —
nachbestellt, entnommen und entsorgt wird von Hand. Abgelaufene
Präparate werden auffällig markiert, aber nicht gesperrt.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen } = App;
const z = {
reiter: "bestand", // bestand | btm
filter: "",
kategorie: "",
suche: "",
stillgelegt: false,
daten: null,
offen: null,
btmMedikament: null,
// Aus der jeweils letzten Antwort uebernommen, damit Dialoge sie sicher haben
rechte: { bestand: false, btm: false, katalog: false },
kategorien: [],
};
/* ---- Übersicht ----------------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
// Nach Kategorie gruppieren
const gruppen = {};
d.liste.forEach((m) => { (gruppen[m.kategorie] = gruppen[m.kategorie] || []).push(m); });
behaelter.innerHTML = `
Medikamente
Bestandsführung. Das System gibt nichts aus, verordnet nichts und
bestellt nichts nach — es zählt, was gebucht wurde.
${d.rechte.katalog
? `
+ Medikament anlegen ` : ""}
${kachel("gesamt", "Im Katalog", d.zaehler.gesamt, "aktive Präparate", "")}
${kachel("knapp", "Unter Mindestbestand", d.zaehler.knapp,
"nachbestellen", d.zaehler.knapp > 0 ? "warn" : "")}
${kachel("leer", "Nicht vorrätig", d.zaehler.leer,
"Bestand null", d.zaehler.leer > 0 ? "hoch" : "")}
${kachel("verfall", "Läuft ab", d.zaehler.verfall,
"Verfallsdatum nahe", d.zaehler.verfall > 0 ? "warn" : "")}
${kachel("btm", "Betäubungsmittel", d.zaehler.btm, "gesondert geführt", "")}
Bestand
${d.rechte.btm
? `
BTM-Buch ` : ""}
Alle Kategorien
${d.kategorien.map((k) =>
`
${sicher(k.name)} `).join("")}
${z.filter
? `Filter „${sicher(z.filter)}" aufheben `
: ""}
${d.rechte.katalog
? `
${z.stillgelegt ? "Stillgelegte" : "Aktive"} ` : ""}
${Object.keys(gruppen).length
? Object.keys(gruppen).map((kategorie) => `
${sicher(gruppen[kategorie][0].kategorieName)}
${gruppen[kategorie].length} Präparate
${gruppen[kategorie].map(zeile).join("")}
`).join("")
: `Kein Medikament gefunden.
`}
`;
verdrahteUebersicht(behaelter);
}
function kachel(filter, titel, wert, zusatz, art) {
const aktiv = z.filter === filter;
return `
${sicher(titel)}
${wert}
${sicher(zusatz)}
`;
}
function zeile(m) {
// Der Bestandsbalken zeigt das Verhältnis zum Mindestbestand
const anteil = m.mindestbestand > 0
? Math.min(100, Math.round((m.bestand / (m.mindestbestand * 3)) * 100))
: (m.bestand > 0 ? 100 : 0);
const farbe = m.leer ? "var(--kritisch)" : m.knapp ? "var(--warn)" : "var(--gut)";
return `
${sicher(m.kennung)}
${sicher(m.name)}
${m.btm ? `BTM ` : ""}
${sicher(m.wirkstoff || "—")}
${m.dosierung_standard ? " · " + sicher(m.dosierung_standard) : ""}
${m.lagerort ? " · " + sicher(m.lagerort) : ""}
${m.abgelaufen
? `
abgelaufen `
: m.laeuftAb
? `
in ${m.tageBisVerfall} Tagen ` : ""}
${!m.aktiv ? `
stillgelegt ` : ""}
${m.bestand} ${sicher(m.einheit)}
`;
}
function verdrahteUebersicht(behaelter) {
behaelter.querySelectorAll("[data-reiter]").forEach((el) => {
el.addEventListener("click", () => {
z.reiter = el.dataset.reiter;
rendern(behaelter);
});
});
behaelter.querySelectorAll("[data-filter]").forEach((el) => {
el.addEventListener("click", () => {
const gewaehlt = el.dataset.filter;
// „gesamt" ist kein Filter, sondern hebt ihn auf
z.filter = (gewaehlt === "gesamt" || z.filter === gewaehlt) ? "" : gewaehlt;
rendern(behaelter);
});
});
const filterweg = behaelter.querySelector("[data-filterweg]");
if (filterweg) filterweg.addEventListener("click", () => {
z.filter = "";
rendern(behaelter);
});
behaelter.querySelector("[data-kategorie]").addEventListener("change", (e) => {
z.kategorie = e.target.value;
rendern(behaelter);
});
const stillgelegt = behaelter.querySelector("[data-stillgelegt]");
if (stillgelegt) stillgelegt.addEventListener("click", () => {
z.stillgelegt = !z.stillgelegt;
rendern(behaelter);
});
const suchfeld = behaelter.querySelector("[data-suche]");
let warten = null;
suchfeld.addEventListener("input", () => {
clearTimeout(warten);
warten = setTimeout(() => {
z.suche = suchfeld.value.trim();
rendern(behaelter);
}, 280);
});
behaelter.querySelectorAll("[data-oeffne]").forEach((el) => {
el.addEventListener("click", () => oeffne(behaelter, +el.dataset.oeffne));
});
const neu = behaelter.querySelector("[data-neu]");
if (neu) neu.addEventListener("click", () => katalogFenster(behaelter, null));
}
/* ---- Einzelansicht --------------------------------------------------------- */
function einzelAnsicht(behaelter, d) {
const m = d.medikament;
behaelter.innerHTML = `
${sicher(m.kennung)} · ${sicher(m.name)}
${sicher(m.wirkstoff || "ohne Wirkstoffangabe")}
${m.btm ? " · Betäubungsmittel " : ""}
${m.aktiv ? "" : " · stillgelegt"}
← Bestand
${m.abgelaufen ? `
⏳
Verfallsdatum überschritten — am ${zeit(m.verfaelltAm)}.
Das Präparat bleibt buchbar und
dokumentierbar. Ob es noch verwendbar ist, beurteilt die Apotheke —
das System sperrt nichts anhand eines Datums.
` : ""}
${m.bestand <= m.mindestbestand ? `
📦
${m.bestand <= 0 ? "Nicht vorrätig" : "Unter Mindestbestand"}
— ${m.bestand} von mindestens ${m.mindestbestand} ${sicher(m.einheit)}.
Nachbestellen muss jemand von Hand.
` : ""}
Bestand ${m.bestand} ${sicher(m.einheit)}
Mindestbestand ${m.mindestbestand} ${sicher(m.einheit)}
Standarddosierung
${sicher(m.dosierung || "keine hinterlegt")}
Lagerort
${sicher(m.lagerort || "nicht angegeben")}
Verfällt am
${m.verfaelltAm ? String(m.verfaelltAm).slice(0, 10) : "kein Datum"}
Hinweis
${sicher(m.hinweis || "keiner")}
${d.rechte.bestand && m.aktiv
? `Bestand buchen ` : ""}
${d.rechte.katalog
? `Stammdaten ` : ""}
${d.rechte.katalog
? `
${m.aktiv ? "Stilllegen" : "Freigeben"} ` : ""}
Buchungen
Bestand nach jeder Buchung
${d.buchungen.length ? d.buchungen.map((b) => `
${sicher(b.artName)}
${b.art === "korrektur" ? "auf" : b.art === "ausgabe"
|| b.art === "verfall" ? "−" : "+"} ${b.menge} ${sicher(m.einheit)}
Stand danach: ${b.bestand_danach}
${sicher(b.grund || "")}
${b.patientName ? " · " + sicher(b.patientName) : ""}
· ${sicher(b.autor)} · ${zeit(b.gebucht_am)}
`).join("")
: `
Noch keine Buchung.
`}
Dokumentierte Gaben
aus den Behandlungsbögen
${d.gaben.length ? d.gaben.map((g) => `
${sicher(g.behandlung)}
${sicher(g.patientName)}
${g.menge} ${sicher(m.einheit)}
${g.dosierung ? sicher(g.dosierung) + " · " : ""}
${sicher(g.autor)} · ${zeit(g.gegeben_am)}
${g.ohneBuchung
? `
ohne Buchung ` : ""}
`).join("")
: `
Noch keine Gabe dokumentiert.
`}
`;
verdrahteEinzel(behaelter, d);
}
function verdrahteEinzel(behaelter, d) {
const m = d.medikament;
behaelter.querySelector("[data-zurueck]").addEventListener("click", () => {
z.offen = null;
rendern(behaelter);
});
const buchen = behaelter.querySelector("[data-buchen]");
if (buchen) buchen.addEventListener("click", () => buchungsFenster(behaelter, d));
const bearbeiten = behaelter.querySelector("[data-bearbeiten]");
if (bearbeiten) bearbeiten.addEventListener("click", () => katalogFenster(behaelter, m));
const schalten = behaelter.querySelector("[data-schalten]");
if (schalten) schalten.addEventListener("click", () => {
const freigeben = !m.aktiv;
bestaetigen({
titel: freigeben ? "Medikament freigeben" : "Medikament stilllegen",
gefahr: !freigeben,
inhalt: `${sicher(m.kennung)} — ${sicher(m.name)}
${freigeben
? "Das Präparat steht wieder zur Auswahl."
: "Das Präparat lässt sich für neue Gaben nicht mehr auswählen."}
${!freigeben && m.bestand > 0 ? `
📦
Es liegen noch ${m.bestand} ${sicher(m.einheit)} im Schrank.
Die Entnahme muss gebucht werden — das System bucht sie nicht weg.
` : ""}
Gelöscht wird nichts. Bestehende Gaben und Buchungen
bleiben vollständig nachvollziehbar.
Grund (freiwillig)
`,
bestaetigenText: freigeben ? "Freigeben" : "Stilllegen",
bestaetigt: async (el, zu) => {
try {
await aufruf("medikament", "schalten", {
id: m.id, aktiv: freigeben,
grund: el.querySelector("[name=grund]").value.trim(),
});
zu();
meldung(freigeben ? "Freigegeben." : "Stillgelegt.", "gut");
oeffne(behaelter, m.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
}
/* ---- Buchen ----------------------------------------------------------------- */
function buchungsFenster(behaelter, d) {
const m = d.medikament;
// Die Ausgabe entsteht nur aus einer Gabe und fehlt hier bewusst
const arten = ["eingang", "ruecknahme", "verfall", "korrektur"];
bestaetigen({
titel: "Bestand buchen · " + m.name,
inhalt: `
Aktueller Bestand: ${m.bestand} ${sicher(m.einheit)}
Art der Buchung
${arten.map((a) =>
`${sicher(d.buchungsarten[a].name)} `).join("")}
Wareneingang und Rücknahme erhöhen den Bestand,
Verfall entnommen verringert ihn.
Korrektur setzt ihn auf den gezählten Wert.
Eine Ausgabe entsteht ausschließlich aus einer dokumentierten Gabe.
Menge
Grund (Pflicht)
`,
bestaetigenText: "Buchen",
bestaetigt: async (el, zu) => {
const grund = el.querySelector("[name=grund]").value.trim();
if (grund.length < 3) return meldung("Bitte einen Grund angeben.", "fehler");
try {
const antwort = await aufruf("medikament", "buchen", {
id: m.id,
art: el.querySelector("[name=art]").value,
menge: +el.querySelector("[name=menge]").value,
grund,
});
zu();
meldung("Gebucht. Neuer Bestand: " + antwort.bestand + " " + m.einheit + ".", "gut");
oeffne(behaelter, m.id);
} catch (f) { meldung(f.message, "fehler"); }
},
nachOeffnen: (hg) => {
// Bei einer Korrektur ist die Menge der neue Stand, nicht die Differenz
const art = hg.querySelector("[name=art]");
const label = hg.querySelector("[data-mengenlabel]");
art.addEventListener("change", () => {
label.textContent = art.value === "korrektur"
? "Gezählter Bestand" : "Menge";
});
},
});
}
/* ---- Katalog ---------------------------------------------------------------- */
function katalogFenster(behaelter, m) {
const neu = !m;
m = m || {
kennung: "", name: "", wirkstoff: "", kategorie: "sonstiges",
dosierung: "", einheit: "Stück", mindestbestand: 0, lagerort: "",
verfaelltAm: null, hinweis: "", btm: false, sortierung: 0,
};
const darfBtm = z.rechte.btm;
fenster({
titel: neu ? "Medikament anlegen" : "Stammdaten · " + m.name,
inhalt: `
Name
Wirkstoff
Hinweis
${neu ? `
ℹ
Ein neues Präparat startet mit Bestand 0 . Den Zugang buchst du
anschließend als Wareneingang — damit steht jede Menge im Schrank
nachvollziehbar in der Historie.
` : ""}`,
knoepfe: [
{ text: "Abbrechen", art: "leise", klick: (_, zu) => zu() },
{ text: neu ? "Anlegen" : "Speichern", klick: async (el, zu) => {
const wert = (name) => el.querySelector(`[name=${name}]`).value.trim();
try {
const antwort = await aufruf("medikament", "speichern", {
id: neu ? null : m.id,
kennung: wert("kennung"),
name: wert("name"),
wirkstoff: wert("wirkstoff"),
kategorie: el.querySelector("[name=kategorie]").value,
dosierung: wert("dosierung"),
einheit: wert("einheit"),
mindestbestand: +wert("mindestbestand"),
verfaelltAm: wert("verfaellt"),
lagerort: wert("lagerort"),
sortierung: +wert("sortierung"),
hinweis: wert("hinweis"),
btm: el.querySelector("[name=btm]").checked,
});
zu();
meldung(neu ? "Angelegt: " + antwort.kennung : "Gespeichert.", "gut");
if (neu) rendern(behaelter);
else oeffne(behaelter, m.id);
} catch (f) { meldung(f.message, "fehler"); }
} },
],
});
}
/* ---- BTM-Buch ---------------------------------------------------------------- */
function btmAnsicht(behaelter, d) {
behaelter.innerHTML = `
BTM-Buch
Lückenlose Bewegungsliste aller Betäubungsmittel. Reine Auskunft —
hier lässt sich nichts ändern.
← Bestand
Aktueller Stand
${d.bestaende.length ? d.bestaende.map((b) => `
${sicher(b.kennung)}
${sicher(b.name)}
${sicher(b.lagerort || "kein Lagerort")}
${b.verfaellt_am ? " · verfällt "
+ String(b.verfaellt_am).slice(0, 10) : ""}
${b.abgelaufen ? `
abgelaufen ` : ""}
${b.bestand} ${sicher(b.einheit)}
`).join("")
: `
Keine Betäubungsmittel im Katalog.
`}
${d.ohneBuchung.length ? `
Gaben ohne Bestandsbuchung
${d.ohneBuchung.length} Einträge
Diese Gaben wurden bewusst ohne Bestandsbuchung dokumentiert — etwa weil
das Präparat aus einem Fahrzeug stammte. Sie stehen hier, damit die
Zahlen im Buch erklärbar bleiben, nicht als Beanstandung.
${d.ohneBuchung.map((g) => `
${sicher(g.behandlung)}
${sicher(g.name)}
${g.menge} ${sicher(g.einheit)}
${sicher(g.patientName)}
· ${sicher(g.autor)} · ${zeit(g.gegeben_am)}
ohne Buchung
`).join("")}
` : ""}
Bewegungen
${d.bewegungen.length} Einträge
${d.gewaehlt ? "· gefiltert" : ""}
${d.gewaehlt
? `
Alle Präparate zeigen ` : ""}
${d.bewegungen.length ? d.bewegungen.map((b) => `
${sicher(b.artName)}
${sicher(b.kennung)}
${sicher(b.name)}
${b.art === "korrektur" ? "auf"
: b.art === "ausgabe" || b.art === "verfall" ? "−" : "+"}
${b.menge} ${sicher(b.einheit)} · Stand danach: ${b.bestand_danach}
${sicher(b.grund || "")}
${b.patientName ? " · " + sicher(b.patientName) : ""}
· ${sicher(b.autor)}
${zeit(b.gebucht_am)}
`).join("")
: `
Keine Bewegungen erfasst.
`}
`;
behaelter.querySelector("[data-zurueck]").addEventListener("click", () => {
z.reiter = "bestand";
z.btmMedikament = null;
rendern(behaelter);
});
behaelter.querySelectorAll("[data-med]").forEach((el) => {
el.addEventListener("click", () => {
z.btmMedikament = +el.dataset.med;
rendern(behaelter);
});
});
const alle = behaelter.querySelector("[data-alle]");
if (alle) alle.addEventListener("click", () => {
z.btmMedikament = null;
rendern(behaelter);
});
}
/* ---- Laden -------------------------------------------------------------------- */
async function oeffne(behaelter, medikamentId) {
try {
const d = await aufruf("medikament", "oeffnen", { id: medikamentId });
z.offen = medikamentId;
z.rechte = d.rechte;
z.kategorien = d.kategorien;
einzelAnsicht(behaelter, d);
} catch (f) { meldung(f.message, "fehler"); }
}
async function rendern(behaelter) {
try {
if (z.reiter === "btm") {
const d = await aufruf("medikament", "btmBuch", { medikament: z.btmMedikament });
return btmAnsicht(behaelter, d);
}
z.daten = await aufruf("medikament", "uebersicht", {
filter: z.filter, kategorie: z.kategorie,
begriff: z.suche, stillgelegt: z.stillgelegt,
});
z.rechte = z.daten.rechte;
z.kategorien = z.daten.kategorien;
// Sprungziel aus einem Hinweis, z. B. „Medikament fast leer"
const ziel = App.zustand.modulZiel;
if (ziel && ziel.medikament) {
App.zustand.modulZiel = null;
return oeffne(behaelter, ziel.medikament);
}
if (z.offen) await oeffne(behaelter, z.offen);
else uebersicht(behaelter);
} catch (f) {
behaelter.innerHTML = `Medikamente
Konnte nicht geladen werden: ${sicher(f.message)}
`;
}
}
App.registriereAnsicht("medikament", { rendern });
App.registriereSuche(async (begriff) => {
try {
const d = await aufruf("medikament", "uebersicht", { begriff });
return (d.liste || []).slice(0, 5).map((m) => ({
bereich: "Medikament",
titel: m.kennung + " · " + m.name,
unter: m.bestand + " " + m.einheit + (m.knapp ? " · unter Mindestbestand" : ""),
}));
} catch (e) { return []; }
});
})();