/* ===================================================================== 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 ? `` : ""}
${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", "")}
${d.rechte.btm ? `` : ""}
${z.filter ? `` : ""} ${d.rechte.katalog ? `` : ""}
${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"}

${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 ? `` : ""} ${d.rechte.katalog ? `` : ""} ${d.rechte.katalog ? `` : ""}
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.

`, 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)}

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.
`, 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: `
Ein Vorschlag im Gabe-Dialog, keine Verordnung.
Ab hier erinnert das System.
${darfBtm ? "Betäubungsmittel erscheinen zusätzlich im BTM-Buch und erfordern für jede Gabe und Buchung das BTM-Recht." : "Zum Kennzeichnen als Betäubungsmittel fehlt dir das BTM-Recht."}
${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.

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 ? `` : ""}
${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 []; } }); })();