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

807 lines
32 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Patientenverwaltung
--
-- Anmeldung, Warteliste, Aufnahme, Verlegung und Entlassung.
--
-- Nichts davon geschieht von selbst. Das System führt die Warteliste
-- und zeigt, welche Zimmer frei sind — welcher Patient wann und wohin
-- aufgenommen wird, entscheidet ausschliesslich ein Mediziner.
--
-- Besonders wichtig: die Dringlichkeit wird NICHT aus der Wartezeit
-- berechnet. Ein Patient, der lange wartet, rutscht nicht automatisch
-- nach oben — das System erinnert nur daran, dass er wartet.
-- =====================================================================
Kern.RegistriereModul({
id = 'patienten', name = 'Patienten', icon = '🧍',
reihenfolge = 3, recht = 'patient.suchen',
beschreibung = 'Anmeldung, Warteliste, Belegung und Entlassung',
})
-- Dringlichkeitsstufen. Reine Beschriftung — keine Stufe löst etwas aus.
local DRINGLICHKEIT = {
[1] = { name = 'Sofort', hinweis = 'Lebensbedrohlich' },
[2] = { name = 'Dringend', hinweis = 'Zeitkritisch' },
[3] = { name = 'Regulär', hinweis = 'Kann warten' },
[4] = { name = 'Nachrangig', hinweis = 'Nicht akut' },
}
-- ---------------------------------------------------------------------
-- Grundbestand an Stationen und Zimmern
--
-- Wird einmalig angelegt, damit sich überhaupt jemand aufnehmen
-- lässt. Die Verwaltung übernimmt später die Krankenhausverwaltung.
-- ---------------------------------------------------------------------
local GRUNDSTATIONEN = {
-- kennung, name, art, etage, zimmer = { kennung, betten, art }
{ 'NA', 'Notaufnahme', 'notaufnahme', 'EG', {
{ 'NA-1', 1, 'behandlung' }, { 'NA-2', 1, 'behandlung' },
{ 'NA-3', 1, 'behandlung' }, { 'SCHOCK', 1, 'behandlung' },
}},
{ 'CH', 'Chirurgie', 'normal', '1. OG', {
{ 'CH-101', 2, 'bett' }, { 'CH-102', 2, 'bett' }, { 'CH-103', 1, 'bett' },
}},
{ 'IN', 'Innere Medizin', 'normal', '1. OG', {
{ 'IN-111', 2, 'bett' }, { 'IN-112', 2, 'bett' },
}},
{ 'ITS', 'Intensivstation', 'intensiv', '2. OG', {
{ 'ITS-1', 1, 'intensiv' }, { 'ITS-2', 1, 'intensiv' },
{ 'ITS-3', 1, 'intensiv' }, { 'ISO-1', 1, 'isolierung' },
}},
{ 'OP', 'OP-Bereich', 'op', '2. OG', {
{ 'OP-1', 1, 'op' }, { 'OP-2', 1, 'op' },
}},
}
CreateThread(function()
Wait(1500)
local vorhanden = tonumber(MySQL.scalar.await('SELECT COUNT(*) FROM medical_station')) or 0
if vorhanden > 0 then return end
for reihenfolge, eintrag in ipairs(GRUNDSTATIONEN) do
local stationId = MySQL.insert.await([[
INSERT INTO medical_station (kennung, name, art, etage, sortierung)
VALUES (?, ?, ?, ?, ?)
]], { eintrag[1], eintrag[2], eintrag[3], eintrag[4], reihenfolge })
for _, zimmer in ipairs(eintrag[5]) do
MySQL.insert.await([[
INSERT INTO medical_zimmer (station, kennung, betten, art)
VALUES (?, ?, ?, ?)
]], { stationId, zimmer[1], zimmer[2], zimmer[3] })
end
end
Aether.Debug('Grundbestand an Stationen und Zimmern angelegt.')
end)
-- ---------------------------------------------------------------------
-- 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
--- Prüft eine Dringlichkeitsstufe
--- @param wert any Eingabe
--- @return number 1 bis 4
local function Dringlichkeit(wert)
local stufe = tonumber(wert) or 3
if stufe < 1 or stufe > 4 then return 3 end
return math.floor(stufe)
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 c.firstname, c.lastname, p.personalnummer
FROM aether_characters c
LEFT JOIN medical_personal p ON p.`character` = c.id
WHERE c.id = ?
]], { charakterId })
if not zeilen or not zeilen[1] then return 'unbekannt' end
return zeilen[1].firstname .. ' ' .. zeilen[1].lastname
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
--- Sucht ein freies Bett in einem Zimmer
--- @param zimmerId number Zimmer
--- @param betten number Bettenzahl des Zimmers
--- @return number|nil Bettnummer
local function FreiesBett(zimmerId, betten)
local belegt = MySQL.query.await(
'SELECT bett FROM medical_belegung WHERE zimmer = ? AND frei_am IS NULL', { zimmerId }) or {}
local genommen = {}
for _, eintrag in ipairs(belegt) do genommen[tonumber(eintrag.bett)] = true end
for nummer = 1, betten do
if not genommen[nummer] then return nummer end
end
return nil
end
--- Gibt alle Betten einer Aufnahme frei
--- @param aufnahmeId number Aufnahme
--- @param mitarbeiter table Handelnder
--- @return table Liste der frei gewordenen Zimmer
local function BettenFreigeben(aufnahmeId, mitarbeiter)
local belegungen = MySQL.query.await([[
SELECT b.id, b.zimmer, z.kennung, z.betten
FROM medical_belegung b
JOIN medical_zimmer z ON z.id = b.zimmer
WHERE b.aufnahme = ? AND b.frei_am IS NULL
]], { aufnahmeId }) or {}
for _, belegung in ipairs(belegungen) do
MySQL.update.await(
'UPDATE medical_belegung SET frei_am = NOW(), frei_von = ? WHERE id = ?',
{ mitarbeiter.charakterId, belegung.id })
end
return belegungen
end
-- =====================================================================
-- ÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('patienten', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
local ordner = tostring(daten.ordner or 'warteliste')
local bedingung, werte
if ordner == 'entlassen' then
bedingung = "a.status = 'entlassen'"
werte = {}
elseif ordner == 'station' then
bedingung = "a.status = 'aufgenommen'"
werte = {}
else
bedingung = "a.status = 'wartend'"
werte = {}
end
if daten.station and daten.station ~= '' then
bedingung = bedingung .. ' AND a.station = ?'
werte[#werte + 1] = daten.station
end
local liste = MySQL.query.await(([[
SELECT a.id, a.patient, a.grund, a.prioritaet, a.station, a.status,
a.aufgenommen_am, a.aufgenommen_von, a.entlassen_am, a.entlassen_von,
a.entlass_grund,
TIMESTAMPDIFF(MINUTE, a.aufgenommen_am, NOW()) AS wartet,
c.firstname, c.lastname, c.dob,
m.blutgruppe, m.allergien, m.hinweis,
z.kennung AS zimmer, s.name AS stationName,
(SELECT COUNT(*) FROM medical_intensiv i
WHERE i.aufnahme = a.id AND i.beendet_am IS NULL) AS aufIntensiv
FROM medical_aufnahme a
JOIN aether_characters c ON c.id = a.patient
LEFT JOIN medical_patient m ON m.`character` = a.patient
LEFT JOIN medical_zimmer z ON z.id = a.zimmer
LEFT JOIN medical_station s ON s.kennung = a.station
WHERE %s
ORDER BY a.prioritaet, a.aufgenommen_am
LIMIT 150
]]):format(bedingung), werte) or {}
for _, eintrag in ipairs(liste) do
eintrag.name = eintrag.firstname .. ' ' .. eintrag.lastname
eintrag.geburt = tostring(eintrag.dob):sub(1, 10)
eintrag.firstname, eintrag.lastname, eintrag.dob = nil, nil, nil
eintrag.dringlichkeit = DRINGLICHKEIT[eintrag.prioritaet]
and DRINGLICHKEIT[eintrag.prioritaet].name or '—'
eintrag.aufgenommenVon = NameVon(eintrag.aufgenommen_von)
eintrag.entlassenVon = eintrag.entlassen_von and NameVon(eintrag.entlassen_von) or nil
eintrag.aufIntensiv = tonumber(eintrag.aufIntensiv) > 0
eintrag.aufgenommen_von, eintrag.entlassen_von = nil, nil
end
-- Zählwerte für die Reiter
local zaehler = {
wartend = tonumber(MySQL.scalar.await(
"SELECT COUNT(*) FROM medical_aufnahme WHERE status = 'wartend'")) or 0,
aufgenommen = tonumber(MySQL.scalar.await(
"SELECT COUNT(*) FROM medical_aufnahme WHERE status = 'aufgenommen'")) or 0,
entlassenHeute = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_aufnahme
WHERE status = 'entlassen' AND DATE(entlassen_am) = CURDATE()
]])) or 0,
}
-- Belegung je Station: was ist frei, was ist belegt
local stationen = MySQL.query.await([[
SELECT s.id, s.kennung, s.name, s.art, s.etage,
COALESCE(SUM(z.betten), 0) AS betten,
COUNT(z.id) AS zimmer
FROM medical_station s
LEFT JOIN medical_zimmer z ON z.station = s.id AND z.status <> 'gesperrt'
WHERE s.aktiv = 1
GROUP BY s.id
ORDER BY s.sortierung, s.kennung
]]) or {}
for _, station in ipairs(stationen) do
station.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
]], { station.id })) or 0
station.betten = tonumber(station.betten) or 0
station.frei = math.max(0, station.betten - station.belegt)
end
return {
ordner = ordner,
liste = liste,
zaehler = zaehler,
stationen = stationen,
dringlichkeit = DRINGLICHKEIT,
rechte = {
aufnehmen = Rechte.Hat(mitarbeiter, 'patient.aufnehmen'),
verlegen = Rechte.Hat(mitarbeiter, 'patient.verlegen'),
entlassen = Rechte.Hat(mitarbeiter, 'patient.entlassen'),
belegen = Rechte.Hat(mitarbeiter, 'station.belegen'),
},
}
end)
Kern.RegistriereAktion('patienten', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
local aufnahmeId = tonumber(daten.id)
if not aufnahmeId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT a.*, c.firstname, c.lastname, c.dob,
m.blutgruppe, m.allergien, m.vorerkrankungen, m.dauermedikation,
m.hinweis, m.notfallkontakt, m.lichtbild,
z.kennung AS zimmer, z.art AS zimmerArt, s.name AS stationName
FROM medical_aufnahme a
JOIN aether_characters c ON c.id = a.patient
LEFT JOIN medical_patient m ON m.`character` = a.patient
LEFT JOIN medical_zimmer z ON z.id = a.zimmer
LEFT JOIN medical_station s ON s.kennung = a.station
WHERE a.id = ?
]], { aufnahmeId })
if not zeilen or not zeilen[1] then return nil, 'Aufnahme nicht gefunden.' end
local a = zeilen[1]
-- Welches Bett belegt der Patient gerade?
local bett = MySQL.query.await([[
SELECT b.bett, b.belegt_am, b.belegt_von, z.kennung
FROM medical_belegung b
JOIN medical_zimmer z ON z.id = b.zimmer
WHERE b.aufnahme = ? AND b.frei_am IS NULL
LIMIT 1
]], { aufnahmeId })
bett = bett and bett[1] or nil
-- Laufende Behandlungen zu dieser Aufnahme
local behandlungen = MySQL.query.await([[
SELECT id, kennung, diagnose, status, erstellt_am, erstellt_von
FROM medical_behandlung WHERE aufnahme = ?
ORDER BY id DESC LIMIT 20
]], { aufnahmeId }) or {}
for _, eintrag in ipairs(behandlungen) do
eintrag.autor = NameVon(eintrag.erstellt_von)
eintrag.erstellt_von = nil
end
return {
aufnahme = {
id = a.id, patient = a.patient,
name = a.firstname .. ' ' .. a.lastname,
geburt = tostring(a.dob):sub(1, 10),
grund = a.grund, prioritaet = a.prioritaet,
dringlichkeit = DRINGLICHKEIT[a.prioritaet] and DRINGLICHKEIT[a.prioritaet].name or '—',
station = a.station, stationName = a.stationName,
zimmer = a.zimmer, zimmerArt = a.zimmerArt,
bett = bett and bett.bett or nil,
status = a.status,
aufgenommenAm = a.aufgenommen_am,
aufgenommenVon = NameVon(a.aufgenommen_von),
entlassenAm = a.entlassen_am,
entlassenVon = a.entlassen_von and NameVon(a.entlassen_von) or nil,
entlassGrund = a.entlass_grund,
},
stamm = {
blutgruppe = a.blutgruppe, allergien = a.allergien,
vorerkrankungen = a.vorerkrankungen, dauermedikation = a.dauermedikation,
hinweis = a.hinweis, notfallkontakt = a.notfallkontakt,
lichtbild = a.lichtbild,
},
behandlungen = behandlungen,
dringlichkeit = DRINGLICHKEIT,
rechte = {
aufnehmen = Rechte.Hat(mitarbeiter, 'patient.aufnehmen'),
verlegen = Rechte.Hat(mitarbeiter, 'patient.verlegen'),
entlassen = Rechte.Hat(mitarbeiter, 'patient.entlassen'),
},
}
end)
-- =====================================================================
-- ANMELDUNG
--
-- Der erste Schritt: ein Patient meldet sich oder wird gebracht.
-- Er landet auf der Warteliste — mehr passiert nicht.
-- =====================================================================
Kern.RegistriereAktion('patienten', 'anmelden', 'patient.aufnehmen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
local grund = Text(daten.grund, 512)
if not patientId then return nil, 'Kein Patient gewählt.' end
if not grund or #grund < 3 then return nil, 'Bitte den Grund der Vorstellung angeben.' end
local person = MySQL.query.await(
'SELECT firstname, lastname FROM aether_characters WHERE id = ? AND deleted = 0',
{ patientId })
if not person or not person[1] then return nil, 'Diese Person ist nicht erfasst.' end
-- Doppelte Anmeldung verhindern
local offen = MySQL.scalar.await([[
SELECT id FROM medical_aufnahme
WHERE patient = ? AND status IN ('wartend', 'aufgenommen')
]], { patientId })
if offen then
return nil, 'Für diesen Patienten läuft bereits eine Aufnahme.'
end
local prioritaet = Dringlichkeit(daten.prioritaet)
local id = MySQL.insert.await([[
INSERT INTO medical_aufnahme
(patient, grund, prioritaet, status, aufgenommen_von)
VALUES (?, ?, ?, 'wartend', ?)
]], { patientId, grund, prioritaet, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'patienten', 'Patient angemeldet',
{ typ = 'aufnahme', id = id },
{ patient = patientId, prioritaet = prioritaet })
Hinweise.Erstelle(nil, {
kategorie = 'patient',
prioritaet = prioritaet <= 2 and 'hoch' or 'normal',
titel = ('Patient wartet · %s'):format(DRINGLICHKEIT[prioritaet].name),
text = ('%s %s — %s'):format(person[1].firstname, person[1].lastname, grund:sub(1, 80)),
zielTyp = 'aufnahme', zielId = id,
})
Kern.Loese('aufnahme.angemeldet', id, mitarbeiter)
return { id = id }
end)
--- Personensuche für die Anmeldung
Kern.RegistriereAktion('patienten', 'patientSuche', 'patient.aufnehmen', 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,
m.blutgruppe, m.allergien, m.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 m ON m.`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)
-- =====================================================================
-- AUFNAHME AUF STATION
-- =====================================================================
--- Freie Zimmer einer Station — reine Auskunft, das System wählt keines aus
Kern.RegistriereAktion('patienten', 'freieZimmer', nil, function(quelle, mitarbeiter, daten)
local bedingung, werte = 's.aktiv = 1', {}
if daten.station and daten.station ~= '' then
bedingung = bedingung .. ' AND s.kennung = ?'
werte[#werte + 1] = daten.station
end
local zimmer = MySQL.query.await(([[
SELECT z.id, z.kennung, z.betten, z.art, z.status, z.hinweis,
s.kennung AS station, s.name AS stationName, s.art AS stationArt
FROM medical_zimmer z
JOIN medical_station s ON s.id = z.station
WHERE %s
ORDER BY s.sortierung, z.kennung
]]):format(bedingung), werte) or {}
for _, eintrag in ipairs(zimmer) do
eintrag.belegt = BelegteBetten(eintrag.id)
eintrag.frei = math.max(0, eintrag.betten - eintrag.belegt)
-- Verfügbar heisst: nicht gesperrt, nicht in Reinigung, noch Platz
eintrag.verfuegbar = eintrag.status == 'frei' and eintrag.frei > 0
end
return { zimmer = zimmer }
end)
Kern.RegistriereAktion('patienten', 'aufnehmen', 'patient.aufnehmen', function(quelle, mitarbeiter, daten)
local aufnahmeId = tonumber(daten.id)
if not aufnahmeId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT a.id, a.patient, a.status, c.firstname, c.lastname
FROM medical_aufnahme a
JOIN aether_characters c ON c.id = a.patient
WHERE a.id = ?
]], { aufnahmeId })
if not zeilen or not zeilen[1] then return nil, 'Aufnahme nicht gefunden.' end
local aufnahme = zeilen[1]
if aufnahme.status ~= 'wartend' then
return nil, 'Dieser Patient ist bereits aufgenommen oder entlassen.'
end
-- Station prüfen
local station = MySQL.query.await(
'SELECT kennung, name FROM medical_station WHERE kennung = ? AND aktiv = 1',
{ tostring(daten.station or '') })
if not station or not station[1] then return nil, 'Unbekannte Station.' end
-- Zimmer ist freiwillig — ein Patient kann auch ohne Bett aufgenommen werden
local zimmerId, bettNummer = nil, nil
if daten.zimmer and tonumber(daten.zimmer) then
local zimmer = MySQL.query.await([[
SELECT z.id, z.kennung, z.betten, z.status, s.kennung AS station
FROM medical_zimmer z
JOIN medical_station s ON s.id = z.station
WHERE z.id = ?
]], { tonumber(daten.zimmer) })
if not zimmer or not zimmer[1] then return nil, 'Unbekanntes Zimmer.' end
if zimmer[1].status ~= 'frei' then
return nil, ('Zimmer %s steht nicht zur Verfügung (%s).')
:format(zimmer[1].kennung, zimmer[1].status)
end
bettNummer = FreiesBett(zimmer[1].id, zimmer[1].betten)
if not bettNummer then
return nil, ('In Zimmer %s ist kein Bett mehr frei.'):format(zimmer[1].kennung)
end
zimmerId = zimmer[1].id
end
MySQL.update.await([[
UPDATE medical_aufnahme
SET status = 'aufgenommen', station = ?, zimmer = ?
WHERE id = ?
]], { station[1].kennung, zimmerId, aufnahmeId })
if zimmerId then
MySQL.insert.await([[
INSERT INTO medical_belegung (zimmer, bett, patient, aufnahme, belegt_von)
VALUES (?, ?, ?, ?, ?)
]], { zimmerId, bettNummer, aufnahme.patient, aufnahmeId, mitarbeiter.charakterId })
end
Protokoll.Schreibe(mitarbeiter, 'patienten', 'Patient aufgenommen',
{ typ = 'aufnahme', id = aufnahmeId },
{ station = station[1].kennung, zimmer = zimmerId, bett = bettNummer },
Text(daten.vermerk, 255))
Kern.Loese('aufnahme.aufgenommen', aufnahmeId, mitarbeiter)
return { ok = true, bett = bettNummer }
end)
-- =====================================================================
-- VERLEGUNG
-- =====================================================================
Kern.RegistriereAktion('patienten', 'verlegen', 'patient.verlegen', function(quelle, mitarbeiter, daten)
local aufnahmeId = tonumber(daten.id)
if not aufnahmeId then return nil, 'Ungültige Kennung.' end
local zeilen = 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 zeilen or not zeilen[1] then return nil, 'Aufnahme nicht gefunden.' end
local aufnahme = zeilen[1]
if aufnahme.status ~= 'aufgenommen' then
return nil, 'Nur aufgenommene Patienten lassen sich verlegen.'
end
local grund = Text(daten.grund, 255)
if not grund then return nil, 'Bitte den Grund der Verlegung angeben.' end
local station = MySQL.query.await(
'SELECT kennung, name FROM medical_station WHERE kennung = ? AND aktiv = 1',
{ tostring(daten.station or '') })
if not station or not station[1] then return nil, 'Unbekannte Station.' end
-- Neues Zimmer prüfen, bevor das alte freigegeben wird
local zimmerId, bettNummer = nil, nil
if daten.zimmer and tonumber(daten.zimmer) then
local zimmer = MySQL.query.await(
'SELECT id, kennung, betten, status FROM medical_zimmer WHERE id = ?',
{ tonumber(daten.zimmer) })
if not zimmer or not zimmer[1] then return nil, 'Unbekanntes Zimmer.' end
if zimmer[1].status ~= 'frei' then
return nil, ('Zimmer %s steht nicht zur Verfügung.'):format(zimmer[1].kennung)
end
-- Verlegung ins selbe Zimmer ist keine Verlegung
if zimmer[1].id == aufnahme.zimmer then
return nil, 'Der Patient liegt bereits in diesem Zimmer.'
end
bettNummer = FreiesBett(zimmer[1].id, zimmer[1].betten)
if not bettNummer then
return nil, ('In Zimmer %s ist kein Bett mehr frei.'):format(zimmer[1].kennung)
end
zimmerId = zimmer[1].id
end
local vorher = BettenFreigeben(aufnahmeId, mitarbeiter)
MySQL.update.await('UPDATE medical_aufnahme SET station = ?, zimmer = ? WHERE id = ?',
{ station[1].kennung, zimmerId, aufnahmeId })
if zimmerId then
MySQL.insert.await([[
INSERT INTO medical_belegung (zimmer, bett, patient, aufnahme, belegt_von)
VALUES (?, ?, ?, ?, ?)
]], { zimmerId, bettNummer, aufnahme.patient, aufnahmeId, mitarbeiter.charakterId })
end
Protokoll.Schreibe(mitarbeiter, 'patienten', 'Patient verlegt',
{ typ = 'aufnahme', id = aufnahmeId },
{ vonStation = aufnahme.station, nachStation = station[1].kennung,
zimmer = zimmerId, bett = bettNummer }, grund)
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'normal',
titel = 'Neue Verlegung',
text = ('%s %s wurde auf %s verlegt.')
:format(aufnahme.firstname, aufnahme.lastname, station[1].name),
zielTyp = 'aufnahme', zielId = aufnahmeId,
})
-- Frei gewordene Zimmer melden — das Freigeben zur Reinigung bleibt manuell
for _, belegung in ipairs(vorher) do
if BelegteBetten(belegung.zimmer) == 0 then
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'niedrig',
titel = 'Zimmer wurde frei',
text = ('Zimmer %s ist jetzt leer.'):format(belegung.kennung),
zielTyp = 'zimmer', zielId = belegung.zimmer,
})
end
end
Kern.Loese('aufnahme.verlegt', aufnahmeId, mitarbeiter)
return { ok = true, bett = bettNummer }
end)
-- =====================================================================
-- DRINGLICHKEIT
--
-- Wird ausschliesslich von Hand gesetzt. Wartezeit ändert sie nie.
-- =====================================================================
Kern.RegistriereAktion('patienten', 'prioritaet', 'patient.aufnehmen', function(quelle, mitarbeiter, daten)
local aufnahmeId = tonumber(daten.id)
if not aufnahmeId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, prioritaet, status FROM medical_aufnahme WHERE id = ?', { aufnahmeId })
if not zeilen or not zeilen[1] then return nil, 'Aufnahme nicht gefunden.' end
if zeilen[1].status == 'entlassen' then
return nil, 'Dieser Vorgang ist bereits abgeschlossen.'
end
local neu = Dringlichkeit(daten.prioritaet)
local grund = Text(daten.grund, 255)
if not grund then return nil, 'Bitte begründen, warum sich die Dringlichkeit ändert.' end
MySQL.update.await('UPDATE medical_aufnahme SET prioritaet = ? WHERE id = ?',
{ neu, aufnahmeId })
Protokoll.Schreibe(mitarbeiter, 'patienten', 'Dringlichkeit geändert',
{ typ = 'aufnahme', id = aufnahmeId },
{ von = zeilen[1].prioritaet, nach = neu }, grund)
return { ok = true }
end)
-- =====================================================================
-- ENTLASSUNG
--
-- Geschieht nie automatisch. Auch nicht, wenn alle Behandlungen
-- abgeschlossen sind — das entscheidet ein Arzt.
-- =====================================================================
Kern.RegistriereAktion('patienten', 'entlassen', 'patient.entlassen', function(quelle, mitarbeiter, daten)
local aufnahmeId = tonumber(daten.id)
if not aufnahmeId then return nil, 'Ungültige Kennung.' end
local grund = Text(daten.grund, 512)
if not grund or #grund < 5 then
return nil, 'Eine Entlassung erfordert eine Begründung.'
end
local zeilen = MySQL.query.await([[
SELECT a.id, a.patient, a.status, c.firstname, c.lastname
FROM medical_aufnahme a
JOIN aether_characters c ON c.id = a.patient
WHERE a.id = ?
]], { aufnahmeId })
if not zeilen or not zeilen[1] then return nil, 'Aufnahme nicht gefunden.' end
local aufnahme = zeilen[1]
if aufnahme.status == 'entlassen' then
return nil, 'Dieser Patient ist bereits entlassen.'
end
-- Offene Punkte NUR melden, nicht verhindern und nicht abschliessen
local offeneBehandlungen = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_behandlung
WHERE aufnahme = ? AND status = 'laufend'
]], { aufnahmeId })) or 0
local aufIntensiv = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_intensiv
WHERE aufnahme = ? AND beendet_am IS NULL
]], { aufnahmeId })) or 0
local freigeworden = BettenFreigeben(aufnahmeId, mitarbeiter)
MySQL.update.await([[
UPDATE medical_aufnahme
SET status = 'entlassen', zimmer = NULL,
entlassen_von = ?, entlassen_am = NOW(), entlass_grund = ?
WHERE id = ?
]], { mitarbeiter.charakterId, grund, aufnahmeId })
Protokoll.Schreibe(mitarbeiter, 'patienten', 'Patient entlassen',
{ typ = 'aufnahme', id = aufnahmeId },
{ patient = aufnahme.patient,
offeneBehandlungen = offeneBehandlungen,
aufIntensiv = aufIntensiv > 0 }, grund)
-- Frei gewordene Zimmer melden
for _, belegung in ipairs(freigeworden) do
if BelegteBetten(belegung.zimmer) == 0 then
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'niedrig',
titel = 'Zimmer wurde frei',
text = ('Zimmer %s ist jetzt leer.'):format(belegung.kennung),
zielTyp = 'zimmer', zielId = belegung.zimmer,
})
end
end
-- Laufende Dokumentation bleibt offen und wird nur in Erinnerung gerufen
if offeneBehandlungen > 0 then
Hinweise.Erstelle({ charakterId = mitarbeiter.charakterId }, {
kategorie = 'doku', prioritaet = 'normal',
titel = 'Dokumentation noch offen',
text = ('%s %s wurde entlassen — %d Behandlung(en) sind noch nicht abgeschlossen.')
:format(aufnahme.firstname, aufnahme.lastname, offeneBehandlungen),
zielTyp = 'aufnahme', zielId = aufnahmeId,
})
end
-- Ein laufender Intensivaufenthalt wird NICHT beendet, nur gemeldet
if aufIntensiv > 0 then
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'hoch',
titel = 'Entlassung trotz laufender Intensivbetreuung',
text = ('%s %s wurde entlassen, der Intensivaufenthalt ist noch offen. Bitte prüfen.')
:format(aufnahme.firstname, aufnahme.lastname),
zielTyp = 'aufnahme', zielId = aufnahmeId,
})
end
Kern.Loese('aufnahme.entlassen', aufnahmeId, mitarbeiter)
return { ok = true, offeneBehandlungen = offeneBehandlungen }
end)
-- =====================================================================
-- ZIMMERSTATUS
--
-- Reinigung und Sperrung setzt immer ein Mensch. Ein leer gewordenes
-- Zimmer wird nicht von selbst wieder als frei gemeldet.
-- =====================================================================
Kern.RegistriereAktion('patienten', '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
local erlaubt = { frei = true, reinigung = true, gesperrt = true }
if not erlaubt[status] then return nil, 'Unbekannter Zimmerstatus.' end
local zimmer = MySQL.query.await(
'SELECT id, kennung, betten FROM medical_zimmer WHERE id = ?', { zimmerId })
if not zimmer or not zimmer[1] then return nil, 'Zimmer nicht gefunden.' end
-- 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
MySQL.update.await('UPDATE medical_zimmer SET status = ?, hinweis = ? WHERE id = ?',
{ status, Text(daten.hinweis, 255), zimmerId })
Protokoll.Schreibe(mitarbeiter, 'patienten', 'Zimmerstatus gesetzt',
{ typ = 'zimmer', id = zimmerId },
{ kennung = zimmer[1].kennung, status = status })
return { ok = true }
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Ressourcen
-- ---------------------------------------------------------------------
--- Liegt dieser Patient gerade im Krankenhaus?
--- Reine Auskunft — keine Ressource kann darüber jemanden aufnehmen.
--- @param patientId number Charakter
--- @return table|nil { status, station, zimmer, seit }
exports('HoleAufenthalt', function(patientId)
if not tonumber(patientId) then return nil end
local zeilen = MySQL.query.await([[
SELECT a.status, a.station, a.aufgenommen_am, z.kennung AS zimmer
FROM medical_aufnahme a
LEFT JOIN medical_zimmer z ON z.id = a.zimmer
WHERE a.patient = ? AND a.status IN ('wartend', 'aufgenommen')
ORDER BY a.id DESC LIMIT 1
]], { tonumber(patientId) })
if not zeilen or not zeilen[1] then return nil end
return {
status = zeilen[1].status,
station = zeilen[1].station,
zimmer = zeilen[1].zimmer,
seit = zeilen[1].aufgenommen_am,
}
end)