aether-framework/resources/[aether]/aether-medical/server/module/medikament.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

572 lines
24 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Medikamentenverwaltung
--
-- Bestandsführung und Katalogpflege. Nichts weiter.
--
-- Das System gibt kein Medikament aus, verordnet keines, bestellt
-- nichts nach und entsorgt nichts. Es zählt, was Menschen gebucht
-- haben, und erinnert, wenn ein Bestand knapp wird oder ein Datum
-- abläuft. Was daraufhin geschieht, entscheidet die Apotheke.
--
-- Abgelaufene Medikamente werden AUFFÄLLIG MARKIERT, aber nicht
-- gesperrt. Ob ein Präparat noch verwendbar ist, ist eine fachliche
-- Beurteilung — keine Datumsrechnung.
-- =====================================================================
Kern.RegistriereModul({
id = 'medikament', name = 'Medikamente', icon = '💊',
reihenfolge = 8, recht = 'medikament.sehen',
beschreibung = 'Bestand, Buchungen, Verfallsdaten und BTM-Buch',
})
-- Buchungsarten und ihre Wirkung auf den Bestand
local BUCHUNGSARTEN = {
eingang = { name = 'Wareneingang', richtung = 1 },
ruecknahme = { name = 'Rücknahme', richtung = 1 },
ausgabe = { name = 'Ausgabe', richtung = -1 },
verfall = { name = 'Verfall entnommen', richtung = -1 },
korrektur = { name = 'Korrektur', richtung = 0 },
}
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
--- Begrenzt einen Text und liefert nil bei leerer Eingabe
local function Text(wert, laenge)
if type(wert) ~= 'string' then return nil end
local sauber = wert:gsub('^%s+', ''):gsub('%s+$', '')
if sauber == '' then return nil end
return sauber:sub(1, laenge)
end
--- Name eines Mitarbeiters für die Anzeige
local function NameVon(charakterId)
if not charakterId then return 'unbekannt' end
local zeilen = MySQL.query.await(
'SELECT firstname, lastname FROM aether_characters WHERE id = ?', { charakterId })
if not zeilen or not zeilen[1] then return 'unbekannt' end
return zeilen[1].firstname .. ' ' .. zeilen[1].lastname
end
--- Prüft eine Kategorie gegen die Konfiguration
local function Kategorie(kennung)
for _, eintrag in ipairs(Medizin.Config.Medikamentkategorien) do
if eintrag.id == kennung then return eintrag.id end
end
return 'sonstiges'
end
-- =====================================================================
-- BESTANDSÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('medikament', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
local bedingungen, werte = {}, {}
if daten.stillgelegt == true then
bedingungen[#bedingungen + 1] = 'm.aktiv = 0'
else
bedingungen[#bedingungen + 1] = 'm.aktiv = 1'
end
if daten.kategorie and daten.kategorie ~= '' then
bedingungen[#bedingungen + 1] = 'm.kategorie = ?'
werte[#werte + 1] = daten.kategorie
end
-- Schnellfilter der Übersicht
local filter = tostring(daten.filter or '')
if filter == 'knapp' then
bedingungen[#bedingungen + 1] = 'm.bestand <= m.mindestbestand'
elseif filter == 'leer' then
bedingungen[#bedingungen + 1] = 'm.bestand <= 0'
elseif filter == 'verfall' then
bedingungen[#bedingungen + 1] =
'm.verfaellt_am IS NOT NULL AND m.verfaellt_am <= DATE_ADD(CURDATE(), INTERVAL ? DAY)'
werte[#werte + 1] = Medizin.Config.Erinnerungen.medikamentVorlaufTage
elseif filter == 'btm' then
bedingungen[#bedingungen + 1] = 'm.betaeubungsmittel = 1'
end
if type(daten.begriff) == 'string' and #daten.begriff >= 2 then
bedingungen[#bedingungen + 1] =
'(m.kennung LIKE ? OR m.name LIKE ? OR m.wirkstoff LIKE ?)'
local muster = '%' .. daten.begriff .. '%'
for _ = 1, 3 do werte[#werte + 1] = muster end
end
local liste = MySQL.query.await(([[
SELECT m.id, m.kennung, m.name, m.wirkstoff, m.kategorie,
m.dosierung_standard, m.einheit, m.bestand, m.mindestbestand,
m.lagerort, m.verfaellt_am, m.hinweis, m.betaeubungsmittel, m.aktiv,
DATEDIFF(m.verfaellt_am, CURDATE()) AS tageBisVerfall,
(SELECT COUNT(*) FROM medical_medikament_buchung b
WHERE b.medikament = m.id AND b.art = 'ausgabe'
AND b.gebucht_am >= DATE_SUB(NOW(), INTERVAL 7 DAY)) AS ausgabenWoche
FROM medical_medikament m
WHERE %s
ORDER BY m.kategorie, m.sortierung, m.name
LIMIT 300
]]):format(table.concat(bedingungen, ' AND ')), werte) or {}
local kategorieNamen = {}
for _, eintrag in ipairs(Medizin.Config.Medikamentkategorien) do
kategorieNamen[eintrag.id] = eintrag.name
end
for _, eintrag in ipairs(liste) do
eintrag.kategorieName = kategorieNamen[eintrag.kategorie] or eintrag.kategorie
eintrag.btm = eintrag.betaeubungsmittel == 1
eintrag.aktiv = eintrag.aktiv == 1
eintrag.knapp = eintrag.bestand <= eintrag.mindestbestand
eintrag.leer = eintrag.bestand <= 0
local tage = tonumber(eintrag.tageBisVerfall)
eintrag.abgelaufen = tage ~= nil and tage < 0
eintrag.laeuftAb = tage ~= nil and tage >= 0
and tage <= Medizin.Config.Erinnerungen.medikamentVorlaufTage
eintrag.betaeubungsmittel = nil
end
return {
liste = liste,
kategorien = Medizin.Config.Medikamentkategorien,
buchungsarten = BUCHUNGSARTEN,
filter = filter,
stillgelegt = daten.stillgelegt == true,
zaehler = {
gesamt = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_medikament WHERE aktiv = 1')) or 0,
knapp = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_medikament
WHERE aktiv = 1 AND bestand > 0 AND bestand <= mindestbestand
]])) or 0,
leer = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_medikament WHERE aktiv = 1 AND bestand <= 0')) or 0,
verfall = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_medikament
WHERE aktiv = 1 AND verfaellt_am IS NOT NULL
AND verfaellt_am <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
]], { Medizin.Config.Erinnerungen.medikamentVorlaufTage })) or 0,
btm = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_medikament WHERE aktiv = 1 AND betaeubungsmittel = 1')) or 0,
},
rechte = {
bestand = Rechte.Hat(mitarbeiter, 'medikament.bestand'),
btm = Rechte.Hat(mitarbeiter, 'medikament.btm'),
katalog = Rechte.Hat(mitarbeiter, 'katalog.bearbeiten'),
},
}
end)
Kern.RegistriereAktion('medikament', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
local medikamentId = tonumber(daten.id)
if not medikamentId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT m.*, DATEDIFF(m.verfaellt_am, CURDATE()) AS tageBisVerfall
FROM medical_medikament m WHERE m.id = ?
]], { medikamentId })
if not zeilen or not zeilen[1] then return nil, 'Medikament nicht gefunden.' end
local m = zeilen[1]
-- Buchungshistorie
local buchungen = MySQL.query.await([[
SELECT b.id, b.art, b.menge, b.bestand_danach, b.grund, b.gebucht_am,
b.gebucht_von, b.patient,
c.firstname, c.lastname
FROM medical_medikament_buchung b
LEFT JOIN aether_characters c ON c.id = b.patient
WHERE b.medikament = ?
ORDER BY b.id DESC LIMIT 60
]], { medikamentId }) or {}
for _, buchung in ipairs(buchungen) do
buchung.artName = BUCHUNGSARTEN[buchung.art] and BUCHUNGSARTEN[buchung.art].name
or buchung.art
buchung.autor = NameVon(buchung.gebucht_von)
buchung.patientName = buchung.firstname
and (buchung.firstname .. ' ' .. buchung.lastname) or nil
buchung.firstname, buchung.lastname, buchung.gebucht_von = nil, nil, nil
end
-- Zuletzt dokumentierte Gaben
local gaben = MySQL.query.await([[
SELECT g.id, g.menge, g.dosierung, g.gegeben_am, g.gegeben_von, g.hinweis,
b.kennung AS behandlung, c.firstname, c.lastname
FROM medical_behandlung_medikament g
JOIN medical_behandlung b ON b.id = g.behandlung
LEFT JOIN aether_characters c ON c.id = b.patient
WHERE g.medikament = ?
ORDER BY g.id DESC LIMIT 30
]], { medikamentId }) or {}
for _, gabe in ipairs(gaben) do
gabe.autor = NameVon(gabe.gegeben_von)
gabe.patientName = (gabe.firstname or 'Unbekannt') .. ' ' .. (gabe.lastname or '')
gabe.ohneBuchung = gabe.hinweis
and gabe.hinweis:find('ohne Bestandsbuchung', 1, true) ~= nil or false
gabe.firstname, gabe.lastname, gabe.gegeben_von = nil, nil, nil
end
local tage = tonumber(m.tageBisVerfall)
return {
medikament = {
id = m.id, kennung = m.kennung, name = m.name, wirkstoff = m.wirkstoff,
kategorie = m.kategorie, dosierung = m.dosierung_standard,
einheit = m.einheit, bestand = m.bestand, mindestbestand = m.mindestbestand,
lagerort = m.lagerort, verfaelltAm = m.verfaellt_am,
tageBisVerfall = tage,
abgelaufen = tage ~= nil and tage < 0,
hinweis = m.hinweis,
btm = m.betaeubungsmittel == 1,
aktiv = m.aktiv == 1,
sortierung = m.sortierung,
},
buchungen = buchungen,
gaben = gaben,
kategorien = Medizin.Config.Medikamentkategorien,
buchungsarten = BUCHUNGSARTEN,
rechte = {
bestand = Rechte.Hat(mitarbeiter, 'medikament.bestand'),
btm = Rechte.Hat(mitarbeiter, 'medikament.btm'),
katalog = Rechte.Hat(mitarbeiter, 'katalog.bearbeiten'),
},
}
end)
-- =====================================================================
-- BESTAND BUCHEN
--
-- Jede Buchung ist die Aufzeichnung dessen, was ein Mensch im Schrank
-- getan hat. Das System bewegt nichts von selbst.
-- =====================================================================
Kern.RegistriereAktion('medikament', 'buchen', 'medikament.bestand', function(quelle, mitarbeiter, daten)
local medikamentId = tonumber(daten.id)
local art = tostring(daten.art or '')
local menge = math.floor(tonumber(daten.menge) or 0)
if not medikamentId then return nil, 'Ungültige Kennung.' end
if not BUCHUNGSARTEN[art] then return nil, 'Unbekannte Buchungsart.' end
if art == 'ausgabe' then
return nil, 'Eine Ausgabe entsteht nur aus einer dokumentierten Gabe.'
end
if menge < 1 or menge > 100000 then return nil, 'Ungültige Menge.' end
local grund = Text(daten.grund, 255)
if not grund then return nil, 'Bitte einen Grund für die Buchung angeben.' end
local zeilen = MySQL.query.await([[
SELECT id, kennung, name, einheit, bestand, mindestbestand, betaeubungsmittel
FROM medical_medikament WHERE id = ?
]], { medikamentId })
if not zeilen or not zeilen[1] then return nil, 'Medikament nicht gefunden.' end
local medikament = zeilen[1]
-- Betäubungsmittel erfordern zusätzlich das eigene Recht
if medikament.betaeubungsmittel == 1 and not Rechte.Hat(mitarbeiter, 'medikament.btm') then
return nil, 'Betäubungsmittel darfst du nicht buchen.'
end
local richtung = BUCHUNGSARTEN[art].richtung
local neuerBestand
if art == 'korrektur' then
-- Eine Korrektur setzt den Bestand auf den gezählten Wert
neuerBestand = menge
else
neuerBestand = medikament.bestand + (richtung * menge)
if neuerBestand < 0 then
return nil, ('Das ergäbe einen Bestand von %d. Vorrätig sind %d %s.')
:format(neuerBestand, medikament.bestand, medikament.einheit)
end
end
MySQL.update.await('UPDATE medical_medikament SET bestand = ? WHERE id = ?',
{ neuerBestand, medikamentId })
MySQL.insert.await([[
INSERT INTO medical_medikament_buchung
(medikament, art, menge, bestand_danach, grund, gebucht_von)
VALUES (?, ?, ?, ?, ?, ?)
]], { medikamentId, art, menge, neuerBestand, grund, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'medikament',
('Bestand gebucht: %s'):format(BUCHUNGSARTEN[art].name),
{ typ = 'medikament', id = medikamentId },
{ kennung = medikament.kennung, menge = menge,
vorher = medikament.bestand, nachher = neuerBestand,
btm = medikament.betaeubungsmittel == 1 }, grund)
-- Erinnern, wenn es knapp wird — nachbestellt wird nichts
if neuerBestand <= medikament.mindestbestand then
Hinweise.Erstelle(nil, {
kategorie = 'medikament',
prioritaet = neuerBestand <= 0 and 'hoch' or 'normal',
titel = neuerBestand <= 0 and 'Medikament nicht mehr vorrätig'
or 'Medikament unter Mindestbestand',
text = ('%s — noch %d %s.'):format(medikament.name, neuerBestand,
medikament.einheit),
zielTyp = 'medikament', zielId = medikamentId,
})
end
Kern.Loese('medikament.gebucht', medikamentId, art, mitarbeiter)
return { bestand = neuerBestand }
end)
-- =====================================================================
-- KATALOG PFLEGEN
--
-- Werte hier sind Stammdaten und Vorschläge. Die Standarddosierung
-- ist ein Anhaltspunkt, keine Verordnung.
-- =====================================================================
Kern.RegistriereAktion('medikament', 'speichern', 'katalog.bearbeiten', function(quelle, mitarbeiter, daten)
local kennung = Text(daten.kennung, 24)
local name = Text(daten.name, 120)
if not kennung or #kennung < 2 then return nil, 'Bitte eine Kennung angeben.' end
if not name or #name < 2 then return nil, 'Bitte einen Namen angeben.' end
kennung = kennung:gsub('%s', ''):upper()
local id = tonumber(daten.id)
local btm = daten.btm == true
-- Betäubungsmittel darf nur kennzeichnen, wer sie auch buchen darf
if btm and not Rechte.Hat(mitarbeiter, 'medikament.btm') then
return nil, 'Nur mit BTM-Recht lässt sich ein Präparat als Betäubungsmittel führen.'
end
local belegt = MySQL.scalar.await(
'SELECT id FROM medical_medikament WHERE kennung = ? AND (? IS NULL OR id <> ?)',
{ kennung, id, id })
if belegt then return nil, 'Diese Kennung ist bereits vergeben.' end
local mindestbestand = math.max(0, math.floor(tonumber(daten.mindestbestand) or 0))
local sortierung = math.floor(tonumber(daten.sortierung) or 0)
local verfaellt = Text(daten.verfaelltAm, 10) -- Format JJJJ-MM-TT
if verfaellt and not verfaellt:match('^%d%d%d%d%-%d%d%-%d%d$') then
return nil, 'Das Verfallsdatum muss im Format JJJJ-MM-TT stehen.'
end
if id then
local alt = MySQL.query.await(
'SELECT kennung, name, mindestbestand, verfaellt_am FROM medical_medikament WHERE id = ?',
{ id })
if not alt or not alt[1] then return nil, 'Medikament nicht gefunden.' end
MySQL.update.await([[
UPDATE medical_medikament
SET kennung = ?, name = ?, wirkstoff = ?, kategorie = ?,
dosierung_standard = ?, einheit = ?, mindestbestand = ?,
lagerort = ?, verfaellt_am = ?, hinweis = ?,
betaeubungsmittel = ?, sortierung = ?
WHERE id = ?
]], {
kennung, name, Text(daten.wirkstoff, 120), Kategorie(daten.kategorie),
Text(daten.dosierung, 64), Text(daten.einheit, 24) or 'Stück',
mindestbestand, Text(daten.lagerort, 64), verfaellt,
Text(daten.hinweis, 255), btm and 1 or 0, sortierung, id,
})
Protokoll.Schreibe(mitarbeiter, 'medikament', 'Medikament bearbeitet',
{ typ = 'medikament', id = id },
{ kennung = kennung,
verfallAlt = alt[1].verfaellt_am, verfallNeu = verfaellt,
mindestAlt = alt[1].mindestbestand, mindestNeu = mindestbestand })
else
-- Ein neues Präparat startet mit Bestand 0. Der Zugang wird gebucht.
id = MySQL.insert.await([[
INSERT INTO medical_medikament
(kennung, name, wirkstoff, kategorie, dosierung_standard, einheit,
bestand, mindestbestand, lagerort, verfaellt_am, hinweis,
betaeubungsmittel, sortierung)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)
]], {
kennung, name, Text(daten.wirkstoff, 120), Kategorie(daten.kategorie),
Text(daten.dosierung, 64), Text(daten.einheit, 24) or 'Stück',
mindestbestand, Text(daten.lagerort, 64), verfaellt,
Text(daten.hinweis, 255), btm and 1 or 0, sortierung,
})
Protokoll.Schreibe(mitarbeiter, 'medikament', 'Medikament angelegt',
{ typ = 'medikament', id = id }, { kennung = kennung, btm = btm })
end
Kern.Loese('medikament.katalog', id, mitarbeiter)
return { id = id, kennung = kennung }
end)
--- Stilllegen statt löschen: alte Gaben bleiben nachvollziehbar
Kern.RegistriereAktion('medikament', 'schalten', 'katalog.bearbeiten', function(quelle, mitarbeiter, daten)
local medikamentId = tonumber(daten.id)
if not medikamentId then return nil, 'Ungültige Kennung.' end
local aktiv = daten.aktiv == true
local zeilen = MySQL.query.await(
'SELECT id, kennung, name, bestand, einheit FROM medical_medikament WHERE id = ?',
{ medikamentId })
if not zeilen or not zeilen[1] then return nil, 'Medikament nicht gefunden.' end
local medikament = zeilen[1]
-- Ein Restbestand wird gemeldet, aber nicht weggebucht
if not aktiv and medikament.bestand > 0 then
Hinweise.Erstelle(nil, {
kategorie = 'medikament', prioritaet = 'normal',
titel = 'Stillgelegtes Medikament mit Restbestand',
text = ('%s wurde stillgelegt, im Schrank liegen noch %d %s. Die Entnahme muss gebucht werden.')
:format(medikament.name, medikament.bestand, medikament.einheit),
zielTyp = 'medikament', zielId = medikamentId,
})
end
MySQL.update.await('UPDATE medical_medikament SET aktiv = ? WHERE id = ?',
{ aktiv and 1 or 0, medikamentId })
Protokoll.Schreibe(mitarbeiter, 'medikament',
aktiv and 'Medikament wieder freigegeben' or 'Medikament stillgelegt',
{ typ = 'medikament', id = medikamentId },
{ kennung = medikament.kennung, restbestand = medikament.bestand },
Text(daten.grund, 255))
return { ok = true }
end)
-- =====================================================================
-- BTM-BUCH
--
-- Lückenlose Bewegungsliste aller Betäubungsmittel. Reine Auskunft;
-- hier lässt sich nichts ändern.
-- =====================================================================
Kern.RegistriereAktion('medikament', 'btmBuch', 'medikament.btm', function(quelle, mitarbeiter, daten)
local medikamentId = tonumber(daten.medikament)
local bedingungen = { 'm.betaeubungsmittel = 1' }
local werte = {}
if medikamentId then
bedingungen[#bedingungen + 1] = 'm.id = ?'
werte[#werte + 1] = medikamentId
end
local bewegungen = MySQL.query.await(([[
SELECT b.id, b.art, b.menge, b.bestand_danach, b.grund, b.gebucht_am,
b.gebucht_von, b.patient,
m.id AS medikamentId, m.kennung, m.name, m.einheit,
c.firstname, c.lastname
FROM medical_medikament_buchung b
JOIN medical_medikament m ON m.id = b.medikament
LEFT JOIN aether_characters c ON c.id = b.patient
WHERE %s
ORDER BY b.id DESC
LIMIT 200
]]):format(table.concat(bedingungen, ' AND ')), werte) or {}
for _, eintrag in ipairs(bewegungen) do
eintrag.artName = BUCHUNGSARTEN[eintrag.art] and BUCHUNGSARTEN[eintrag.art].name
or eintrag.art
eintrag.autor = NameVon(eintrag.gebucht_von)
eintrag.patientName = eintrag.firstname
and (eintrag.firstname .. ' ' .. eintrag.lastname) or nil
eintrag.firstname, eintrag.lastname, eintrag.gebucht_von = nil, nil, nil
end
-- Gaben, die ohne Bestandsbuchung dokumentiert wurden.
-- Das ist keine Unregelmässigkeit, sondern ein bewusster Vermerk —
-- er gehört aber ins Buch, damit die Zahlen erklärbar bleiben.
local ohneBuchung = MySQL.query.await([[
SELECT g.id, g.menge, g.gegeben_am, g.gegeben_von, g.hinweis,
m.kennung, m.name, m.einheit,
bh.kennung AS behandlung,
c.firstname, c.lastname
FROM medical_behandlung_medikament g
JOIN medical_medikament m ON m.id = g.medikament
JOIN medical_behandlung bh ON bh.id = g.behandlung
LEFT JOIN aether_characters c ON c.id = bh.patient
WHERE m.betaeubungsmittel = 1
AND g.hinweis LIKE '%ohne Bestandsbuchung%'
ORDER BY g.id DESC LIMIT 60
]]) or {}
for _, eintrag in ipairs(ohneBuchung) do
eintrag.autor = NameVon(eintrag.gegeben_von)
eintrag.patientName = (eintrag.firstname or 'Unbekannt')
.. ' ' .. (eintrag.lastname or '')
eintrag.firstname, eintrag.lastname, eintrag.gegeben_von = nil, nil, nil
end
-- Aktueller Stand aller Betäubungsmittel
local bestaende = MySQL.query.await([[
SELECT id, kennung, name, einheit, bestand, mindestbestand, lagerort,
verfaellt_am, DATEDIFF(verfaellt_am, CURDATE()) AS tageBisVerfall
FROM medical_medikament
WHERE betaeubungsmittel = 1 AND aktiv = 1
ORDER BY name
]]) or {}
for _, eintrag in ipairs(bestaende) do
local tage = tonumber(eintrag.tageBisVerfall)
eintrag.abgelaufen = tage ~= nil and tage < 0
end
return {
bewegungen = bewegungen,
ohneBuchung = ohneBuchung,
bestaende = bestaende,
gewaehlt = medikamentId,
}
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Ressourcen
-- ---------------------------------------------------------------------
--- Liefert den Bestand eines Medikaments
--- @param kennung string Kennung, z. B. 'ANA-01'
--- @return table|nil
exports('HoleBestand', function(kennung)
if type(kennung) ~= 'string' then return nil end
local zeilen = MySQL.query.await([[
SELECT kennung, name, einheit, bestand, mindestbestand, verfaellt_am, aktiv
FROM medical_medikament WHERE kennung = ?
]], { kennung })
return zeilen and zeilen[1] or nil
end)
--- Meldet einen Wareneingang aus einer anderen Ressource.
--- Der buchende Mitarbeiter muss im Dienst sein — das System bucht nie
--- aus eigenem Antrieb.
--- @param charakterId number Buchender Mitarbeiter
--- @param kennung string Medikament
--- @param menge number Anzahl
--- @param grund string Begründung
--- @return number|nil Neuer Bestand
exports('BucheEingang', function(charakterId, kennung, menge, grund)
local quelle = Dienst.QuelleVonCharakter(tonumber(charakterId) or 0)
local mitarbeiter = quelle and Dienst.Hole(quelle)
if not mitarbeiter then return nil end
local medikament = MySQL.scalar.await(
'SELECT id FROM medical_medikament WHERE kennung = ?', { tostring(kennung) })
if not medikament then return nil end
local ergebnis = Kern.FuehreAus(quelle, mitarbeiter, 'medikament', 'buchen', {
id = medikament, art = 'eingang',
menge = menge, grund = grund or 'Wareneingang',
})
return ergebnis and ergebnis.bestand or nil
end)