- 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>
807 lines
32 KiB
Lua
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)
|