aether-framework/resources/[aether]/aether-medical/server/module/station.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

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)