- 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>
823 lines
33 KiB
Lua
823 lines
33 KiB
Lua
-- =====================================================================
|
|
-- aether-medical / Modul: Stationen und Zimmerbelegung
|
|
--
|
|
-- Der Bettenspiegel zeigt, wer wo liegt und was frei ist. Mehr nicht.
|
|
--
|
|
-- Das System belegt kein Bett, gibt keines frei, schiebt niemanden von
|
|
-- einem Zimmer ins andere und setzt ein Zimmer nach der Entlassung
|
|
-- nicht von selbst wieder auf "frei". Jede Zeile hier entsteht, weil
|
|
-- ein Mensch sie ausgelöst hat — und trägt dessen Namen.
|
|
--
|
|
-- Auch die Reinigung ist eine Entscheidung: ein leer gewordenes
|
|
-- Zimmer meldet sich, bleibt aber in dem Status, in dem es war.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereModul({
|
|
id = 'station', name = 'Stationen', icon = '🛏',
|
|
reihenfolge = 9, recht = 'station.sehen',
|
|
beschreibung = 'Bettenspiegel, Zimmerstatus und Stationsverwaltung',
|
|
})
|
|
|
|
-- Zimmerstatus, die von Hand gesetzt werden dürfen
|
|
local ZIMMERSTATUS = {
|
|
frei = true,
|
|
reinigung = true,
|
|
gesperrt = true,
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- 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 Mitarbeiters 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 Kennung aus einer Konfigurationsliste
|
|
--- @param liste table Liste aus Medizin.Config
|
|
--- @param wert any Eingabe
|
|
--- @return string|nil Gültige Kennung
|
|
local function AusListe(liste, wert)
|
|
local gesucht = tostring(wert or '')
|
|
for _, eintrag in ipairs(liste) do
|
|
if eintrag.id == gesucht then return eintrag.id end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Wie viele Betten eines Zimmers sind gerade belegt?
|
|
--- @param zimmerId number Zimmer
|
|
--- @return number
|
|
local function BelegteBetten(zimmerId)
|
|
return tonumber(MySQL.scalar.await(
|
|
'SELECT COUNT(*) FROM medical_belegung WHERE zimmer = ? AND frei_am IS NULL',
|
|
{ zimmerId })) or 0
|
|
end
|
|
|
|
--- Liefert die Nummern der belegten Betten eines Zimmers
|
|
--- @param zimmerId number Zimmer
|
|
--- @return table { [bettNummer] = true }
|
|
local function BelegteBettNummern(zimmerId)
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT bett FROM medical_belegung WHERE zimmer = ? AND frei_am IS NULL',
|
|
{ zimmerId }) or {}
|
|
|
|
local genommen = {}
|
|
for _, eintrag in ipairs(zeilen) do genommen[tonumber(eintrag.bett)] = true end
|
|
return genommen
|
|
end
|
|
|
|
--- Sucht das erste freie Bett eines Zimmers
|
|
--- @param zimmerId number Zimmer
|
|
--- @param betten number Bettenzahl des Zimmers
|
|
--- @return number|nil Bettnummer
|
|
local function FreiesBett(zimmerId, betten)
|
|
local genommen = BelegteBettNummern(zimmerId)
|
|
for nummer = 1, betten do
|
|
if not genommen[nummer] then return nummer end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Meldet ein leer gewordenes Zimmer — ohne seinen Status anzufassen
|
|
--- @param zimmerId number Zimmer
|
|
--- @param kennung string Zimmerkennung für den Text
|
|
local function MeldeWennLeer(zimmerId, kennung)
|
|
if BelegteBetten(zimmerId) > 0 then return end
|
|
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'patient', prioritaet = 'niedrig',
|
|
titel = 'Zimmer wurde frei',
|
|
text = ('Zimmer %s ist jetzt leer. Reinigung und Freigabe bleiben eine Entscheidung.')
|
|
:format(kennung),
|
|
zielTyp = 'zimmer', zielId = zimmerId,
|
|
})
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- BETTENSPIEGEL
|
|
--
|
|
-- Eine reine Auskunft: welche Station, welches Zimmer, welches Bett,
|
|
-- wer liegt darin. Das System bewertet die Belegung nicht und schlägt
|
|
-- keine Verteilung vor.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
|
|
local darfVerwalten = Rechte.Hat(mitarbeiter, 'station.verwalten')
|
|
-- Abgeschaltete Stationen sieht nur, wer sie auch pflegen darf
|
|
local zeigeInaktive = darfVerwalten and daten.alle == true
|
|
|
|
local stationen = MySQL.query.await(([[
|
|
SELECT id, kennung, name, art, etage, aktiv, sortierung
|
|
FROM medical_station
|
|
%s
|
|
ORDER BY sortierung, kennung
|
|
]]):format(zeigeInaktive and '' or 'WHERE aktiv = 1')) or {}
|
|
|
|
local zimmerListe = MySQL.query.await([[
|
|
SELECT id, station, kennung, betten, art, status, hinweis
|
|
FROM medical_zimmer
|
|
ORDER BY kennung
|
|
]]) or {}
|
|
|
|
-- Alle laufenden Belegungen in einem Zug — je Zimmer zugeordnet
|
|
local belegungen = MySQL.query.await([[
|
|
SELECT b.id, b.zimmer, b.bett, b.patient, b.aufnahme,
|
|
b.belegt_am, b.belegt_von,
|
|
TIMESTAMPDIFF(MINUTE, b.belegt_am, NOW()) AS liegtSeit,
|
|
c.firstname, c.lastname,
|
|
a.status AS aufnahmeStatus, a.prioritaet, a.grund,
|
|
p.blutgruppe, p.allergien, p.hinweis AS warnhinweis,
|
|
(SELECT COUNT(*) FROM medical_intensiv i
|
|
WHERE i.aufnahme = b.aufnahme AND i.beendet_am IS NULL) AS aufIntensiv
|
|
FROM medical_belegung b
|
|
JOIN aether_characters c ON c.id = b.patient
|
|
LEFT JOIN medical_aufnahme a ON a.id = b.aufnahme
|
|
LEFT JOIN medical_patient p ON p.`character` = b.patient
|
|
WHERE b.frei_am IS NULL
|
|
]]) or {}
|
|
|
|
-- Belegungen nach Zimmer sortieren, damit die Betten schnell zu füllen sind
|
|
local belegungNachZimmer = {}
|
|
for _, belegung in ipairs(belegungen) do
|
|
belegung.name = belegung.firstname .. ' ' .. belegung.lastname
|
|
belegung.belegtVon = NameVon(belegung.belegt_von)
|
|
belegung.liegtSeit = tonumber(belegung.liegtSeit) or 0
|
|
belegung.aufIntensiv = (tonumber(belegung.aufIntensiv) or 0) > 0
|
|
belegung.firstname, belegung.lastname, belegung.belegt_von = nil, nil, nil
|
|
|
|
local zimmerId = tonumber(belegung.zimmer)
|
|
belegungNachZimmer[zimmerId] = belegungNachZimmer[zimmerId] or {}
|
|
belegungNachZimmer[zimmerId][tonumber(belegung.bett)] = belegung
|
|
end
|
|
|
|
-- Zimmer den Stationen zuordnen und jedes Bett einzeln aufführen
|
|
local zimmerNachStation = {}
|
|
for _, zimmer in ipairs(zimmerListe) do
|
|
zimmer.betten = tonumber(zimmer.betten) or 1
|
|
|
|
local imZimmer = belegungNachZimmer[tonumber(zimmer.id)] or {}
|
|
zimmer.bettenListe = {}
|
|
|
|
for nummer = 1, zimmer.betten do
|
|
zimmer.bettenListe[nummer] = {
|
|
nummer = nummer,
|
|
belegung = imZimmer[nummer],
|
|
}
|
|
end
|
|
|
|
zimmer.belegt = 0
|
|
for _ in pairs(imZimmer) do zimmer.belegt = zimmer.belegt + 1 end
|
|
zimmer.frei = math.max(0, zimmer.betten - zimmer.belegt)
|
|
-- Verfügbar heisst: nicht gesperrt, nicht in Reinigung, noch Platz
|
|
zimmer.verfuegbar = zimmer.status == 'frei' and zimmer.frei > 0
|
|
|
|
local stationId = tonumber(zimmer.station)
|
|
zimmerNachStation[stationId] = zimmerNachStation[stationId] or {}
|
|
table.insert(zimmerNachStation[stationId], zimmer)
|
|
end
|
|
|
|
-- Zählwerte über das ganze Haus
|
|
local zaehler = {
|
|
betten = 0, belegt = 0, frei = 0,
|
|
zimmer = 0, gesperrt = 0, reinigung = 0,
|
|
}
|
|
|
|
for _, station in ipairs(stationen) do
|
|
station.zimmer = zimmerNachStation[tonumber(station.id)] or {}
|
|
station.aktiv = tonumber(station.aktiv) == 1
|
|
|
|
station.betten, station.belegt, station.frei = 0, 0, 0
|
|
for _, zimmer in ipairs(station.zimmer) do
|
|
station.betten = station.betten + zimmer.betten
|
|
station.belegt = station.belegt + zimmer.belegt
|
|
-- Als frei zählt nur, was auch belegt werden dürfte
|
|
if zimmer.status == 'frei' then station.frei = station.frei + zimmer.frei end
|
|
|
|
zaehler.zimmer = zaehler.zimmer + 1
|
|
if zimmer.status == 'gesperrt' then zaehler.gesperrt = zaehler.gesperrt + 1 end
|
|
if zimmer.status == 'reinigung' then zaehler.reinigung = zaehler.reinigung + 1 end
|
|
end
|
|
|
|
zaehler.betten = zaehler.betten + station.betten
|
|
zaehler.belegt = zaehler.belegt + station.belegt
|
|
zaehler.frei = zaehler.frei + station.frei
|
|
end
|
|
|
|
-- Aufgenommene Patienten, die in keinem Bett liegen.
|
|
-- Das ist kein Fehler — es wird nur sichtbar gemacht.
|
|
local ohneBett = MySQL.query.await([[
|
|
SELECT a.id, a.patient, a.station, a.grund, a.prioritaet, a.aufgenommen_am,
|
|
TIMESTAMPDIFF(MINUTE, a.aufgenommen_am, NOW()) AS wartet,
|
|
c.firstname, c.lastname,
|
|
s.name AS stationName,
|
|
p.blutgruppe, p.allergien, p.hinweis AS warnhinweis
|
|
FROM medical_aufnahme a
|
|
JOIN aether_characters c ON c.id = a.patient
|
|
LEFT JOIN medical_station s ON s.kennung = a.station
|
|
LEFT JOIN medical_patient p ON p.`character` = a.patient
|
|
WHERE a.status = 'aufgenommen'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM medical_belegung b
|
|
WHERE b.aufnahme = a.id AND b.frei_am IS NULL)
|
|
ORDER BY a.prioritaet, a.aufgenommen_am
|
|
LIMIT 100
|
|
]]) or {}
|
|
|
|
for _, eintrag in ipairs(ohneBett) do
|
|
eintrag.name = eintrag.firstname .. ' ' .. eintrag.lastname
|
|
eintrag.wartet = tonumber(eintrag.wartet) or 0
|
|
eintrag.firstname, eintrag.lastname = nil, nil
|
|
end
|
|
|
|
return {
|
|
stationen = stationen,
|
|
ohneBett = ohneBett,
|
|
zaehler = zaehler,
|
|
stationsarten = Medizin.Config.Stationsarten,
|
|
zimmerarten = Medizin.Config.Zimmerarten,
|
|
zimmerstatus = Medizin.Config.Zimmerstatus,
|
|
rechte = {
|
|
belegen = Rechte.Hat(mitarbeiter, 'station.belegen'),
|
|
verwalten = darfVerwalten,
|
|
verlegen = Rechte.Hat(mitarbeiter, 'patient.verlegen'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EINZELNES ZIMMER
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'zimmer', nil, function(quelle, mitarbeiter, daten)
|
|
local zimmerId = tonumber(daten.id)
|
|
if not zimmerId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT z.id, z.kennung, z.betten, z.art, z.status, z.hinweis,
|
|
s.id AS stationId, s.kennung AS station, s.name AS stationName,
|
|
s.art AS stationArt, s.etage
|
|
FROM medical_zimmer z
|
|
JOIN medical_station s ON s.id = z.station
|
|
WHERE z.id = ?
|
|
]], { zimmerId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Zimmer nicht gefunden.' end
|
|
|
|
local zimmer = zeilen[1]
|
|
zimmer.betten = tonumber(zimmer.betten) or 1
|
|
|
|
-- Wer liegt gerade darin?
|
|
local aktuell = MySQL.query.await([[
|
|
SELECT b.id, b.bett, b.patient, b.aufnahme, b.belegt_am, b.belegt_von,
|
|
TIMESTAMPDIFF(MINUTE, b.belegt_am, NOW()) AS liegtSeit,
|
|
c.firstname, c.lastname, c.dob,
|
|
a.status AS aufnahmeStatus, a.prioritaet, a.grund,
|
|
p.blutgruppe, p.allergien, p.hinweis AS warnhinweis
|
|
FROM medical_belegung b
|
|
JOIN aether_characters c ON c.id = b.patient
|
|
LEFT JOIN medical_aufnahme a ON a.id = b.aufnahme
|
|
LEFT JOIN medical_patient p ON p.`character` = b.patient
|
|
WHERE b.zimmer = ? AND b.frei_am IS NULL
|
|
ORDER BY b.bett
|
|
]], { zimmerId }) or {}
|
|
|
|
-- Betten einzeln aufführen, damit auch die leeren sichtbar bleiben
|
|
local nachBett = {}
|
|
for _, belegung in ipairs(aktuell) do
|
|
belegung.name = belegung.firstname .. ' ' .. belegung.lastname
|
|
belegung.geburt = tostring(belegung.dob):sub(1, 10)
|
|
belegung.belegtVon = NameVon(belegung.belegt_von)
|
|
belegung.liegtSeit = tonumber(belegung.liegtSeit) or 0
|
|
belegung.firstname, belegung.lastname, belegung.dob = nil, nil, nil
|
|
belegung.belegt_von = nil
|
|
nachBett[tonumber(belegung.bett)] = belegung
|
|
end
|
|
|
|
local bettenListe = {}
|
|
for nummer = 1, zimmer.betten do
|
|
bettenListe[nummer] = { nummer = nummer, belegung = nachBett[nummer] }
|
|
end
|
|
|
|
-- Frühere Belegungen — die Spur bleibt, auch wenn das Bett längst frei ist
|
|
local verlauf = MySQL.query.await([[
|
|
SELECT b.bett, b.belegt_am, b.belegt_von, b.frei_am, b.frei_von,
|
|
c.firstname, c.lastname
|
|
FROM medical_belegung b
|
|
JOIN aether_characters c ON c.id = b.patient
|
|
WHERE b.zimmer = ? AND b.frei_am IS NOT NULL
|
|
ORDER BY b.frei_am DESC
|
|
LIMIT 25
|
|
]], { zimmerId }) or {}
|
|
|
|
for _, eintrag in ipairs(verlauf) do
|
|
eintrag.name = eintrag.firstname .. ' ' .. eintrag.lastname
|
|
eintrag.belegtVon = NameVon(eintrag.belegt_von)
|
|
eintrag.freiVon = NameVon(eintrag.frei_von)
|
|
eintrag.firstname, eintrag.lastname = nil, nil
|
|
eintrag.belegt_von, eintrag.frei_von = nil, nil
|
|
end
|
|
|
|
return {
|
|
zimmer = zimmer,
|
|
betten = bettenListe,
|
|
verlauf = verlauf,
|
|
zimmerarten = Medizin.Config.Zimmerarten,
|
|
zimmerstatus = Medizin.Config.Zimmerstatus,
|
|
rechte = {
|
|
belegen = Rechte.Hat(mitarbeiter, 'station.belegen'),
|
|
verwalten = Rechte.Hat(mitarbeiter, 'station.verwalten'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- BETT ZUWEISEN
|
|
--
|
|
-- Nur für aufgenommene Patienten, die in keinem Bett liegen. Wer
|
|
-- bereits eines hat, wird über die Patientenverwaltung verlegt —
|
|
-- damit eine Verlegung auch als Verlegung dokumentiert ist.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'bettZuweisen', 'station.belegen', function(quelle, mitarbeiter, daten)
|
|
local aufnahmeId = tonumber(daten.aufnahme)
|
|
local zimmerId = tonumber(daten.zimmer)
|
|
|
|
if not aufnahmeId then return nil, 'Kein Patient gewählt.' end
|
|
if not zimmerId then return nil, 'Kein Zimmer gewählt.' end
|
|
|
|
local aufnahmen = MySQL.query.await([[
|
|
SELECT a.id, a.patient, a.status, a.station, a.zimmer,
|
|
c.firstname, c.lastname
|
|
FROM medical_aufnahme a
|
|
JOIN aether_characters c ON c.id = a.patient
|
|
WHERE a.id = ?
|
|
]], { aufnahmeId })
|
|
if not aufnahmen or not aufnahmen[1] then return nil, 'Aufnahme nicht gefunden.' end
|
|
|
|
local aufnahme = aufnahmen[1]
|
|
if aufnahme.status ~= 'aufgenommen' then
|
|
return nil, 'Nur aufgenommene Patienten bekommen ein Bett.'
|
|
end
|
|
|
|
-- Doppelbelegung ausschliessen
|
|
local schonBelegt = MySQL.scalar.await(
|
|
'SELECT id FROM medical_belegung WHERE aufnahme = ? AND frei_am IS NULL', { aufnahmeId })
|
|
if schonBelegt then
|
|
return nil, 'Der Patient liegt bereits in einem Bett. Eine Verlegung nimmt die Patientenverwaltung vor.'
|
|
end
|
|
|
|
local zimmerZeilen = MySQL.query.await([[
|
|
SELECT z.id, z.kennung, z.betten, z.art, z.status,
|
|
s.kennung AS station, s.name AS stationName, s.aktiv AS stationAktiv
|
|
FROM medical_zimmer z
|
|
JOIN medical_station s ON s.id = z.station
|
|
WHERE z.id = ?
|
|
]], { zimmerId })
|
|
if not zimmerZeilen or not zimmerZeilen[1] then return nil, 'Unbekanntes Zimmer.' end
|
|
|
|
local zimmer = zimmerZeilen[1]
|
|
if tonumber(zimmer.stationAktiv) ~= 1 then
|
|
return nil, 'Diese Station ist abgeschaltet.'
|
|
end
|
|
if zimmer.status ~= 'frei' then
|
|
return nil, ('Zimmer %s steht nicht zur Verfügung (%s).'):format(zimmer.kennung, zimmer.status)
|
|
end
|
|
|
|
-- Ein Bett auf einer anderen Station ist eine Verlegung und braucht das Recht dazu
|
|
local wechseltStation = aufnahme.station ~= nil and aufnahme.station ~= zimmer.station
|
|
if wechseltStation and not Rechte.Hat(mitarbeiter, 'patient.verlegen') then
|
|
return nil, 'Dieses Bett liegt auf einer anderen Station — dafür fehlt dir das Recht zur Verlegung.'
|
|
end
|
|
|
|
-- Bettwunsch prüfen, sonst das erste freie Bett nehmen
|
|
local bettNummer = tonumber(daten.bett)
|
|
local betten = tonumber(zimmer.betten) or 1
|
|
|
|
if bettNummer then
|
|
if bettNummer < 1 or bettNummer > betten then
|
|
return nil, ('Zimmer %s hat kein Bett %d.'):format(zimmer.kennung, bettNummer)
|
|
end
|
|
if BelegteBettNummern(zimmerId)[bettNummer] then
|
|
return nil, ('Bett %d ist belegt.'):format(bettNummer)
|
|
end
|
|
else
|
|
bettNummer = FreiesBett(zimmerId, betten)
|
|
if not bettNummer then
|
|
return nil, ('In Zimmer %s ist kein Bett mehr frei.'):format(zimmer.kennung)
|
|
end
|
|
end
|
|
|
|
MySQL.insert.await([[
|
|
INSERT INTO medical_belegung (zimmer, bett, patient, aufnahme, belegt_von)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
]], { zimmerId, bettNummer, aufnahme.patient, aufnahmeId, mitarbeiter.charakterId })
|
|
|
|
-- Die Aufnahme führt mit, wo der Patient liegt
|
|
MySQL.update.await('UPDATE medical_aufnahme SET station = ?, zimmer = ? WHERE id = ?',
|
|
{ zimmer.station, zimmerId, aufnahmeId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Bett zugewiesen',
|
|
{ typ = 'zimmer', id = zimmerId },
|
|
{ aufnahme = aufnahmeId, patient = aufnahme.patient,
|
|
zimmer = zimmer.kennung, bett = bettNummer,
|
|
vonStation = aufnahme.station, nachStation = zimmer.station },
|
|
Text(daten.vermerk, 255))
|
|
|
|
-- Ein Stationswechsel wird zusätzlich gemeldet, damit ihn niemand übersieht
|
|
if wechseltStation then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'patient', prioritaet = 'normal',
|
|
titel = 'Patient auf andere Station gelegt',
|
|
text = ('%s %s liegt jetzt auf %s, Zimmer %s.')
|
|
:format(aufnahme.firstname, aufnahme.lastname, zimmer.stationName, zimmer.kennung),
|
|
zielTyp = 'aufnahme', zielId = aufnahmeId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('belegung.zugewiesen', aufnahmeId, zimmerId, bettNummer, mitarbeiter)
|
|
|
|
return { ok = true, zimmer = zimmer.kennung, bett = bettNummer }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- BETT FREIGEBEN
|
|
--
|
|
-- Gibt nur das Bett frei — der Patient bleibt aufgenommen. Entlassen
|
|
-- wird ausschliesslich über die Patientenverwaltung.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'bettFreigeben', 'station.belegen', function(quelle, mitarbeiter, daten)
|
|
local belegungId = tonumber(daten.id)
|
|
if not belegungId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local grund = Text(daten.grund, 255)
|
|
if not grund or #grund < 3 then
|
|
return nil, 'Bitte angeben, warum das Bett frei wird.'
|
|
end
|
|
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT b.id, b.zimmer, b.bett, b.patient, b.aufnahme, b.frei_am,
|
|
z.kennung AS zimmerKennung,
|
|
c.firstname, c.lastname
|
|
FROM medical_belegung b
|
|
JOIN medical_zimmer z ON z.id = b.zimmer
|
|
JOIN aether_characters c ON c.id = b.patient
|
|
WHERE b.id = ?
|
|
]], { belegungId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Belegung nicht gefunden.' end
|
|
|
|
local belegung = zeilen[1]
|
|
if belegung.frei_am then return nil, 'Dieses Bett ist bereits frei.' end
|
|
|
|
MySQL.update.await(
|
|
'UPDATE medical_belegung SET frei_am = NOW(), frei_von = ? WHERE id = ?',
|
|
{ mitarbeiter.charakterId, belegungId })
|
|
|
|
-- Die Aufnahme verliert ihr Zimmer, bleibt aber auf ihrer Station
|
|
if belegung.aufnahme then
|
|
MySQL.update.await(
|
|
'UPDATE medical_aufnahme SET zimmer = NULL WHERE id = ? AND zimmer = ?',
|
|
{ belegung.aufnahme, belegung.zimmer })
|
|
end
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Bett freigegeben',
|
|
{ typ = 'zimmer', id = belegung.zimmer },
|
|
{ belegung = belegungId, patient = belegung.patient,
|
|
zimmer = belegung.zimmerKennung, bett = belegung.bett,
|
|
aufnahme = belegung.aufnahme }, grund)
|
|
|
|
-- Der Patient ist weiterhin im Haus — das darf nicht untergehen
|
|
if belegung.aufnahme then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'patient', prioritaet = 'normal',
|
|
titel = 'Patient ohne Bett',
|
|
text = ('%s %s ist weiterhin aufgenommen, liegt aber in keinem Zimmer mehr.')
|
|
:format(belegung.firstname, belegung.lastname),
|
|
zielTyp = 'aufnahme', zielId = belegung.aufnahme,
|
|
})
|
|
end
|
|
|
|
MeldeWennLeer(belegung.zimmer, belegung.zimmerKennung)
|
|
Kern.Loese('belegung.freigegeben', belegungId, belegung.zimmer, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- ZIMMERSTATUS
|
|
--
|
|
-- Reinigung und Sperrung setzt immer ein Mensch. Ein leer gewordenes
|
|
-- Zimmer wird nicht von selbst wieder als frei gemeldet.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'zimmerStatus', 'station.belegen', function(quelle, mitarbeiter, daten)
|
|
local zimmerId = tonumber(daten.id)
|
|
local status = tostring(daten.status or '')
|
|
|
|
if not zimmerId then return nil, 'Ungültige Kennung.' end
|
|
if not ZIMMERSTATUS[status] then return nil, 'Unbekannter Zimmerstatus.' end
|
|
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, kennung, status, hinweis FROM medical_zimmer WHERE id = ?', { zimmerId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Zimmer nicht gefunden.' end
|
|
|
|
local zimmer = zeilen[1]
|
|
|
|
-- Ein belegtes Zimmer lässt sich nicht sperren, ohne die Belegung zu klären
|
|
if status == 'gesperrt' and BelegteBetten(zimmerId) > 0 then
|
|
return nil, 'In diesem Zimmer liegen noch Patienten.'
|
|
end
|
|
|
|
-- Ein leerer Hinweis löscht den alten; ein fehlendes Feld lässt ihn stehen
|
|
local hinweis = zimmer.hinweis
|
|
if daten.hinweis ~= nil then hinweis = Text(daten.hinweis, 255) end
|
|
|
|
MySQL.update.await('UPDATE medical_zimmer SET status = ?, hinweis = ? WHERE id = ?',
|
|
{ status, hinweis, zimmerId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Zimmerstatus gesetzt',
|
|
{ typ = 'zimmer', id = zimmerId },
|
|
{ kennung = zimmer.kennung, von = zimmer.status, nach = status }, hinweis)
|
|
|
|
Kern.Loese('zimmer.status', zimmerId, status, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- STATIONSVERWALTUNG
|
|
--
|
|
-- Stammdatenpflege. Eine Station wird abgeschaltet, nie gelöscht —
|
|
-- sonst verlieren alte Aufnahmen ihren Ort.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'stationSpeichern', 'station.verwalten', function(quelle, mitarbeiter, daten)
|
|
local stationId = tonumber(daten.id)
|
|
|
|
local kennung = Text(daten.kennung, 24)
|
|
local name = Text(daten.name, 120)
|
|
local art = AusListe(Medizin.Config.Stationsarten, daten.art)
|
|
|
|
if not kennung then return nil, 'Bitte eine Kennung angeben.' end
|
|
if not name then return nil, 'Bitte einen Namen angeben.' end
|
|
if not art then return nil, 'Unbekannte Stationsart.' end
|
|
|
|
kennung = kennung:upper()
|
|
|
|
-- Kennungen müssen eindeutig bleiben, weil Aufnahmen darauf verweisen
|
|
local kollision = MySQL.scalar.await(
|
|
'SELECT id FROM medical_station WHERE kennung = ? AND id <> ?',
|
|
{ kennung, stationId or 0 })
|
|
if kollision then return nil, ('Die Kennung %s ist bereits vergeben.'):format(kennung) end
|
|
|
|
local etage = Text(daten.etage, 24)
|
|
local sortierung = math.floor(tonumber(daten.sortierung) or 0)
|
|
|
|
if stationId then
|
|
local vorher = MySQL.query.await(
|
|
'SELECT kennung FROM medical_station WHERE id = ?', { stationId })
|
|
if not vorher or not vorher[1] then return nil, 'Station nicht gefunden.' end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_station
|
|
SET kennung = ?, name = ?, art = ?, etage = ?, sortierung = ?
|
|
WHERE id = ?
|
|
]], { kennung, name, art, etage, sortierung, stationId })
|
|
|
|
-- Aufnahmen verweisen über die Kennung — die muss mitwandern
|
|
if vorher[1].kennung ~= kennung then
|
|
MySQL.update.await('UPDATE medical_aufnahme SET station = ? WHERE station = ?',
|
|
{ kennung, vorher[1].kennung })
|
|
end
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Station bearbeitet',
|
|
{ typ = 'station', id = stationId },
|
|
{ kennung = kennung, name = name, art = art, etage = etage })
|
|
else
|
|
stationId = MySQL.insert.await([[
|
|
INSERT INTO medical_station (kennung, name, art, etage, sortierung)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
]], { kennung, name, art, etage, sortierung })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Station angelegt',
|
|
{ typ = 'station', id = stationId },
|
|
{ kennung = kennung, name = name, art = art, etage = etage })
|
|
end
|
|
|
|
Kern.Loese('station.geaendert', stationId, mitarbeiter)
|
|
|
|
return { id = stationId }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('station', 'stationAktiv', 'station.verwalten', function(quelle, mitarbeiter, daten)
|
|
local stationId = tonumber(daten.id)
|
|
if not stationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, kennung, name, aktiv FROM medical_station WHERE id = ?', { stationId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Station nicht gefunden.' end
|
|
|
|
local aktiv = daten.aktiv == true
|
|
|
|
-- Eine Station mit Patienten darin wird nicht abgeschaltet
|
|
if not aktiv then
|
|
local belegt = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_belegung b
|
|
JOIN medical_zimmer z ON z.id = b.zimmer
|
|
WHERE z.station = ? AND b.frei_am IS NULL
|
|
]], { stationId })) or 0
|
|
|
|
if belegt > 0 then
|
|
return nil, ('Auf dieser Station liegen noch %d Patient(en).'):format(belegt)
|
|
end
|
|
end
|
|
|
|
MySQL.update.await('UPDATE medical_station SET aktiv = ? WHERE id = ?',
|
|
{ aktiv and 1 or 0, stationId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station',
|
|
aktiv and 'Station eingeschaltet' or 'Station abgeschaltet',
|
|
{ typ = 'station', id = stationId },
|
|
{ kennung = zeilen[1].kennung }, Text(daten.grund, 255))
|
|
|
|
Kern.Loese('station.geaendert', stationId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- ZIMMERVERWALTUNG
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('station', 'zimmerSpeichern', 'station.verwalten', function(quelle, mitarbeiter, daten)
|
|
local zimmerId = tonumber(daten.id)
|
|
local stationId = tonumber(daten.station)
|
|
|
|
local kennung = Text(daten.kennung, 24)
|
|
local art = AusListe(Medizin.Config.Zimmerarten, daten.art)
|
|
local betten = math.floor(tonumber(daten.betten) or 0)
|
|
|
|
if not kennung then return nil, 'Bitte eine Kennung angeben.' end
|
|
if not art then return nil, 'Unbekannte Zimmerart.' end
|
|
if betten < 1 or betten > 12 then return nil, 'Ein Zimmer hat 1 bis 12 Betten.' end
|
|
|
|
kennung = kennung:upper()
|
|
|
|
if zimmerId then
|
|
local vorher = MySQL.query.await(
|
|
'SELECT id, station, betten FROM medical_zimmer WHERE id = ?', { zimmerId })
|
|
if not vorher or not vorher[1] then return nil, 'Zimmer nicht gefunden.' end
|
|
|
|
-- Die Station eines bestehenden Zimmers bleibt, wie sie ist
|
|
stationId = tonumber(vorher[1].station)
|
|
|
|
-- Betten lassen sich nicht wegkürzen, solange jemand darin liegt
|
|
local genommen = BelegteBettNummern(zimmerId)
|
|
for nummer in pairs(genommen) do
|
|
if nummer > betten then
|
|
return nil, ('Bett %d ist belegt — die Bettenzahl lässt sich nicht verringern.')
|
|
:format(nummer)
|
|
end
|
|
end
|
|
end
|
|
|
|
if not stationId then return nil, 'Keine Station gewählt.' end
|
|
|
|
local station = MySQL.query.await(
|
|
'SELECT id, kennung, name FROM medical_station WHERE id = ?', { stationId })
|
|
if not station or not station[1] then return nil, 'Unbekannte Station.' end
|
|
|
|
-- Innerhalb einer Station soll jede Zimmerkennung nur einmal vorkommen
|
|
local kollision = MySQL.scalar.await(
|
|
'SELECT id FROM medical_zimmer WHERE station = ? AND kennung = ? AND id <> ?',
|
|
{ stationId, kennung, zimmerId or 0 })
|
|
if kollision then
|
|
return nil, ('Zimmer %s gibt es auf dieser Station bereits.'):format(kennung)
|
|
end
|
|
|
|
local hinweis = Text(daten.hinweis, 255)
|
|
|
|
if zimmerId then
|
|
MySQL.update.await([[
|
|
UPDATE medical_zimmer SET kennung = ?, betten = ?, art = ?, hinweis = ?
|
|
WHERE id = ?
|
|
]], { kennung, betten, art, hinweis, zimmerId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Zimmer bearbeitet',
|
|
{ typ = 'zimmer', id = zimmerId },
|
|
{ kennung = kennung, betten = betten, art = art, station = station[1].kennung })
|
|
else
|
|
-- Neue Zimmer starten gesperrt: erst freigeben, dann belegen
|
|
zimmerId = MySQL.insert.await([[
|
|
INSERT INTO medical_zimmer (station, kennung, betten, art, status, hinweis)
|
|
VALUES (?, ?, ?, ?, 'gesperrt', ?)
|
|
]], { stationId, kennung, betten, art, hinweis })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Zimmer angelegt',
|
|
{ typ = 'zimmer', id = zimmerId },
|
|
{ kennung = kennung, betten = betten, art = art, station = station[1].kennung })
|
|
end
|
|
|
|
Kern.Loese('zimmer.geaendert', zimmerId, mitarbeiter)
|
|
|
|
return { id = zimmerId }
|
|
end)
|
|
|
|
--- Entfernt ein Zimmer, das nie benutzt wurde.
|
|
--- Sobald jemand darin gelegen hat, bleibt es bestehen — sonst verlöre
|
|
--- der Belegungsverlauf seinen Ort. Dann hilft nur noch "gesperrt".
|
|
Kern.RegistriereAktion('station', 'zimmerEntfernen', 'station.verwalten', function(quelle, mitarbeiter, daten)
|
|
local zimmerId = tonumber(daten.id)
|
|
if not zimmerId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, kennung, station FROM medical_zimmer WHERE id = ?', { zimmerId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Zimmer nicht gefunden.' end
|
|
|
|
local benutzt = tonumber(MySQL.scalar.await(
|
|
'SELECT COUNT(*) FROM medical_belegung WHERE zimmer = ?', { zimmerId })) or 0
|
|
if benutzt > 0 then
|
|
return nil, 'In diesem Zimmer lagen bereits Patienten — es lässt sich nur noch sperren.'
|
|
end
|
|
|
|
-- Auch ein nie belegtes Zimmer kann in einer laufenden OP-Planung stehen
|
|
local imOpPlan = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_operation
|
|
WHERE saal = ? AND status IN ('geplant', 'laufend')
|
|
]], { zimmerId })) or 0
|
|
if imOpPlan > 0 then
|
|
return nil, 'Für diesen Saal sind noch Operationen geplant.'
|
|
end
|
|
|
|
MySQL.query.await('DELETE FROM medical_zimmer WHERE id = ?', { zimmerId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'station', 'Zimmer entfernt',
|
|
{ typ = 'zimmer', id = zimmerId },
|
|
{ kennung = zeilen[1].kennung }, Text(daten.grund, 255))
|
|
|
|
Kern.Loese('zimmer.geaendert', zimmerId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Schnittstelle für andere Ressourcen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Liefert den Bettenspiegel als flache Auskunft.
|
|
--- Reine Anzeige — keine Ressource kann darüber ein Bett belegen.
|
|
---
|
|
--- `betten` ist die gesamte Bettenzahl, `frei` zählt nur, was auch
|
|
--- belegt werden dürfte: gesperrte Zimmer und Zimmer in Reinigung
|
|
--- bleiben aussen vor.
|
|
--- @return table Liste { station, name, betten, belegt, frei }
|
|
exports('HoleBettenspiegel', function()
|
|
local stationen = MySQL.query.await([[
|
|
SELECT id, kennung AS station, name
|
|
FROM medical_station
|
|
WHERE aktiv = 1
|
|
ORDER BY sortierung, kennung
|
|
]]) or {}
|
|
|
|
for _, eintrag in ipairs(stationen) do
|
|
local zimmerListe = MySQL.query.await(
|
|
'SELECT id, betten, status FROM medical_zimmer WHERE station = ?',
|
|
{ eintrag.id }) or {}
|
|
|
|
eintrag.betten, eintrag.belegt, eintrag.frei = 0, 0, 0
|
|
|
|
for _, zimmer in ipairs(zimmerListe) do
|
|
local betten = tonumber(zimmer.betten) or 1
|
|
local belegt = BelegteBetten(zimmer.id)
|
|
|
|
eintrag.betten = eintrag.betten + betten
|
|
eintrag.belegt = eintrag.belegt + belegt
|
|
if zimmer.status == 'frei' then
|
|
eintrag.frei = eintrag.frei + math.max(0, betten - belegt)
|
|
end
|
|
end
|
|
|
|
eintrag.id = nil
|
|
end
|
|
|
|
return stationen
|
|
end)
|