- 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>
691 lines
27 KiB
Lua
691 lines
27 KiB
Lua
-- =====================================================================
|
|
-- aether-medical / Modul: Blutbank
|
|
--
|
|
-- Ein Lagerbuch für Blutkonserven. Nichts weiter.
|
|
--
|
|
-- Das System transfundiert nicht, wählt keine Konserve aus und
|
|
-- verwirft keine. Eine abgelaufene Konserve bleibt so lange im
|
|
-- Bestand, bis ein Mensch sie herausnimmt — das Ablaufdatum ist
|
|
-- eine Erinnerung, keine Sperre.
|
|
--
|
|
-- Die Verträglichkeitstabelle ist eine Nachschlagehilfe. Passt eine
|
|
-- Konserve laut Tabelle nicht zum Empfänger, wird das deutlich
|
|
-- angezeigt und mitprotokolliert — verhindert wird es nicht. Ob
|
|
-- transfundiert wird, entscheidet ausschliesslich ein Arzt.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereModul({
|
|
id = 'blutbank', name = 'Blutbank', icon = '🩸',
|
|
reihenfolge = 11, recht = 'blut.sehen',
|
|
beschreibung = 'Bestand, Reservierungen, Ausgabe und Verfallsdaten',
|
|
})
|
|
|
|
-- Status und ihre erlaubten Übergänge. Jeder Schritt braucht einen
|
|
-- Menschen — es gibt keinen Weg, den das System von selbst geht.
|
|
local UEBERGAENGE = {
|
|
vorraetig = { reserviert = true, ausgegeben = true, verworfen = true },
|
|
reserviert = { vorraetig = true, ausgegeben = true, verworfen = true },
|
|
ausgegeben = {},
|
|
verworfen = {},
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfsfunktionen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Begrenzt einen Text und liefert nil bei leerer Eingabe
|
|
--- @param wert any Eingabe
|
|
--- @param laenge number Maximale Zeichenzahl
|
|
--- @return string|nil
|
|
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 Charakters für die Anzeige
|
|
--- @param charakterId number|nil Charakter
|
|
--- @return string
|
|
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 Blutgruppe gegen die Konfiguration
|
|
--- @param wert any Eingabe
|
|
--- @return string|nil Gültige Blutgruppe
|
|
local function Blutgruppe(wert)
|
|
local gesucht = tostring(wert or '')
|
|
for _, gruppe in ipairs(Medizin.Config.Blutgruppen) do
|
|
if gruppe == gesucht then return gruppe end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Prüft einen Lagerort gegen die Konfiguration
|
|
--- @param wert any Eingabe
|
|
--- @return string|nil Gültiger Lagerort
|
|
local function Lagerort(wert)
|
|
local gesucht = tostring(wert or '')
|
|
for _, ort in ipairs(Medizin.Config.Blutlagerorte) do
|
|
if ort == gesucht then return ort end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Prüft ein Datum im Format JJJJ-MM-TT
|
|
--- @param wert any Eingabe
|
|
--- @return string|nil
|
|
local function Datum(wert)
|
|
local roh = Text(wert, 10)
|
|
if not roh then return nil end
|
|
if not roh:match('^%d%d%d%d%-%d%d%-%d%d$') then return nil end
|
|
return roh
|
|
end
|
|
|
|
--- Passt eine Konserve laut Nachschlagetabelle zum Empfänger?
|
|
---
|
|
--- Liefert `true`, wenn die Kombination in der Tabelle steht, und
|
|
--- `nil`, wenn sich dazu nichts sagen lässt — etwa weil die
|
|
--- Blutgruppe des Patienten nie bestimmt wurde. Der Rückgabewert
|
|
--- sperrt nichts; er wird nur angezeigt und protokolliert.
|
|
--- @param empfaenger string|nil Blutgruppe des Patienten
|
|
--- @param konserve string Blutgruppe der Konserve
|
|
--- @return boolean|nil
|
|
local function Vertraeglich(empfaenger, konserve)
|
|
if not empfaenger or empfaenger == '' then return nil end
|
|
|
|
local erlaubt = Medizin.Config.Blutvertraeglichkeit[empfaenger]
|
|
if not erlaubt then return nil end
|
|
|
|
for _, gruppe in ipairs(erlaubt) do
|
|
if gruppe == konserve then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Lädt eine Konserve mit den Daten des Empfängers
|
|
--- @param konserveId number Konserve
|
|
--- @return table|nil
|
|
local function LadeKonserve(konserveId)
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT b.*,
|
|
DATEDIFF(b.verfaellt_am, CURDATE()) AS tage,
|
|
c.firstname, c.lastname,
|
|
p.blutgruppe AS empfaengerGruppe
|
|
FROM medical_blut b
|
|
LEFT JOIN aether_characters c ON c.id = b.reserviert_fuer
|
|
LEFT JOIN medical_patient p ON p.`character` = b.reserviert_fuer
|
|
WHERE b.id = ?
|
|
]], { konserveId })
|
|
|
|
return zeilen and zeilen[1] or nil
|
|
end
|
|
|
|
--- Prüft, ob ein Statuswechsel überhaupt vorgesehen ist
|
|
--- @param von string Aktueller Status
|
|
--- @param nach string Gewünschter Status
|
|
--- @return boolean
|
|
local function DarfWechseln(von, nach)
|
|
return (UEBERGAENGE[von] or {})[nach] == true
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- ÜBERSICHT
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('blutbank', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
|
|
local ordner = tostring(daten.ordner or 'vorraetig')
|
|
local vorlauf = Medizin.Config.Erinnerungen.blutVorlaufTage
|
|
|
|
-- Welche Status gehören in diesen Ordner?
|
|
local bedingung, werte
|
|
if ordner == 'reserviert' then
|
|
bedingung = "b.status = 'reserviert'"
|
|
elseif ordner == 'archiv' then
|
|
bedingung = "b.status IN ('ausgegeben', 'verworfen')"
|
|
else
|
|
bedingung = "b.status = 'vorraetig'"
|
|
end
|
|
werte = {}
|
|
|
|
if daten.gruppe and daten.gruppe ~= '' then
|
|
bedingung = bedingung .. ' AND b.blutgruppe = ?'
|
|
werte[#werte + 1] = tostring(daten.gruppe)
|
|
end
|
|
|
|
-- Im Bestand steht die älteste Konserve oben. Das ist eine
|
|
-- Sortierung, keine Vorgabe — welche genommen wird, entscheidet
|
|
-- weiterhin ein Mensch.
|
|
local sortierung = ordner == 'archiv'
|
|
and 'b.id DESC'
|
|
or 'b.verfaellt_am IS NULL, b.verfaellt_am, b.konserve'
|
|
|
|
local liste = MySQL.query.await(([[
|
|
SELECT b.id, b.konserve, b.blutgruppe, b.menge_ml, b.lagerort, b.status,
|
|
b.eingelagert_am, b.eingelagert_von, b.verfaellt_am,
|
|
b.reserviert_fuer, b.reserviert_von,
|
|
b.ausgegeben_von, b.ausgegeben_am,
|
|
DATEDIFF(b.verfaellt_am, CURDATE()) AS tage,
|
|
c.firstname, c.lastname,
|
|
p.blutgruppe AS empfaengerGruppe
|
|
FROM medical_blut b
|
|
LEFT JOIN aether_characters c ON c.id = b.reserviert_fuer
|
|
LEFT JOIN medical_patient p ON p.`character` = b.reserviert_fuer
|
|
WHERE %s
|
|
ORDER BY %s
|
|
LIMIT 200
|
|
]]):format(bedingung, sortierung), werte) or {}
|
|
|
|
for _, eintrag in ipairs(liste) do
|
|
eintrag.tage = eintrag.verfaellt_am and tonumber(eintrag.tage) or nil
|
|
-- Abgelaufen heisst hier nur: das Datum ist vorbei. Sonst nichts.
|
|
eintrag.abgelaufen = eintrag.tage ~= nil and eintrag.tage <= 0
|
|
eintrag.laeuftBaldAb = eintrag.tage ~= nil
|
|
and eintrag.tage > 0 and eintrag.tage <= vorlauf
|
|
|
|
eintrag.empfaenger = eintrag.firstname
|
|
and (eintrag.firstname .. ' ' .. eintrag.lastname) or nil
|
|
|
|
-- Bewusst ohne "and ... or": ein `false` darf hier nicht zu `nil`
|
|
-- werden, sonst verschwindet genau die Abweichung, um die es geht.
|
|
if eintrag.reserviert_fuer then
|
|
eintrag.vertraeglich = Vertraeglich(eintrag.empfaengerGruppe, eintrag.blutgruppe)
|
|
end
|
|
|
|
eintrag.eingelagertVon = NameVon(eintrag.eingelagert_von)
|
|
eintrag.reserviertVon = eintrag.reserviert_von
|
|
and NameVon(eintrag.reserviert_von) or nil
|
|
eintrag.ausgegebenVon = eintrag.ausgegeben_von
|
|
and NameVon(eintrag.ausgegeben_von) or nil
|
|
|
|
eintrag.firstname, eintrag.lastname = nil, nil
|
|
eintrag.eingelagert_von, eintrag.reserviert_von, eintrag.ausgegeben_von =
|
|
nil, nil, nil
|
|
end
|
|
|
|
-- Bestand je Blutgruppe
|
|
local bestand = {}
|
|
for _, gruppe in ipairs(Medizin.Config.Blutgruppen) do
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT status, COUNT(*) AS anzahl
|
|
FROM medical_blut
|
|
WHERE blutgruppe = ? AND status IN ('vorraetig', 'reserviert')
|
|
GROUP BY status
|
|
]], { gruppe }) or {}
|
|
|
|
local eintrag = { gruppe = gruppe, vorraetig = 0, reserviert = 0 }
|
|
for _, zeile in ipairs(zeilen) do
|
|
eintrag[zeile.status] = tonumber(zeile.anzahl) or 0
|
|
end
|
|
|
|
eintrag.baldAb = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_blut
|
|
WHERE blutgruppe = ? AND status IN ('vorraetig', 'reserviert')
|
|
AND verfaellt_am IS NOT NULL
|
|
AND verfaellt_am <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
|
]], { gruppe, vorlauf })) or 0
|
|
|
|
eintrag.knapp = eintrag.vorraetig <= Medizin.Config.Erinnerungen.blutMindestbestand
|
|
bestand[#bestand + 1] = eintrag
|
|
end
|
|
|
|
-- Zählwerte über das ganze Lager
|
|
local function Zaehle(sql, parameter)
|
|
return tonumber(MySQL.scalar.await(sql, parameter)) or 0
|
|
end
|
|
|
|
local zaehler = {
|
|
vorraetig = Zaehle("SELECT COUNT(*) FROM medical_blut WHERE status = 'vorraetig'"),
|
|
reserviert = Zaehle("SELECT COUNT(*) FROM medical_blut WHERE status = 'reserviert'"),
|
|
baldAb = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_blut
|
|
WHERE status IN ('vorraetig', 'reserviert') AND verfaellt_am IS NOT NULL
|
|
AND verfaellt_am <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
|
AND verfaellt_am >= CURDATE()
|
|
]], { vorlauf }),
|
|
abgelaufen = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_blut
|
|
WHERE status IN ('vorraetig', 'reserviert')
|
|
AND verfaellt_am IS NOT NULL AND verfaellt_am < CURDATE()
|
|
]]),
|
|
ausgegebenHeute = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_blut
|
|
WHERE status = 'ausgegeben' AND DATE(ausgegeben_am) = CURDATE()
|
|
]]),
|
|
}
|
|
|
|
return {
|
|
ordner = ordner,
|
|
liste = liste,
|
|
bestand = bestand,
|
|
zaehler = zaehler,
|
|
gruppen = Medizin.Config.Blutgruppen,
|
|
status = Medizin.Config.Blutstatus,
|
|
lagerorte = Medizin.Config.Blutlagerorte,
|
|
vertraeglichkeit = Medizin.Config.Blutvertraeglichkeit,
|
|
vorlaufTage = vorlauf,
|
|
mindestbestand = Medizin.Config.Erinnerungen.blutMindestbestand,
|
|
rechte = {
|
|
verwalten = Rechte.Hat(mitarbeiter, 'blut.verwalten'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EINZELNE KONSERVE
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('blutbank', 'konserve', nil, function(quelle, mitarbeiter, daten)
|
|
local konserveId = tonumber(daten.id)
|
|
if not konserveId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local eintrag = LadeKonserve(konserveId)
|
|
if not eintrag then return nil, 'Konserve nicht gefunden.' end
|
|
|
|
local tage = eintrag.verfaellt_am and tonumber(eintrag.tage) or nil
|
|
|
|
-- Auch hier gilt: `false` ist eine Aussage und darf nicht zu `nil` werden
|
|
local passt = nil
|
|
if eintrag.reserviert_fuer then
|
|
passt = Vertraeglich(eintrag.empfaengerGruppe, eintrag.blutgruppe)
|
|
end
|
|
|
|
-- Der Weg der Konserve steht im Protokoll und wird nur gelesen
|
|
local spur = Protokoll.Lies({ zielTyp = 'blut', zielId = konserveId, limit = 50 })
|
|
|
|
for _, zeile in ipairs(spur) do
|
|
zeile.autor = zeile.firstname
|
|
and (zeile.firstname .. ' ' .. zeile.lastname) or 'System'
|
|
zeile.firstname, zeile.lastname = nil, nil
|
|
zeile.details = nil
|
|
end
|
|
|
|
return {
|
|
konserve = {
|
|
id = eintrag.id, kennung = eintrag.konserve,
|
|
blutgruppe = eintrag.blutgruppe, menge = eintrag.menge_ml,
|
|
lagerort = eintrag.lagerort, status = eintrag.status,
|
|
eingelagertAm = eintrag.eingelagert_am,
|
|
eingelagertVon = NameVon(eintrag.eingelagert_von),
|
|
verfaelltAm = eintrag.verfaellt_am, tage = tage,
|
|
abgelaufen = tage ~= nil and tage <= 0,
|
|
empfaenger = eintrag.reserviert_fuer,
|
|
empfaengerName = eintrag.firstname
|
|
and (eintrag.firstname .. ' ' .. eintrag.lastname) or nil,
|
|
empfaengerGruppe = eintrag.empfaengerGruppe,
|
|
vertraeglich = passt,
|
|
reserviertVon = eintrag.reserviert_von
|
|
and NameVon(eintrag.reserviert_von) or nil,
|
|
ausgegebenAm = eintrag.ausgegeben_am,
|
|
ausgegebenVon = eintrag.ausgegeben_von
|
|
and NameVon(eintrag.ausgegeben_von) or nil,
|
|
},
|
|
spur = spur,
|
|
rechte = {
|
|
verwalten = Rechte.Hat(mitarbeiter, 'blut.verwalten'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EINLAGERN
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('blutbank', 'einlagern', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local gruppe = Blutgruppe(daten.blutgruppe)
|
|
if not gruppe then return nil, 'Unbekannte Blutgruppe.' end
|
|
|
|
local menge = math.floor(tonumber(daten.menge) or 0)
|
|
if menge < 50 or menge > 1000 then
|
|
return nil, 'Eine Konserve fasst zwischen 50 und 1000 ml.'
|
|
end
|
|
|
|
local anzahl = math.floor(tonumber(daten.anzahl) or 1)
|
|
if anzahl < 1 or anzahl > 20 then
|
|
return nil, 'Es lassen sich 1 bis 20 Konserven auf einmal einlagern.'
|
|
end
|
|
|
|
local lagerort = Lagerort(daten.lagerort)
|
|
if not lagerort then return nil, 'Unbekannter Lagerort.' end
|
|
|
|
-- Ein Verfallsdatum ist freiwillig; ohne eines erinnert später nichts
|
|
local verfaellt = Datum(daten.verfaellt)
|
|
if daten.verfaellt and daten.verfaellt ~= '' and not verfaellt then
|
|
return nil, 'Das Verfallsdatum braucht die Form JJJJ-MM-TT.'
|
|
end
|
|
|
|
local angelegt = {}
|
|
|
|
for _ = 1, anzahl do
|
|
local kennung = Kern.ErzeugeKennung('BK', 'medical_blut', 'konserve')
|
|
|
|
local id = MySQL.insert.await([[
|
|
INSERT INTO medical_blut
|
|
(konserve, blutgruppe, menge_ml, lagerort, eingelagert_von, verfaellt_am)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
]], { kennung, gruppe, menge, lagerort, mitarbeiter.charakterId, verfaellt })
|
|
|
|
angelegt[#angelegt + 1] = { id = id, kennung = kennung }
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'blutbank', 'Konserve eingelagert',
|
|
{ typ = 'blut', id = id },
|
|
{ konserve = kennung, blutgruppe = gruppe, menge = menge,
|
|
lagerort = lagerort, verfaellt = verfaellt },
|
|
Text(daten.vermerk, 255))
|
|
end
|
|
|
|
Kern.Loese('blut.eingelagert', gruppe, anzahl, mitarbeiter)
|
|
|
|
return { angelegt = angelegt, anzahl = #angelegt }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- RESERVIEREN
|
|
--
|
|
-- Eine Reservierung legt eine Konserve für einen Patienten beiseite.
|
|
-- Sie transfundiert nichts und verpflichtet zu nichts.
|
|
-- =====================================================================
|
|
|
|
--- Personensuche für die Reservierung
|
|
Kern.RegistriereAktion('blutbank', 'patientSuche', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local begriff = type(daten.begriff) == 'string' and daten.begriff or ''
|
|
if #begriff < 2 then return { treffer = {} } end
|
|
|
|
local muster = '%' .. begriff .. '%'
|
|
local treffer = MySQL.query.await([[
|
|
SELECT c.id, c.firstname, c.lastname, c.dob,
|
|
p.blutgruppe, p.allergien, p.hinweis,
|
|
(SELECT a.status FROM medical_aufnahme a
|
|
WHERE a.patient = c.id AND a.status IN ('wartend', 'aufgenommen')
|
|
LIMIT 1) AS laufend
|
|
FROM aether_characters c
|
|
LEFT JOIN medical_patient p ON p.`character` = c.id
|
|
WHERE c.deleted = 0
|
|
AND (c.firstname LIKE ? OR c.lastname LIKE ?
|
|
OR CONCAT(c.firstname, ' ', c.lastname) LIKE ? OR c.dob LIKE ?)
|
|
ORDER BY c.lastname, c.firstname
|
|
LIMIT 20
|
|
]], { muster, muster, muster, muster }) or {}
|
|
|
|
for _, eintrag in ipairs(treffer) do
|
|
eintrag.dob = tostring(eintrag.dob):sub(1, 10)
|
|
end
|
|
|
|
return { treffer = treffer }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('blutbank', 'reservieren', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local konserveId = tonumber(daten.id)
|
|
local patientId = tonumber(daten.patient)
|
|
|
|
if not konserveId then return nil, 'Ungültige Kennung.' end
|
|
if not patientId then return nil, 'Kein Patient gewählt.' end
|
|
|
|
local eintrag = LadeKonserve(konserveId)
|
|
if not eintrag then return nil, 'Konserve nicht gefunden.' end
|
|
|
|
if not DarfWechseln(eintrag.status, 'reserviert') then
|
|
return nil, ('Eine Konserve im Status "%s" lässt sich nicht reservieren.')
|
|
:format(eintrag.status)
|
|
end
|
|
|
|
local person = MySQL.query.await([[
|
|
SELECT c.firstname, c.lastname, p.blutgruppe
|
|
FROM aether_characters c
|
|
LEFT JOIN medical_patient p ON p.`character` = c.id
|
|
WHERE c.id = ? AND c.deleted = 0
|
|
]], { patientId })
|
|
if not person or not person[1] then return nil, 'Diese Person ist nicht erfasst.' end
|
|
|
|
-- Die Tabelle sagt etwas dazu — entschieden wird trotzdem hier oben
|
|
local passt = Vertraeglich(person[1].blutgruppe, eintrag.blutgruppe)
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_blut
|
|
SET status = 'reserviert', reserviert_fuer = ?, reserviert_von = ?
|
|
WHERE id = ?
|
|
]], { patientId, mitarbeiter.charakterId, konserveId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'blutbank', 'Konserve reserviert',
|
|
{ typ = 'blut', id = konserveId },
|
|
{ konserve = eintrag.konserve, blutgruppe = eintrag.blutgruppe,
|
|
patient = patientId, patientGruppe = person[1].blutgruppe,
|
|
lautTabelleVertraeglich = passt },
|
|
Text(daten.vermerk, 255))
|
|
|
|
-- Eine Abweichung von der Tabelle wird deutlich gemeldet, nicht verhindert
|
|
if passt == false then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'blut', prioritaet = 'hoch',
|
|
titel = 'Reservierung weicht von der Verträglichkeitstabelle ab',
|
|
text = ('%s (%s) für %s %s mit Blutgruppe %s — bitte prüfen.')
|
|
:format(eintrag.konserve, eintrag.blutgruppe,
|
|
person[1].firstname, person[1].lastname, person[1].blutgruppe),
|
|
zielTyp = 'blut', zielId = konserveId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('blut.reserviert', konserveId, patientId, mitarbeiter)
|
|
|
|
return { ok = true, vertraeglich = passt }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('blutbank', 'reservierungLoesen', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local konserveId = tonumber(daten.id)
|
|
if not konserveId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local grund = Text(daten.grund, 255)
|
|
if not grund then return nil, 'Bitte angeben, warum die Reservierung entfällt.' end
|
|
|
|
local eintrag = LadeKonserve(konserveId)
|
|
if not eintrag then return nil, 'Konserve nicht gefunden.' end
|
|
if eintrag.status ~= 'reserviert' then
|
|
return nil, 'Diese Konserve ist nicht reserviert.'
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_blut
|
|
SET status = 'vorraetig', reserviert_fuer = NULL, reserviert_von = NULL
|
|
WHERE id = ?
|
|
]], { konserveId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'blutbank', 'Reservierung aufgehoben',
|
|
{ typ = 'blut', id = konserveId },
|
|
{ konserve = eintrag.konserve, patient = eintrag.reserviert_fuer }, grund)
|
|
|
|
Kern.Loese('blut.freigegeben', konserveId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- AUSGABE
|
|
--
|
|
-- Dokumentiert, dass eine Konserve das Lager verlassen hat. Was mit
|
|
-- ihr geschieht, entscheidet und verantwortet der Arzt am Patienten.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('blutbank', 'ausgeben', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local konserveId = tonumber(daten.id)
|
|
if not konserveId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local eintrag = LadeKonserve(konserveId)
|
|
if not eintrag then return nil, 'Konserve nicht gefunden.' end
|
|
|
|
if not DarfWechseln(eintrag.status, 'ausgegeben') then
|
|
return nil, ('Eine Konserve im Status "%s" lässt sich nicht ausgeben.')
|
|
:format(eintrag.status)
|
|
end
|
|
|
|
-- Ohne Reservierung muss bei der Ausgabe gesagt werden, für wen
|
|
local patientId = tonumber(daten.patient) or eintrag.reserviert_fuer
|
|
if not patientId then return nil, 'Bitte angeben, für welchen Patienten.' end
|
|
|
|
local vermerk = Text(daten.vermerk, 255)
|
|
if not vermerk then return nil, 'Bitte einen Vermerk zur Ausgabe hinterlassen.' end
|
|
|
|
local person = MySQL.query.await([[
|
|
SELECT c.firstname, c.lastname, p.blutgruppe
|
|
FROM aether_characters c
|
|
LEFT JOIN medical_patient p ON p.`character` = c.id
|
|
WHERE c.id = ? AND c.deleted = 0
|
|
]], { patientId })
|
|
if not person or not person[1] then return nil, 'Diese Person ist nicht erfasst.' end
|
|
|
|
local passt = Vertraeglich(person[1].blutgruppe, eintrag.blutgruppe)
|
|
local tage = eintrag.verfaellt_am and tonumber(eintrag.tage) or nil
|
|
local abgelaufen = tage ~= nil and tage <= 0
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_blut
|
|
SET status = 'ausgegeben', reserviert_fuer = ?,
|
|
ausgegeben_von = ?, ausgegeben_am = NOW()
|
|
WHERE id = ?
|
|
]], { patientId, mitarbeiter.charakterId, konserveId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'blutbank', 'Konserve ausgegeben',
|
|
{ typ = 'blut', id = konserveId },
|
|
{ konserve = eintrag.konserve, blutgruppe = eintrag.blutgruppe,
|
|
patient = patientId, patientGruppe = person[1].blutgruppe,
|
|
lautTabelleVertraeglich = passt, abgelaufen = abgelaufen }, vermerk)
|
|
|
|
if passt == false then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'blut', prioritaet = 'kritisch',
|
|
titel = 'Ausgabe weicht von der Verträglichkeitstabelle ab',
|
|
text = ('%s (%s) an %s %s mit Blutgruppe %s — ausgegeben von %s.')
|
|
:format(eintrag.konserve, eintrag.blutgruppe,
|
|
person[1].firstname, person[1].lastname,
|
|
person[1].blutgruppe, mitarbeiter.name),
|
|
zielTyp = 'blut', zielId = konserveId,
|
|
})
|
|
end
|
|
|
|
if abgelaufen then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'blut', prioritaet = 'hoch',
|
|
titel = 'Abgelaufene Konserve ausgegeben',
|
|
text = ('%s war seit %d Tagen über dem Datum — ausgegeben von %s.')
|
|
:format(eintrag.konserve, math.abs(tage), mitarbeiter.name),
|
|
zielTyp = 'blut', zielId = konserveId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('blut.ausgegeben', konserveId, patientId, mitarbeiter)
|
|
|
|
return { ok = true, vertraeglich = passt, abgelaufen = abgelaufen }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- VERWERFEN
|
|
--
|
|
-- Auch abgelaufene Konserven verschwinden nicht von selbst. Ob eine
|
|
-- Konserve noch brauchbar ist, ist eine fachliche Beurteilung —
|
|
-- keine Datumsrechnung.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('blutbank', 'verwerfen', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local konserveId = tonumber(daten.id)
|
|
if not konserveId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local grund = Text(daten.grund, 255)
|
|
if not grund or #grund < 3 then
|
|
return nil, 'Das Verwerfen einer Konserve erfordert eine Begründung.'
|
|
end
|
|
|
|
local eintrag = LadeKonserve(konserveId)
|
|
if not eintrag then return nil, 'Konserve nicht gefunden.' end
|
|
|
|
if not DarfWechseln(eintrag.status, 'verworfen') then
|
|
return nil, ('Eine Konserve im Status "%s" lässt sich nicht verwerfen.')
|
|
:format(eintrag.status)
|
|
end
|
|
|
|
local warReserviert = eintrag.status == 'reserviert'
|
|
|
|
MySQL.update.await("UPDATE medical_blut SET status = 'verworfen' WHERE id = ?",
|
|
{ konserveId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'blutbank', 'Konserve verworfen',
|
|
{ typ = 'blut', id = konserveId },
|
|
{ konserve = eintrag.konserve, blutgruppe = eintrag.blutgruppe,
|
|
vorherigerStatus = eintrag.status,
|
|
verfaellt = eintrag.verfaellt_am }, grund)
|
|
|
|
-- Eine verworfene Reservierung darf niemandem entgehen
|
|
if warReserviert and eintrag.reserviert_fuer then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'blut', prioritaet = 'hoch',
|
|
titel = 'Reservierte Konserve verworfen',
|
|
text = ('%s war für %s %s zurückgelegt.')
|
|
:format(eintrag.konserve, eintrag.firstname or '', eintrag.lastname or ''),
|
|
zielTyp = 'blut', zielId = konserveId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('blut.verworfen', konserveId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- LAGERORT
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('blutbank', 'umlagern', 'blut.verwalten', function(quelle, mitarbeiter, daten)
|
|
local konserveId = tonumber(daten.id)
|
|
if not konserveId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local lagerort = Lagerort(daten.lagerort)
|
|
if not lagerort then return nil, 'Unbekannter Lagerort.' end
|
|
|
|
local eintrag = LadeKonserve(konserveId)
|
|
if not eintrag then return nil, 'Konserve nicht gefunden.' end
|
|
if eintrag.status ~= 'vorraetig' and eintrag.status ~= 'reserviert' then
|
|
return nil, 'Diese Konserve liegt nicht mehr im Lager.'
|
|
end
|
|
|
|
MySQL.update.await('UPDATE medical_blut SET lagerort = ? WHERE id = ?',
|
|
{ lagerort, konserveId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'blutbank', 'Konserve umgelagert',
|
|
{ typ = 'blut', id = konserveId },
|
|
{ konserve = eintrag.konserve, von = eintrag.lagerort, nach = lagerort },
|
|
Text(daten.vermerk, 255))
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Schnittstelle für andere Ressourcen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Liefert den Bestand je Blutgruppe.
|
|
--- Reine Auskunft — keine Ressource kann darüber eine Konserve
|
|
--- reservieren, ausgeben oder verwerfen.
|
|
--- @return table Liste { blutgruppe, vorraetig, reserviert }
|
|
exports('HoleBlutbestand', function()
|
|
local liste = {}
|
|
|
|
for _, gruppe in ipairs(Medizin.Config.Blutgruppen) do
|
|
liste[#liste + 1] = {
|
|
blutgruppe = gruppe,
|
|
vorraetig = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_blut
|
|
WHERE blutgruppe = ? AND status = 'vorraetig'
|
|
]], { gruppe })) or 0,
|
|
reserviert = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_blut
|
|
WHERE blutgruppe = ? AND status = 'reserviert'
|
|
]], { gruppe })) or 0,
|
|
}
|
|
end
|
|
|
|
return liste
|
|
end)
|