- 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>
680 lines
28 KiB
JavaScript
680 lines
28 KiB
JavaScript
/* =====================================================================
|
||
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 = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>Medikamente</h2>
|
||
<p>Bestandsführung. Das System gibt nichts aus, verordnet nichts und
|
||
bestellt nichts nach — es zählt, was gebucht wurde.</p>
|
||
</div>
|
||
${d.rechte.katalog
|
||
? `<button class="knopf" data-neu="1">+ Medikament anlegen</button>` : ""}
|
||
</div>
|
||
|
||
<div class="raster k5">
|
||
${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", "")}
|
||
</div>
|
||
|
||
<div class="reiter">
|
||
<button class="${z.reiter === "bestand" ? "aktiv" : ""}" data-reiter="bestand">
|
||
Bestand</button>
|
||
${d.rechte.btm
|
||
? `<button class="${z.reiter === "btm" ? "aktiv" : ""}" data-reiter="btm">
|
||
BTM-Buch</button>` : ""}
|
||
</div>
|
||
|
||
<div class="filterleiste">
|
||
<input data-suche="1" placeholder="Kennung, Name oder Wirkstoff…"
|
||
value="${sicher(z.suche)}">
|
||
<select data-kategorie="1">
|
||
<option value="">Alle Kategorien</option>
|
||
${d.kategorien.map((k) =>
|
||
`<option value="${sicher(k.id)}" ${k.id === z.kategorie ? "selected" : ""}>
|
||
${sicher(k.name)}</option>`).join("")}
|
||
</select>
|
||
${z.filter
|
||
? `<button class="knopf leise" data-filterweg="1">Filter „${sicher(z.filter)}" aufheben</button>`
|
||
: ""}
|
||
${d.rechte.katalog
|
||
? `<button class="knopf ${z.stillgelegt ? "" : "leise"}" data-stillgelegt="1">
|
||
${z.stillgelegt ? "Stillgelegte" : "Aktive"}</button>` : ""}
|
||
</div>
|
||
|
||
${Object.keys(gruppen).length
|
||
? Object.keys(gruppen).map((kategorie) => `
|
||
<div class="karte" style="margin-bottom:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">${sicher(gruppen[kategorie][0].kategorieName)}</span>
|
||
<span class="feld-hinweis">${gruppen[kategorie].length} Präparate</span>
|
||
</div>
|
||
<div class="liste">${gruppen[kategorie].map(zeile).join("")}</div>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Kein Medikament gefunden.</div>`}
|
||
`;
|
||
|
||
verdrahteUebersicht(behaelter);
|
||
}
|
||
|
||
function kachel(filter, titel, wert, zusatz, art) {
|
||
const aktiv = z.filter === filter;
|
||
return `<div class="kennzahl klickbar ${art} ${aktiv ? "gewaehlt" : ""}"
|
||
data-filter="${sicher(filter)}">
|
||
<div class="kz-titel">${sicher(titel)}</div>
|
||
<div class="kz-wert">${wert}</div>
|
||
<div class="kz-zusatz">${sicher(zusatz)}</div>
|
||
</div>`;
|
||
}
|
||
|
||
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 `<div class="liste-zeile ${m.aktiv ? "" : "med-aus"}" data-oeffne="${m.id}">
|
||
<span class="db-nummer">${sicher(m.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(m.name)}
|
||
${m.btm ? `<span class="marker kritisch">BTM</span>` : ""}</div>
|
||
<div class="lz-unter">
|
||
${sicher(m.wirkstoff || "—")}
|
||
${m.dosierung_standard ? " · " + sicher(m.dosierung_standard) : ""}
|
||
${m.lagerort ? " · " + sicher(m.lagerort) : ""}
|
||
</div>
|
||
</div>
|
||
|
||
${m.abgelaufen
|
||
? `<span class="marker kritisch" title="Verfallsdatum überschritten">abgelaufen</span>`
|
||
: m.laeuftAb
|
||
? `<span class="marker warn" title="Verfällt am ${sicher(m.verfaellt_am)}">
|
||
in ${m.tageBisVerfall} Tagen</span>` : ""}
|
||
${!m.aktiv ? `<span class="marker neutral">stillgelegt</span>` : ""}
|
||
|
||
<div class="med-bestand">
|
||
<div class="med-balken"><i style="width:${anteil}%;background:${farbe}"></i></div>
|
||
<span class="med-zahl ${m.leer ? "leer" : m.knapp ? "knapp" : ""}">
|
||
${m.bestand} ${sicher(m.einheit)}</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
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 = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>${sicher(m.kennung)} · ${sicher(m.name)}</h2>
|
||
<p>${sicher(m.wirkstoff || "ohne Wirkstoffangabe")}
|
||
${m.btm ? " · <b>Betäubungsmittel</b>" : ""}
|
||
${m.aktiv ? "" : " · stillgelegt"}</p>
|
||
</div>
|
||
<button class="knopf leise" data-zurueck="1">← Bestand</button>
|
||
</div>
|
||
|
||
${m.abgelaufen ? `
|
||
<div class="warnung kritisch">
|
||
<span class="w-symbol">⏳</span>
|
||
<div><b>Verfallsdatum überschritten</b> — am ${zeit(m.verfaelltAm)}.
|
||
<br><span class="feld-hinweis">Das Präparat bleibt buchbar und
|
||
dokumentierbar. Ob es noch verwendbar ist, beurteilt die Apotheke —
|
||
das System sperrt nichts anhand eines Datums.</span></div>
|
||
</div>` : ""}
|
||
|
||
${m.bestand <= m.mindestbestand ? `
|
||
<div class="warnung ${m.bestand <= 0 ? "kritisch" : "warn"}">
|
||
<span class="w-symbol">📦</span>
|
||
<div><b>${m.bestand <= 0 ? "Nicht vorrätig" : "Unter Mindestbestand"}</b>
|
||
— ${m.bestand} von mindestens ${m.mindestbestand} ${sicher(m.einheit)}.
|
||
Nachbestellen muss jemand von Hand.</div>
|
||
</div>` : ""}
|
||
|
||
<div class="beh-kopf">
|
||
<div class="werte-liste">
|
||
<div><span>Bestand</span><b>${m.bestand} ${sicher(m.einheit)}</b></div>
|
||
<div><span>Mindestbestand</span><b>${m.mindestbestand} ${sicher(m.einheit)}</b></div>
|
||
<div class="${m.dosierung ? "" : "leer"}"><span>Standarddosierung</span>
|
||
<b>${sicher(m.dosierung || "keine hinterlegt")}</b></div>
|
||
<div class="${m.lagerort ? "" : "leer"}"><span>Lagerort</span>
|
||
<b>${sicher(m.lagerort || "nicht angegeben")}</b></div>
|
||
<div class="${m.verfaelltAm ? "" : "leer"}"><span>Verfällt am</span>
|
||
<b>${m.verfaelltAm ? String(m.verfaelltAm).slice(0, 10) : "kein Datum"}</b></div>
|
||
<div class="${m.hinweis ? "" : "leer"}"><span>Hinweis</span>
|
||
<b>${sicher(m.hinweis || "keiner")}</b></div>
|
||
</div>
|
||
<div class="beh-kopf-aktionen">
|
||
${d.rechte.bestand && m.aktiv
|
||
? `<button class="knopf klein" data-buchen="1">Bestand buchen</button>` : ""}
|
||
${d.rechte.katalog
|
||
? `<button class="knopf leise klein" data-bearbeiten="1">Stammdaten</button>` : ""}
|
||
${d.rechte.katalog
|
||
? `<button class="knopf ${m.aktiv ? "leise" : ""} klein" data-schalten="1">
|
||
${m.aktiv ? "Stilllegen" : "Freigeben"}</button>` : ""}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="db-spalten">
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Buchungen</span>
|
||
<span class="feld-hinweis">Bestand nach jeder Buchung</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.buchungen.length ? d.buchungen.map((b) => `
|
||
<div class="liste-zeile">
|
||
<span class="marker ${b.art === "ausgabe" || b.art === "verfall"
|
||
? "warn" : b.art === "korrektur" ? "info" : "gut"}">
|
||
${sicher(b.artName)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${b.art === "korrektur" ? "auf" : b.art === "ausgabe"
|
||
|| b.art === "verfall" ? "−" : "+"} ${b.menge} ${sicher(m.einheit)}
|
||
<span class="pat-geburt">Stand danach: ${b.bestand_danach}</span></div>
|
||
<div class="lz-unter">${sicher(b.grund || "")}
|
||
${b.patientName ? " · " + sicher(b.patientName) : ""}
|
||
· ${sicher(b.autor)} · ${zeit(b.gebucht_am)}</div>
|
||
</div>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch keine Buchung.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Dokumentierte Gaben</span>
|
||
<span class="feld-hinweis">aus den Behandlungsbögen</span>
|
||
</div>
|
||
<div class="liste">
|
||
${d.gaben.length ? d.gaben.map((g) => `
|
||
<div class="liste-zeile">
|
||
<span class="db-nummer">${sicher(g.behandlung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(g.patientName)}
|
||
<span class="pat-geburt">${g.menge} ${sicher(m.einheit)}</span></div>
|
||
<div class="lz-unter">${g.dosierung ? sicher(g.dosierung) + " · " : ""}
|
||
${sicher(g.autor)} · ${zeit(g.gegeben_am)}</div>
|
||
</div>
|
||
${g.ohneBuchung
|
||
? `<span class="marker warn" title="Ohne Bestandsbuchung dokumentiert">
|
||
ohne Buchung</span>` : ""}
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Noch keine Gabe dokumentiert.</div>`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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: `<p><b>${sicher(m.kennung)}</b> — ${sicher(m.name)}</p>
|
||
<p>${freigeben
|
||
? "Das Präparat steht wieder zur Auswahl."
|
||
: "Das Präparat lässt sich für neue Gaben nicht mehr auswählen."}</p>
|
||
${!freigeben && m.bestand > 0 ? `
|
||
<div class="warnung warn">
|
||
<span class="w-symbol">📦</span>
|
||
<div>Es liegen noch <b>${m.bestand} ${sicher(m.einheit)}</b> im Schrank.
|
||
Die Entnahme muss gebucht werden — das System bucht sie nicht weg.</div>
|
||
</div>` : ""}
|
||
<p class="feld-hinweis">Gelöscht wird nichts. Bestehende Gaben und Buchungen
|
||
bleiben vollständig nachvollziehbar.</p>
|
||
<div class="feld"><label>Grund (freiwillig)</label>
|
||
<input name="grund" maxlength="255"></div>`,
|
||
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: `
|
||
<p>Aktueller Bestand: <b>${m.bestand} ${sicher(m.einheit)}</b></p>
|
||
<div class="feld"><label>Art der Buchung</label>
|
||
<select name="art">
|
||
${arten.map((a) =>
|
||
`<option value="${sicher(a)}">${sicher(d.buchungsarten[a].name)}</option>`).join("")}
|
||
</select>
|
||
<div class="feld-hinweis">
|
||
<b>Wareneingang</b> und <b>Rücknahme</b> erhöhen den Bestand,
|
||
<b>Verfall entnommen</b> verringert ihn.
|
||
<b>Korrektur</b> setzt ihn auf den gezählten Wert.
|
||
Eine <b>Ausgabe</b> entsteht ausschließlich aus einer dokumentierten Gabe.
|
||
</div></div>
|
||
<div class="feld"><label data-mengenlabel="1">Menge</label>
|
||
<input name="menge" type="number" min="1" max="100000" value="1"></div>
|
||
<div class="feld"><label>Grund (Pflicht)</label>
|
||
<input name="grund" maxlength="255"
|
||
placeholder="z. B. Lieferung Apotheke, Inventur vom 12.05."></div>`,
|
||
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: `
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Kennung</label>
|
||
<input name="kennung" maxlength="24" value="${sicher(m.kennung)}"
|
||
placeholder="z. B. ANA-04"></div>
|
||
<div class="feld"><label>Kategorie</label>
|
||
<select name="kategorie">
|
||
${z.kategorien.map((k) =>
|
||
`<option value="${sicher(k.id)}" ${k.id === m.kategorie ? "selected" : ""}>
|
||
${sicher(k.name)}</option>`).join("")}
|
||
</select></div>
|
||
</div>
|
||
<div class="feld"><label>Name</label>
|
||
<input name="name" maxlength="120" value="${sicher(m.name)}"></div>
|
||
<div class="feld"><label>Wirkstoff</label>
|
||
<input name="wirkstoff" maxlength="120" value="${sicher(m.wirkstoff || "")}"></div>
|
||
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Standarddosierung</label>
|
||
<input name="dosierung" maxlength="64" value="${sicher(m.dosierung || "")}"
|
||
placeholder="z. B. 1 g i.v.">
|
||
<div class="feld-hinweis">Ein Vorschlag im Gabe-Dialog, keine Verordnung.</div></div>
|
||
<div class="feld"><label>Einheit</label>
|
||
<input name="einheit" maxlength="24" value="${sicher(m.einheit || "Stück")}"></div>
|
||
</div>
|
||
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Mindestbestand</label>
|
||
<input name="mindestbestand" type="number" min="0" value="${m.mindestbestand || 0}">
|
||
<div class="feld-hinweis">Ab hier erinnert das System.</div></div>
|
||
<div class="feld"><label>Verfällt am</label>
|
||
<input name="verfaellt" maxlength="10" placeholder="JJJJ-MM-TT"
|
||
value="${m.verfaelltAm ? String(m.verfaelltAm).slice(0, 10) : ""}"></div>
|
||
</div>
|
||
|
||
<div class="feld-reihe">
|
||
<div class="feld"><label>Lagerort</label>
|
||
<input name="lagerort" maxlength="64" value="${sicher(m.lagerort || "")}"></div>
|
||
<div class="feld"><label>Sortierung</label>
|
||
<input name="sortierung" type="number" value="${m.sortierung || 0}"></div>
|
||
</div>
|
||
|
||
<div class="feld"><label>Hinweis</label>
|
||
<input name="hinweis" maxlength="255" value="${sicher(m.hinweis || "")}"
|
||
placeholder="z. B. Wechselwirkung mit Antikoagulantien"></div>
|
||
|
||
<div class="feld">
|
||
<label><input type="checkbox" name="btm" class="haken"
|
||
${m.btm ? "checked" : ""} ${darfBtm ? "" : "disabled"}>
|
||
Betäubungsmittel</label>
|
||
<div class="feld-hinweis">${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."}</div>
|
||
</div>
|
||
|
||
${neu ? `<div class="grundsatz">
|
||
<span>ℹ</span>
|
||
<div>Ein neues Präparat startet mit <b>Bestand 0</b>. Den Zugang buchst du
|
||
anschließend als Wareneingang — damit steht jede Menge im Schrank
|
||
nachvollziehbar in der Historie.</div>
|
||
</div>` : ""}`,
|
||
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 = `
|
||
<div class="seiten-kopf">
|
||
<div>
|
||
<h2>BTM-Buch</h2>
|
||
<p>Lückenlose Bewegungsliste aller Betäubungsmittel. Reine Auskunft —
|
||
hier lässt sich nichts ändern.</p>
|
||
</div>
|
||
<button class="knopf leise" data-zurueck="1">← Bestand</button>
|
||
</div>
|
||
|
||
<div class="karte" style="margin-bottom:14px">
|
||
<div class="karte-kopf"><span class="karte-titel">Aktueller Stand</span></div>
|
||
<div class="liste">
|
||
${d.bestaende.length ? d.bestaende.map((b) => `
|
||
<div class="liste-zeile klickbar" data-med="${b.id}">
|
||
<span class="db-nummer">${sicher(b.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.name)}</div>
|
||
<div class="lz-unter">${sicher(b.lagerort || "kein Lagerort")}
|
||
${b.verfaellt_am ? " · verfällt "
|
||
+ String(b.verfaellt_am).slice(0, 10) : ""}</div>
|
||
</div>
|
||
${b.abgelaufen ? `<span class="marker kritisch">abgelaufen</span>` : ""}
|
||
<span class="med-zahl ${b.bestand <= b.mindestbestand ? "knapp" : ""}">
|
||
${b.bestand} ${sicher(b.einheit)}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Betäubungsmittel im Katalog.</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
${d.ohneBuchung.length ? `
|
||
<div class="karte" style="margin-bottom:14px">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Gaben ohne Bestandsbuchung</span>
|
||
<span class="feld-hinweis">${d.ohneBuchung.length} Einträge</span>
|
||
</div>
|
||
<p class="feld-hinweis" style="margin-top:0">
|
||
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.
|
||
</p>
|
||
<div class="liste">
|
||
${d.ohneBuchung.map((g) => `
|
||
<div class="liste-zeile">
|
||
<span class="db-nummer">${sicher(g.behandlung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(g.name)}
|
||
<span class="pat-geburt">${g.menge} ${sicher(g.einheit)}</span></div>
|
||
<div class="lz-unter">${sicher(g.patientName)}
|
||
· ${sicher(g.autor)} · ${zeit(g.gegeben_am)}</div>
|
||
</div>
|
||
<span class="marker warn">ohne Buchung</span>
|
||
</div>`).join("")}
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
<div class="karte">
|
||
<div class="karte-kopf">
|
||
<span class="karte-titel">Bewegungen</span>
|
||
<span class="feld-hinweis">${d.bewegungen.length} Einträge
|
||
${d.gewaehlt ? "· gefiltert" : ""}</span>
|
||
</div>
|
||
${d.gewaehlt
|
||
? `<button class="knopf leise klein" data-alle="1"
|
||
style="margin-bottom:10px">Alle Präparate zeigen</button>` : ""}
|
||
<div class="liste">
|
||
${d.bewegungen.length ? d.bewegungen.map((b) => `
|
||
<div class="liste-zeile">
|
||
<span class="marker ${b.art === "ausgabe" || b.art === "verfall"
|
||
? "warn" : b.art === "korrektur" ? "info" : "gut"}">
|
||
${sicher(b.artName)}</span>
|
||
<span class="db-nummer">${sicher(b.kennung)}</span>
|
||
<div class="lz-haupt">
|
||
<div class="lz-titel">${sicher(b.name)}
|
||
<span class="pat-geburt">${b.art === "korrektur" ? "auf"
|
||
: b.art === "ausgabe" || b.art === "verfall" ? "−" : "+"}
|
||
${b.menge} ${sicher(b.einheit)} · Stand danach: ${b.bestand_danach}</span></div>
|
||
<div class="lz-unter">${sicher(b.grund || "")}
|
||
${b.patientName ? " · " + sicher(b.patientName) : ""}
|
||
· ${sicher(b.autor)}</div>
|
||
</div>
|
||
<span class="db-zeit">${zeit(b.gebucht_am)}</span>
|
||
</div>`).join("")
|
||
: `<div class="liste-leer">Keine Bewegungen erfasst.</div>`}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `<div class="seiten-kopf"><div><h2>Medikamente</h2>
|
||
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
|
||
}
|
||
}
|
||
|
||
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 []; }
|
||
});
|
||
})();
|