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

695 lines
28 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Intensivstation
--
-- Ein Intensivaufenthalt ist hier eine Akte, kein Monitor.
--
-- Das System misst nichts. Es kennt keinen Blutdruck, keine Sättigung
-- und keinen Alarm. Der Zustand eines Patienten steht genau deshalb
-- im Datensatz, weil ein Mediziner ihn dort eingetragen hat — er
-- ändert sich nie von selbst, weder durch Zeitablauf noch durch
-- irgendeine Rechnung.
--
-- Auch Geräte werden nur benannt, nicht gesteuert. Und ein Aufenthalt
-- endet erst, wenn jemand ihn beendet: nicht mit der Entlassung,
-- nicht mit dem Bett und nicht mit dem Dienstende.
-- =====================================================================
Kern.RegistriereModul({
id = 'intensiv', name = 'Intensiv', icon = '📈',
reihenfolge = 10, recht = 'intensiv.sehen',
beschreibung = 'Intensivaufenthalte, Zustand, Geräte und Verlauf',
})
-- ---------------------------------------------------------------------
-- 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 einen Zustand gegen die Konfiguration
--- @param wert any Eingabe
--- @return string|nil Gültige Kennung
local function Zustand(wert)
local gesucht = tostring(wert or '')
for _, eintrag in ipairs(Medizin.Config.Intensivzustaende) do
if eintrag.id == gesucht then return eintrag.id end
end
return nil
end
--- Anzeigename eines Zustands
--- @param kennung string Zustand
--- @return string
local function ZustandName(kennung)
for _, eintrag in ipairs(Medizin.Config.Intensivzustaende) do
if eintrag.id == kennung then return eintrag.name end
end
return tostring(kennung)
end
--- Baut die Sortierreihenfolge der Zustände für MySQL.
--- Sortiert wird nach dem, was Menschen eingetragen haben — das System
--- bewertet dabei nichts, es stellt nur das Eingetragene nach oben.
--- @return string FIELD(...)-Ausdruck
local function ZustandSortierung()
local teile = {}
for _, eintrag in ipairs(Medizin.Config.Intensivzustaende) do
-- Die Kennungen stammen aus der eigenen Konfiguration
teile[#teile + 1] = ("'%s'"):format(eintrag.id:gsub("'", ''))
end
return ('FIELD(i.zustand, %s)'):format(table.concat(teile, ', '))
end
--- Lädt einen Intensivaufenthalt mit allen Begleitdaten
--- @param intensivId number Aufenthalt
--- @return table|nil Zeile
local function LadeAufenthalt(intensivId)
local zeilen = MySQL.query.await([[
SELECT i.*,
TIMESTAMPDIFF(MINUTE, i.aufgenommen_am,
COALESCE(i.beendet_am, NOW())) AS liegtSeit,
c.firstname, c.lastname, c.dob,
z.kennung AS zimmerKennung, z.art AS zimmerArt,
s.name AS stationName,
a.status AS aufnahmeStatus, a.grund AS aufnahmeGrund
FROM medical_intensiv i
JOIN aether_characters c ON c.id = i.patient
LEFT JOIN medical_zimmer z ON z.id = i.zimmer
LEFT JOIN medical_station s ON s.id = z.station
LEFT JOIN medical_aufnahme a ON a.id = i.aufnahme
WHERE i.id = ?
]], { intensivId })
return zeilen and zeilen[1] or nil
end
--- Schreibt einen Verlaufseintrag.
--- Der Text stammt immer von einem Menschen — das System formuliert
--- hier nichts und fasst nichts zusammen.
--- @param intensivId number Aufenthalt
--- @param text string Wortlaut
--- @param charakterId number Verfasser
--- @return number Eintrag
local function SchreibeVerlauf(intensivId, text, charakterId)
return MySQL.insert.await(
'INSERT INTO medical_intensiv_verlauf (intensiv, text, von) VALUES (?, ?, ?)',
{ intensivId, text, charakterId })
end
-- =====================================================================
-- ÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('intensiv', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
local ordner = tostring(daten.ordner or 'laufend')
local beendet = ordner == 'beendet'
local liste = MySQL.query.await(([[
SELECT i.id, i.patient, i.aufnahme, i.zimmer, i.zustand, i.geraete, i.notiz,
i.verantwortlich, i.aufgenommen_am, i.aufgenommen_von,
i.beendet_am, i.beendet_von,
TIMESTAMPDIFF(MINUTE, i.aufgenommen_am,
COALESCE(i.beendet_am, NOW())) AS liegtSeit,
TIMESTAMPDIFF(MINUTE, COALESCE(
(SELECT MAX(v.am) FROM medical_intensiv_verlauf v WHERE v.intensiv = i.id),
i.aufgenommen_am), NOW()) AS stillSeit,
(SELECT COUNT(*) FROM medical_intensiv_verlauf v
WHERE v.intensiv = i.id) AS eintraege,
c.firstname, c.lastname, c.dob,
p.blutgruppe, p.allergien, p.hinweis AS warnhinweis,
z.kennung AS zimmerKennung, s.name AS stationName
FROM medical_intensiv i
JOIN aether_characters c ON c.id = i.patient
LEFT JOIN medical_patient p ON p.`character` = i.patient
LEFT JOIN medical_zimmer z ON z.id = i.zimmer
LEFT JOIN medical_station s ON s.id = z.station
WHERE i.beendet_am IS %s NULL
ORDER BY %s
LIMIT 100
]]):format(beendet and 'NOT' or '',
beendet and 'i.beendet_am DESC' or (ZustandSortierung() .. ', i.aufgenommen_am'))) or {}
local stille = Medizin.Config.Erinnerungen.intensivStilleNach
for _, eintrag in ipairs(liste) do
eintrag.name = eintrag.firstname .. ' ' .. eintrag.lastname
eintrag.geburt = tostring(eintrag.dob):sub(1, 10)
eintrag.zustandName = ZustandName(eintrag.zustand)
eintrag.liegtSeit = tonumber(eintrag.liegtSeit) or 0
eintrag.stillSeit = tonumber(eintrag.stillSeit) or 0
eintrag.eintraege = tonumber(eintrag.eintraege) or 0
eintrag.verantwortlichName = eintrag.verantwortlich
and NameVon(eintrag.verantwortlich) or nil
eintrag.aufgenommenVon = NameVon(eintrag.aufgenommen_von)
eintrag.beendetVon = eintrag.beendet_von and NameVon(eintrag.beendet_von) or nil
-- Nur eine Erinnerung, keine Bewertung der Betreuung
eintrag.langeStill = not beendet and eintrag.stillSeit >= stille
eintrag.firstname, eintrag.lastname, eintrag.dob = nil, nil, nil
eintrag.aufgenommen_von, eintrag.beendet_von = nil, nil
end
-- Zählwerte über die laufenden Aufenthalte
local zaehler = {
laufend = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_intensiv WHERE beendet_am IS NULL')) or 0,
kritisch = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_intensiv
WHERE beendet_am IS NULL AND zustand = 'kritisch'
]])) or 0,
ohneArzt = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_intensiv
WHERE beendet_am IS NULL AND verantwortlich IS NULL
]])) or 0,
beendetHeute = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_intensiv
WHERE beendet_am IS NOT NULL AND DATE(beendet_am) = CURDATE()
]])) or 0,
}
-- Wie viele laufende Aufenthalte haben lange keinen Eintrag bekommen?
zaehler.langeStill = tonumber(MySQL.scalar.await(([[
SELECT COUNT(*) FROM medical_intensiv i
WHERE i.beendet_am IS NULL
AND COALESCE(
(SELECT MAX(v.am) FROM medical_intensiv_verlauf v WHERE v.intensiv = i.id),
i.aufgenommen_am) < DATE_SUB(NOW(), INTERVAL ? MINUTE)
]]), { stille })) or 0
return {
ordner = ordner,
liste = liste,
zaehler = zaehler,
zustaende = Medizin.Config.Intensivzustaende,
stilleNach = stille,
rechte = {
betreuen = Rechte.Hat(mitarbeiter, 'intensiv.betreuen'),
},
}
end)
-- =====================================================================
-- EINZELANSICHT
-- =====================================================================
Kern.RegistriereAktion('intensiv', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
local intensivId = tonumber(daten.id)
if not intensivId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeAufenthalt(intensivId)
if not eintrag then return nil, 'Intensivaufenthalt nicht gefunden.' end
-- Stammdaten, die am Bett gebraucht werden
local stamm = MySQL.query.await([[
SELECT blutgruppe, allergien, vorerkrankungen, dauermedikation,
hinweis, notfallkontakt, lichtbild
FROM medical_patient WHERE `character` = ?
]], { eintrag.patient })
stamm = stamm and stamm[1] or {}
-- Der Verlauf — jüngster Eintrag zuerst
local verlauf = MySQL.query.await([[
SELECT v.id, v.text, v.am, v.von, c.firstname, c.lastname
FROM medical_intensiv_verlauf v
LEFT JOIN aether_characters c ON c.id = v.von
WHERE v.intensiv = ?
ORDER BY v.id DESC
LIMIT 100
]], { intensivId }) or {}
for _, zeile in ipairs(verlauf) do
zeile.autor = zeile.firstname and (zeile.firstname .. ' ' .. zeile.lastname) or 'unbekannt'
zeile.firstname, zeile.lastname, zeile.von = nil, nil, nil
end
return {
aufenthalt = {
id = eintrag.id, patient = eintrag.patient, aufnahme = eintrag.aufnahme,
name = eintrag.firstname .. ' ' .. eintrag.lastname,
geburt = tostring(eintrag.dob):sub(1, 10),
zustand = eintrag.zustand, zustandName = ZustandName(eintrag.zustand),
geraete = eintrag.geraete, notiz = eintrag.notiz,
zimmer = eintrag.zimmer, zimmerKennung = eintrag.zimmerKennung,
zimmerArt = eintrag.zimmerArt, stationName = eintrag.stationName,
aufnahmeStatus = eintrag.aufnahmeStatus,
aufnahmeGrund = eintrag.aufnahmeGrund,
liegtSeit = tonumber(eintrag.liegtSeit) or 0,
verantwortlich = eintrag.verantwortlich,
verantwortlichName = eintrag.verantwortlich
and NameVon(eintrag.verantwortlich) or nil,
aufgenommenAm = eintrag.aufgenommen_am,
aufgenommenVon = NameVon(eintrag.aufgenommen_von),
beendetAm = eintrag.beendet_am,
beendetVon = eintrag.beendet_von and NameVon(eintrag.beendet_von) or nil,
laufend = eintrag.beendet_am == nil,
},
stamm = stamm,
verlauf = verlauf,
zustaende = Medizin.Config.Intensivzustaende,
geraeteliste = Medizin.Config.Intensivgeraete,
rechte = {
betreuen = Rechte.Hat(mitarbeiter, 'intensiv.betreuen'),
},
}
end)
-- =====================================================================
-- AUFNAHME AUF DIE INTENSIVSTATION
--
-- Wer auf Intensiv kommt, entscheidet ein Arzt. Kein Zustand, keine
-- Diagnose und kein Wert bringt jemanden von selbst hierher.
-- =====================================================================
--- Aufgenommene Patienten ohne laufenden Intensivaufenthalt
--- sowie die Zimmer, in denen eine Betreuung stattfinden könnte.
Kern.RegistriereAktion('intensiv', 'kandidaten', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local patienten = MySQL.query.await([[
SELECT a.id AS aufnahme, a.patient, a.grund, a.prioritaet, a.station,
TIMESTAMPDIFF(MINUTE, a.aufgenommen_am, NOW()) AS imHausSeit,
c.firstname, c.lastname, c.dob,
p.blutgruppe, p.allergien, p.hinweis AS warnhinweis,
z.id AS zimmer, z.kennung AS zimmerKennung,
s.name AS stationName
FROM medical_aufnahme a
JOIN aether_characters c ON c.id = a.patient
LEFT JOIN medical_patient p ON p.`character` = a.patient
LEFT JOIN medical_zimmer z ON z.id = a.zimmer
LEFT JOIN medical_station s ON s.id = z.station
WHERE a.status = 'aufgenommen'
AND NOT EXISTS (
SELECT 1 FROM medical_intensiv i
WHERE i.patient = a.patient AND i.beendet_am IS NULL)
ORDER BY a.prioritaet, a.aufgenommen_am
LIMIT 100
]]) or {}
for _, eintrag in ipairs(patienten) do
eintrag.name = eintrag.firstname .. ' ' .. eintrag.lastname
eintrag.geburt = tostring(eintrag.dob):sub(1, 10)
eintrag.imHausSeit = tonumber(eintrag.imHausSeit) or 0
eintrag.firstname, eintrag.lastname, eintrag.dob = nil, nil, nil
end
-- Intensivplätze zur Auswahl. Das Bett selbst verwaltet die
-- Stationsverwaltung — hier wird nur festgehalten, wo betreut wird.
local zimmer = MySQL.query.await([[
SELECT z.id, z.kennung, z.art, z.status, z.betten, z.hinweis,
s.name AS stationName,
(SELECT COUNT(*) FROM medical_belegung b
WHERE b.zimmer = z.id AND b.frei_am IS NULL) AS belegt
FROM medical_zimmer z
JOIN medical_station s ON s.id = z.station
WHERE s.aktiv = 1 AND z.art IN ('intensiv', 'isolierung')
ORDER BY s.sortierung, z.kennung
]]) or {}
for _, eintrag in ipairs(zimmer) do
eintrag.belegt = tonumber(eintrag.belegt) or 0
eintrag.betten = tonumber(eintrag.betten) or 1
end
return { patienten = patienten, zimmer = zimmer }
end)
Kern.RegistriereAktion('intensiv', 'aufnehmen', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local aufnahmeId = tonumber(daten.aufnahme)
if not aufnahmeId then return nil, 'Kein Patient gewählt.' end
local zustand = Zustand(daten.zustand)
if not zustand then return nil, 'Bitte einen Zustand eintragen.' end
local notiz = Text(daten.notiz, 1000)
if not notiz or #notiz < 5 then
return nil, 'Bitte festhalten, warum der Patient auf die Intensivstation kommt.'
end
local aufnahmen = MySQL.query.await([[
SELECT a.id, a.patient, a.status, 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 lassen sich auf die Intensivstation legen.'
end
-- Zwei laufende Aufenthalte für denselben Patienten ergeben keinen Sinn
local laufend = MySQL.scalar.await(
'SELECT id FROM medical_intensiv WHERE patient = ? AND beendet_am IS NULL',
{ aufnahme.patient })
if laufend then
return nil, 'Für diesen Patienten läuft bereits ein Intensivaufenthalt.'
end
-- Zimmer ist freiwillig: die Betreuung kann auch ohne festen Platz beginnen
local zimmerId = tonumber(daten.zimmer)
if zimmerId then
local zimmer = MySQL.query.await(
'SELECT id FROM medical_zimmer WHERE id = ?', { zimmerId })
if not zimmer or not zimmer[1] then return nil, 'Unbekanntes Zimmer.' end
end
-- Die Verantwortung übernimmt nur, wer das ausdrücklich angibt
local verantwortlich = daten.selbst == true and mitarbeiter.charakterId or nil
local geraete = Text(daten.geraete, 512)
local intensivId = MySQL.insert.await([[
INSERT INTO medical_intensiv
(patient, aufnahme, zimmer, geraete, verantwortlich, zustand, notiz, aufgenommen_von)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
]], { aufnahme.patient, aufnahmeId, zimmerId, geraete, verantwortlich,
zustand, notiz, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'intensiv', 'Intensivaufenthalt eröffnet',
{ typ = 'intensiv', id = intensivId },
{ patient = aufnahme.patient, aufnahme = aufnahmeId,
zimmer = zimmerId, zustand = zustand,
verantwortlich = verantwortlich }, notiz)
Hinweise.Erstelle(nil, {
kategorie = 'patient',
prioritaet = zustand == 'kritisch' and 'hoch' or 'normal',
titel = ('Intensivaufnahme · %s'):format(ZustandName(zustand)),
text = ('%s %s wird ab jetzt auf der Intensivstation betreut.')
:format(aufnahme.firstname, aufnahme.lastname),
zielTyp = 'intensiv', zielId = intensivId,
})
-- Ohne zuständigen Arzt bleibt der Aufenthalt sichtbar offen
if not verantwortlich then
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'hoch',
titel = 'Intensivpatient ohne zuständigen Arzt',
text = ('Für %s %s ist noch niemand eingetragen.')
:format(aufnahme.firstname, aufnahme.lastname),
zielTyp = 'intensiv', zielId = intensivId,
})
end
Kern.Loese('intensiv.aufgenommen', intensivId, mitarbeiter)
return { id = intensivId }
end)
-- =====================================================================
-- ZUSTAND
--
-- Der Zustand ist eine ärztliche Einschätzung und sonst nichts. Er
-- wird von Hand gesetzt, mit Begründung, und der Wortlaut dieser
-- Begründung wandert unverändert in den Verlauf.
-- =====================================================================
Kern.RegistriereAktion('intensiv', 'zustand', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local intensivId = tonumber(daten.id)
if not intensivId then return nil, 'Ungültige Kennung.' end
local neu = Zustand(daten.zustand)
if not neu then return nil, 'Unbekannter Zustand.' end
local begruendung = Text(daten.grund, 1000)
if not begruendung or #begruendung < 3 then
return nil, 'Bitte begründen, worauf sich die Einschätzung stützt.'
end
local eintrag = LadeAufenthalt(intensivId)
if not eintrag then return nil, 'Intensivaufenthalt nicht gefunden.' end
if eintrag.beendet_am then return nil, 'Dieser Aufenthalt ist bereits beendet.' end
MySQL.update.await('UPDATE medical_intensiv SET zustand = ? WHERE id = ?',
{ neu, intensivId })
-- Der Wortlaut des Arztes bleibt der Wortlaut des Arztes
SchreibeVerlauf(intensivId, begruendung, mitarbeiter.charakterId)
Protokoll.Schreibe(mitarbeiter, 'intensiv', 'Zustand eingetragen',
{ typ = 'intensiv', id = intensivId },
{ patient = eintrag.patient, von = eintrag.zustand, nach = neu }, begruendung)
-- Eine Verschlechterung auf kritisch erfährt das ganze Haus
if neu == 'kritisch' and eintrag.zustand ~= 'kritisch' then
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'kritisch',
titel = 'Intensivpatient als kritisch eingetragen',
text = ('%s %s — eingetragen von %s.')
:format(eintrag.firstname, eintrag.lastname, mitarbeiter.name),
zielTyp = 'intensiv', zielId = intensivId,
})
end
Kern.Loese('intensiv.zustand', intensivId, neu, mitarbeiter)
return { ok = true }
end)
-- =====================================================================
-- VERLAUF
-- =====================================================================
Kern.RegistriereAktion('intensiv', 'verlauf', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local intensivId = tonumber(daten.id)
if not intensivId then return nil, 'Ungültige Kennung.' end
local text = Text(daten.text, 1000)
if not text or #text < 3 then return nil, 'Bitte einen Text eingeben.' end
local eintrag = LadeAufenthalt(intensivId)
if not eintrag then return nil, 'Intensivaufenthalt nicht gefunden.' end
if eintrag.beendet_am then
return nil, 'Zu einem beendeten Aufenthalt lässt sich nichts mehr nachtragen.'
end
local verlaufId = SchreibeVerlauf(intensivId, text, mitarbeiter.charakterId)
Protokoll.Schreibe(mitarbeiter, 'intensiv', 'Verlaufseintrag geschrieben',
{ typ = 'intensiv', id = intensivId },
{ patient = eintrag.patient, eintrag = verlaufId })
Kern.Loese('intensiv.verlauf', intensivId, verlaufId, mitarbeiter)
return { id = verlaufId }
end)
-- =====================================================================
-- GERÄTE
--
-- Eine Liste von Namen. Das System schliesst nichts an, trennt nichts
-- und überwacht nichts — es weiss nur, was jemand notiert hat.
-- =====================================================================
Kern.RegistriereAktion('intensiv', 'geraete', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local intensivId = tonumber(daten.id)
if not intensivId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeAufenthalt(intensivId)
if not eintrag then return nil, 'Intensivaufenthalt nicht gefunden.' end
if eintrag.beendet_am then return nil, 'Dieser Aufenthalt ist bereits beendet.' end
-- Leere Eingabe heisst: es ist nichts mehr angeschlossen
local geraete = Text(daten.geraete, 512)
MySQL.update.await('UPDATE medical_intensiv SET geraete = ? WHERE id = ?',
{ geraete, intensivId })
Protokoll.Schreibe(mitarbeiter, 'intensiv', 'Geräte eingetragen',
{ typ = 'intensiv', id = intensivId },
{ patient = eintrag.patient, vorher = eintrag.geraete, nachher = geraete })
return { ok = true }
end)
-- =====================================================================
-- ZUSTÄNDIGKEIT
--
-- Verantwortung wird übernommen oder übertragen, nie zugeteilt.
-- =====================================================================
--- Wer im Dienst darf einen Intensivaufenthalt verantworten?
Kern.RegistriereAktion('intensiv', 'betreuer', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local liste = {}
for _, eintrag in pairs(Dienst.AlleImDienst()) do
if Rechte.Hat(eintrag, 'intensiv.betreuen') then
liste[#liste + 1] = {
charakterId = eintrag.charakterId,
name = eintrag.name,
rolleName = eintrag.rolleName,
personalnummer = eintrag.personalnummer,
funktion = eintrag.funktion,
status = eintrag.status,
selbst = eintrag.charakterId == mitarbeiter.charakterId,
}
end
end
table.sort(liste, function(a, b) return tostring(a.name) < tostring(b.name) end)
return { liste = liste }
end)
Kern.RegistriereAktion('intensiv', 'verantwortlich', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local intensivId = tonumber(daten.id)
if not intensivId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeAufenthalt(intensivId)
if not eintrag then return nil, 'Intensivaufenthalt nicht gefunden.' end
if eintrag.beendet_am then return nil, 'Dieser Aufenthalt ist bereits beendet.' end
-- Ohne Ziel wird die Zuständigkeit niedergelegt
local zielId = tonumber(daten.charakter)
if zielId then
local zielQuelle = Dienst.QuelleVonCharakter(zielId)
local ziel = zielQuelle and Dienst.Hole(zielQuelle)
if not ziel then return nil, 'Diese Person ist nicht im Dienst.' end
if not Rechte.Hat(ziel, 'intensiv.betreuen') then
return nil, 'Diese Person darf keinen Intensivaufenthalt verantworten.'
end
else
local grund = Text(daten.grund, 255)
if not grund then
return nil, 'Bitte angeben, warum die Zuständigkeit niedergelegt wird.'
end
end
MySQL.update.await('UPDATE medical_intensiv SET verantwortlich = ? WHERE id = ?',
{ zielId, intensivId })
Protokoll.Schreibe(mitarbeiter, 'intensiv',
zielId and 'Zuständigkeit übernommen oder übertragen' or 'Zuständigkeit niedergelegt',
{ typ = 'intensiv', id = intensivId },
{ patient = eintrag.patient, vorher = eintrag.verantwortlich, nachher = zielId },
Text(daten.grund, 255))
if zielId and zielId ~= mitarbeiter.charakterId then
-- Die betroffene Person erfährt es unmittelbar
Hinweise.Erstelle({ charakterId = zielId }, {
kategorie = 'patient', prioritaet = 'hoch',
titel = 'Du bist jetzt für einen Intensivpatienten zuständig',
text = ('%s %s — eingetragen von %s.')
:format(eintrag.firstname, eintrag.lastname, mitarbeiter.name),
zielTyp = 'intensiv', zielId = intensivId,
})
end
if not zielId then
Hinweise.Erstelle(nil, {
kategorie = 'patient', prioritaet = 'hoch',
titel = 'Intensivpatient ohne zuständigen Arzt',
text = ('Für %s %s ist niemand mehr eingetragen.')
:format(eintrag.firstname, eintrag.lastname),
zielTyp = 'intensiv', zielId = intensivId,
})
end
return { ok = true }
end)
-- =====================================================================
-- AUFENTHALT BEENDEN
--
-- Beendet ausschliesslich die Intensivbetreuung. Der Patient bleibt
-- aufgenommen, sein Bett bleibt belegt, und keine Behandlung wird
-- dadurch abgeschlossen.
-- =====================================================================
Kern.RegistriereAktion('intensiv', 'beenden', 'intensiv.betreuen', function(quelle, mitarbeiter, daten)
local intensivId = tonumber(daten.id)
if not intensivId then return nil, 'Ungültige Kennung.' end
local grund = Text(daten.grund, 1000)
if not grund or #grund < 5 then
return nil, 'Das Ende der Intensivbetreuung erfordert eine Begründung.'
end
local eintrag = LadeAufenthalt(intensivId)
if not eintrag then return nil, 'Intensivaufenthalt nicht gefunden.' end
if eintrag.beendet_am then return nil, 'Dieser Aufenthalt ist bereits beendet.' end
-- Ein kritischer Zustand hält nicht auf, wird aber festgehalten
local warKritisch = eintrag.zustand == 'kritisch'
MySQL.update.await([[
UPDATE medical_intensiv
SET beendet_am = NOW(), beendet_von = ?
WHERE id = ?
]], { mitarbeiter.charakterId, intensivId })
-- Die Begründung schliesst den Verlauf mit den Worten des Arztes ab
SchreibeVerlauf(intensivId, grund, mitarbeiter.charakterId)
Protokoll.Schreibe(mitarbeiter, 'intensiv', 'Intensivaufenthalt beendet',
{ typ = 'intensiv', id = intensivId },
{ patient = eintrag.patient, zustand = eintrag.zustand,
warKritisch = warKritisch }, grund)
Hinweise.Erstelle(nil, {
kategorie = 'patient',
prioritaet = warKritisch and 'hoch' or 'normal',
titel = 'Intensivbetreuung beendet',
text = ('%s %s — der Patient bleibt aufgenommen, Bett und Behandlungen sind unberührt.')
:format(eintrag.firstname, eintrag.lastname),
zielTyp = eintrag.aufnahme and 'aufnahme' or 'intensiv',
zielId = eintrag.aufnahme or intensivId,
})
Kern.Loese('intensiv.beendet', intensivId, mitarbeiter)
return { ok = true, warKritisch = warKritisch }
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Ressourcen
-- ---------------------------------------------------------------------
--- Liegt dieser Patient gerade auf der Intensivstation?
--- Reine Auskunft — keine Ressource kann darüber jemanden aufnehmen,
--- entlassen oder seinen Zustand ändern.
--- @param patientId number Charakter
--- @return table|nil { id, zustand, zimmer, seit, verantwortlich }
exports('HoleIntensivaufenthalt', function(patientId)
if not tonumber(patientId) then return nil end
local zeilen = MySQL.query.await([[
SELECT i.id, i.zustand, i.aufgenommen_am, i.verantwortlich,
z.kennung AS zimmer
FROM medical_intensiv i
LEFT JOIN medical_zimmer z ON z.id = i.zimmer
WHERE i.patient = ? AND i.beendet_am IS NULL
ORDER BY i.id DESC LIMIT 1
]], { tonumber(patientId) })
if not zeilen or not zeilen[1] then return nil end
return {
id = zeilen[1].id,
zustand = zeilen[1].zustand,
zimmer = zeilen[1].zimmer,
seit = zeilen[1].aufgenommen_am,
verantwortlich = zeilen[1].verantwortlich
and NameVon(zeilen[1].verantwortlich) or nil,
}
end)