- 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>
808 lines
32 KiB
Lua
808 lines
32 KiB
Lua
-- =====================================================================
|
|
-- aether-medical / Modul: Operationsverwaltung
|
|
--
|
|
-- Operationen werden vollständig manuell dokumentiert.
|
|
--
|
|
-- Das System führt keine Operation durch, startet keine, beendet keine
|
|
-- und entscheidet nie über einen Ausgang. Der geplante Zeitpunkt ist
|
|
-- eine Erinnerung — er beginnt nichts. Beginn und Ende trägt das
|
|
-- OP-Team ein, wenn es soweit ist.
|
|
--
|
|
-- Überschneidungen im OP-Saal werden GEMELDET, nicht verhindert.
|
|
-- Ob zwei Eingriffe im selben Saal ein Problem sind, weiss nur ein
|
|
-- Mensch — vielleicht dauert der erste kürzer als geplant.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereModul({
|
|
id = 'operation', name = 'Operationen', icon = '🔪',
|
|
reihenfolge = 7,
|
|
recht = { 'operation.planen', 'operation.dokumentieren' },
|
|
beschreibung = 'OP-Planung, OP-Team und Operationsbericht',
|
|
})
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- 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
|
|
|
|
--- Name eines Mitarbeiters für die Anzeige
|
|
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 Funktion im OP-Team
|
|
--- @param kennung any Eingabe
|
|
--- @return string
|
|
local function TeamFunktion(kennung)
|
|
for _, eintrag in ipairs(Medizin.Config.OpFunktionen) do
|
|
if eintrag.id == kennung then return eintrag.id end
|
|
end
|
|
return 'assistenz'
|
|
end
|
|
|
|
--- Lädt das OP-Team einer Operation
|
|
--- @param operationId number Operation
|
|
--- @return table
|
|
local function LadeTeam(operationId)
|
|
local team = MySQL.query.await([[
|
|
SELECT t.`character`, t.funktion,
|
|
c.firstname, c.lastname, p.personalnummer, p.rolle
|
|
FROM medical_operation_team t
|
|
LEFT JOIN aether_characters c ON c.id = t.`character`
|
|
LEFT JOIN medical_personal p ON p.`character` = t.`character`
|
|
WHERE t.operation = ?
|
|
]], { operationId }) or {}
|
|
|
|
local funktionsNamen = {}
|
|
for _, eintrag in ipairs(Medizin.Config.OpFunktionen) do
|
|
funktionsNamen[eintrag.id] = eintrag.name
|
|
end
|
|
|
|
for _, mitglied in ipairs(team) do
|
|
mitglied.name = (mitglied.firstname or 'Unbekannt') .. ' ' .. (mitglied.lastname or '')
|
|
local rolle = Medizin.HoleRolle(mitglied.rolle)
|
|
mitglied.rolleName = rolle and rolle.name or mitglied.rolle
|
|
mitglied.funktionName = funktionsNamen[mitglied.funktion] or mitglied.funktion
|
|
mitglied.firstname, mitglied.lastname = nil, nil
|
|
end
|
|
|
|
-- Operateur zuerst, dann der Rest
|
|
table.sort(team, function(a, b)
|
|
if a.funktion == b.funktion then return (a.name or '') < (b.name or '') end
|
|
return a.funktion == 'operateur'
|
|
end)
|
|
|
|
return team
|
|
end
|
|
|
|
--- Sucht Operationen, die sich im selben Saal zeitlich überschneiden.
|
|
--- Reine Auskunft — das Ergebnis verhindert nichts.
|
|
--- @param saalId number Zimmer
|
|
--- @param geplantFuer string|nil Zeitpunkt
|
|
--- @param ohneId number|nil Eigene Operation ausklammern
|
|
--- @return table
|
|
local function Ueberschneidungen(saalId, geplantFuer, ohneId)
|
|
if not saalId then return {} end
|
|
|
|
local treffer = MySQL.query.await([[
|
|
SELECT o.id, o.kennung, o.art, o.status, o.geplant_fuer, o.beginn,
|
|
c.firstname, c.lastname
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
WHERE o.saal = ?
|
|
AND o.status IN ('geplant', 'laufend')
|
|
AND (? IS NULL OR o.id <> ?)
|
|
AND (
|
|
o.status = 'laufend'
|
|
OR (? IS NOT NULL AND o.geplant_fuer IS NOT NULL
|
|
AND ABS(TIMESTAMPDIFF(MINUTE, o.geplant_fuer, ?)) < 120)
|
|
)
|
|
ORDER BY o.geplant_fuer
|
|
]], { saalId, ohneId, ohneId, geplantFuer, geplantFuer }) or {}
|
|
|
|
for _, eintrag in ipairs(treffer) do
|
|
eintrag.patientName = (eintrag.firstname or '') .. ' ' .. (eintrag.lastname or '')
|
|
eintrag.firstname, eintrag.lastname = nil, nil
|
|
end
|
|
|
|
return treffer
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- ÜBERSICHT UND OP-PLAN
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('operation', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
|
|
local ordner = tostring(daten.ordner or 'plan')
|
|
|
|
local bedingung
|
|
if ordner == 'archiv' then
|
|
bedingung = "o.status IN ('abgeschlossen', 'abgesagt')"
|
|
elseif ordner == 'laufend' then
|
|
bedingung = "o.status = 'laufend'"
|
|
else
|
|
bedingung = "o.status IN ('geplant', 'laufend')"
|
|
end
|
|
|
|
local liste = MySQL.query.await(([[
|
|
SELECT o.id, o.kennung, o.patient, o.art, o.saal, o.status,
|
|
o.geplant_fuer, o.beginn, o.ende, o.erstellt_von, o.komplikationen,
|
|
TIMESTAMPDIFF(MINUTE, NOW(), o.geplant_fuer) AS restMinuten,
|
|
TIMESTAMPDIFF(MINUTE, o.beginn, COALESCE(o.ende, NOW())) AS dauerMinuten,
|
|
c.firstname, c.lastname, c.dob,
|
|
m.blutgruppe, m.allergien, m.hinweis,
|
|
z.kennung AS saalName,
|
|
(SELECT COUNT(*) FROM medical_operation_team t WHERE t.operation = o.id) AS teamGroesse,
|
|
(SELECT COUNT(*) FROM medical_operation_team t
|
|
WHERE t.operation = o.id AND t.`character` = ?) AS ichDabei
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
LEFT JOIN medical_patient m ON m.`character` = o.patient
|
|
LEFT JOIN medical_zimmer z ON z.id = o.saal
|
|
WHERE %s
|
|
ORDER BY
|
|
CASE o.status WHEN 'laufend' THEN 0 ELSE 1 END,
|
|
o.geplant_fuer IS NULL, o.geplant_fuer, o.id DESC
|
|
LIMIT 120
|
|
]]):format(bedingung), { mitarbeiter.charakterId }) or {}
|
|
|
|
for _, eintrag in ipairs(liste) do
|
|
eintrag.patientName = eintrag.firstname .. ' ' .. eintrag.lastname
|
|
eintrag.geburt = tostring(eintrag.dob):sub(1, 10)
|
|
eintrag.erstelltVon = NameVon(eintrag.erstellt_von)
|
|
eintrag.ichDabei = tonumber(eintrag.ichDabei) > 0
|
|
eintrag.teamGroesse = tonumber(eintrag.teamGroesse) or 0
|
|
eintrag.hatKomplikationen = eintrag.komplikationen ~= nil
|
|
eintrag.firstname, eintrag.lastname, eintrag.dob = nil, nil, nil
|
|
eintrag.erstellt_von, eintrag.komplikationen = nil, nil
|
|
end
|
|
|
|
-- OP-Säle mit ihrer aktuellen Lage
|
|
local saele = MySQL.query.await([[
|
|
SELECT z.id, z.kennung, z.status, z.hinweis, s.name AS stationName
|
|
FROM medical_zimmer z
|
|
JOIN medical_station s ON s.id = z.station
|
|
WHERE z.art = 'op'
|
|
ORDER BY z.kennung
|
|
]]) or {}
|
|
|
|
for _, saal in ipairs(saele) do
|
|
local laufend = MySQL.query.await([[
|
|
SELECT o.id, o.kennung, o.art, o.beginn,
|
|
TIMESTAMPDIFF(MINUTE, o.beginn, NOW()) AS dauer,
|
|
c.firstname, c.lastname
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
WHERE o.saal = ? AND o.status = 'laufend'
|
|
ORDER BY o.beginn LIMIT 1
|
|
]], { saal.id })
|
|
|
|
if laufend and laufend[1] then
|
|
saal.laufend = {
|
|
id = laufend[1].id, kennung = laufend[1].kennung,
|
|
art = laufend[1].art, dauer = tonumber(laufend[1].dauer) or 0,
|
|
patientName = laufend[1].firstname .. ' ' .. laufend[1].lastname,
|
|
}
|
|
end
|
|
|
|
saal.geplant = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_operation
|
|
WHERE saal = ? AND status = 'geplant'
|
|
]], { saal.id })) or 0
|
|
end
|
|
|
|
return {
|
|
ordner = ordner,
|
|
liste = liste,
|
|
saele = saele,
|
|
funktionen = Medizin.Config.OpFunktionen,
|
|
zaehler = {
|
|
geplant = tonumber(MySQL.scalar.await(
|
|
"SELECT COUNT(*) FROM medical_operation WHERE status = 'geplant'")) or 0,
|
|
laufend = tonumber(MySQL.scalar.await(
|
|
"SELECT COUNT(*) FROM medical_operation WHERE status = 'laufend'")) or 0,
|
|
faellig = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_operation
|
|
WHERE status = 'geplant' AND geplant_fuer IS NOT NULL AND geplant_fuer <= NOW()
|
|
]])) or 0,
|
|
heute = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM medical_operation
|
|
WHERE status = 'abgeschlossen' AND DATE(ende) = CURDATE()
|
|
]])) or 0,
|
|
},
|
|
rechte = {
|
|
planen = Rechte.Hat(mitarbeiter, 'operation.planen'),
|
|
dokumentieren = Rechte.Hat(mitarbeiter, 'operation.dokumentieren'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
Kern.RegistriereAktion('operation', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT o.*, c.firstname, c.lastname, c.dob,
|
|
m.blutgruppe, m.allergien, m.vorerkrankungen, m.dauermedikation,
|
|
m.implantate, m.hinweis, m.lichtbild,
|
|
z.kennung AS saalName,
|
|
a.grund AS aufnahmeGrund, a.status AS aufnahmeStatus
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
LEFT JOIN medical_patient m ON m.`character` = o.patient
|
|
LEFT JOIN medical_zimmer z ON z.id = o.saal
|
|
LEFT JOIN medical_aufnahme a ON a.id = o.aufnahme
|
|
WHERE o.id = ?
|
|
]], { operationId })
|
|
|
|
if not zeilen or not zeilen[1] then return nil, 'Operation nicht gefunden.' end
|
|
local o = zeilen[1]
|
|
|
|
local team = LadeTeam(operationId)
|
|
local ichDabei = false
|
|
for _, mitglied in ipairs(team) do
|
|
if mitglied.character == mitarbeiter.charakterId then ichDabei = true end
|
|
end
|
|
|
|
-- Verfügbare OP-Säle für eine Umplanung
|
|
local saele = MySQL.query.await([[
|
|
SELECT z.id, z.kennung, z.status FROM medical_zimmer z
|
|
WHERE z.art = 'op' ORDER BY z.kennung
|
|
]]) or {}
|
|
|
|
return {
|
|
operation = {
|
|
id = o.id, kennung = o.kennung, patient = o.patient,
|
|
patientName = o.firstname .. ' ' .. o.lastname,
|
|
geburt = tostring(o.dob):sub(1, 10),
|
|
art = o.art, saal = o.saal, saalName = o.saalName,
|
|
aufnahme = o.aufnahme, aufnahmeGrund = o.aufnahmeGrund,
|
|
geplantFuer = o.geplant_fuer,
|
|
beginn = o.beginn, ende = o.ende,
|
|
verlauf = o.verlauf, komplikationen = o.komplikationen,
|
|
nachsorge = o.nachsorge,
|
|
status = o.status,
|
|
erstelltAm = o.erstellt_am,
|
|
erstelltVon = NameVon(o.erstellt_von),
|
|
},
|
|
stamm = {
|
|
blutgruppe = o.blutgruppe, allergien = o.allergien,
|
|
vorerkrankungen = o.vorerkrankungen, dauermedikation = o.dauermedikation,
|
|
implantate = o.implantate, hinweis = o.hinweis, lichtbild = o.lichtbild,
|
|
},
|
|
team = team,
|
|
ichDabei = ichDabei,
|
|
saele = saele,
|
|
funktionen = Medizin.Config.OpFunktionen,
|
|
-- Zeitgleiche Eingriffe im selben Saal — als Hinweis, nicht als Sperre
|
|
ueberschneidungen = Ueberschneidungen(o.saal, o.geplant_fuer, o.id),
|
|
rechte = {
|
|
planen = Rechte.Hat(mitarbeiter, 'operation.planen'),
|
|
dokumentieren = Rechte.Hat(mitarbeiter, 'operation.dokumentieren'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- PLANEN
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('operation', 'patientSuche', 'operation.planen', function(quelle, mitarbeiter, daten)
|
|
local begriff = type(daten.begriff) == 'string' and daten.begriff or ''
|
|
if #begriff < 2 then return { treffer = {}, saele = {} } 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.id FROM medical_aufnahme a
|
|
WHERE a.patient = c.id AND a.status = 'aufgenommen'
|
|
ORDER BY a.id DESC LIMIT 1) AS aufnahme
|
|
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
|
|
|
|
local saele = MySQL.query.await([[
|
|
SELECT z.id, z.kennung, z.status,
|
|
(SELECT COUNT(*) FROM medical_operation o
|
|
WHERE o.saal = z.id AND o.status = 'laufend') AS belegt
|
|
FROM medical_zimmer z WHERE z.art = 'op' ORDER BY z.kennung
|
|
]]) or {}
|
|
|
|
-- Kollegen im Dienst als mögliche Teammitglieder
|
|
local kollegen = {}
|
|
for _, eintrag in pairs(Dienst.AlleImDienst()) do
|
|
kollegen[#kollegen + 1] = {
|
|
charakterId = eintrag.charakterId,
|
|
name = eintrag.name,
|
|
personalnummer = eintrag.personalnummer,
|
|
rolleName = eintrag.rolleName,
|
|
}
|
|
end
|
|
table.sort(kollegen, function(a, b) return a.personalnummer < b.personalnummer end)
|
|
|
|
return { treffer = treffer, saele = saele, kollegen = kollegen }
|
|
end)
|
|
|
|
--- Kollegen im Dienst, die sich ins OP-Team einteilen lassen
|
|
Kern.RegistriereAktion('operation', 'kollegen', 'operation.planen', function(quelle, mitarbeiter)
|
|
local kollegen = {}
|
|
for _, eintrag in pairs(Dienst.AlleImDienst()) do
|
|
kollegen[#kollegen + 1] = {
|
|
charakterId = eintrag.charakterId,
|
|
name = eintrag.name,
|
|
personalnummer = eintrag.personalnummer,
|
|
rolleName = eintrag.rolleName,
|
|
funktion = eintrag.funktion,
|
|
}
|
|
end
|
|
table.sort(kollegen, function(a, b) return a.personalnummer < b.personalnummer end)
|
|
|
|
return { kollegen = kollegen, funktionen = Medizin.Config.OpFunktionen }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('operation', 'planen', 'operation.planen', function(quelle, mitarbeiter, daten)
|
|
local patientId = tonumber(daten.patient)
|
|
if not patientId then return nil, 'Kein Patient gewählt.' 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
|
|
|
|
local art = Text(daten.art, 160)
|
|
if not art or #art < 3 then return nil, 'Bitte die Art des Eingriffs angeben.' end
|
|
|
|
-- OP-Saal ist freiwillig, muss aber existieren
|
|
local saalId = nil
|
|
if daten.saal and tonumber(daten.saal) then
|
|
local saal = MySQL.query.await(
|
|
"SELECT id, kennung, status FROM medical_zimmer WHERE id = ? AND art = 'op'",
|
|
{ tonumber(daten.saal) })
|
|
if not saal or not saal[1] then return nil, 'Unbekannter OP-Saal.' end
|
|
if saal[1].status == 'gesperrt' then
|
|
return nil, ('OP-Saal %s ist gesperrt.'):format(saal[1].kennung)
|
|
end
|
|
saalId = saal[1].id
|
|
end
|
|
|
|
-- Zeitpunkt in Minuten ab jetzt. 0 heisst: keine Zeitplanung.
|
|
local vorlauf = math.floor(tonumber(daten.vorlaufMinuten) or 0)
|
|
if vorlauf < 0 or vorlauf > 20160 then
|
|
return nil, 'Der Vorlauf muss zwischen 0 Minuten und 14 Tagen liegen.'
|
|
end
|
|
|
|
local aufnahmeId = tonumber(daten.aufnahme)
|
|
if aufnahmeId then
|
|
local gueltig = MySQL.scalar.await(
|
|
'SELECT id FROM medical_aufnahme WHERE id = ? AND patient = ?',
|
|
{ aufnahmeId, patientId })
|
|
if not gueltig then aufnahmeId = nil end
|
|
end
|
|
|
|
local kennung = Kern.ErzeugeKennung('OP', 'medical_operation', 'kennung')
|
|
|
|
local id = MySQL.insert.await([[
|
|
INSERT INTO medical_operation
|
|
(kennung, patient, aufnahme, art, saal, geplant_fuer, status, erstellt_von)
|
|
VALUES (?, ?, ?, ?, ?,
|
|
CASE WHEN ? > 0 THEN DATE_ADD(NOW(), INTERVAL ? MINUTE) ELSE NULL END,
|
|
'geplant', ?)
|
|
]], { kennung, patientId, aufnahmeId, art, saalId, vorlauf, vorlauf,
|
|
mitarbeiter.charakterId })
|
|
|
|
-- Der Planende gehört als Operateur ins Team, sofern nicht anders gewünscht
|
|
if daten.selbstOperateur ~= false then
|
|
MySQL.query.await([[
|
|
INSERT INTO medical_operation_team (operation, `character`, funktion)
|
|
VALUES (?, ?, 'operateur')
|
|
ON DUPLICATE KEY UPDATE funktion = VALUES(funktion)
|
|
]], { id, mitarbeiter.charakterId })
|
|
end
|
|
|
|
-- Weitere Teammitglieder aus der Planung
|
|
if type(daten.team) == 'table' then
|
|
for _, mitglied in ipairs(daten.team) do
|
|
local charakterId = tonumber(mitglied.charakterId)
|
|
if charakterId and charakterId ~= mitarbeiter.charakterId then
|
|
MySQL.query.await([[
|
|
INSERT INTO medical_operation_team (operation, `character`, funktion)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE funktion = VALUES(funktion)
|
|
]], { id, charakterId, TeamFunktion(mitglied.funktion) })
|
|
end
|
|
end
|
|
end
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'Operation geplant',
|
|
{ typ = 'operation', id = id },
|
|
{ kennung = kennung, patient = patientId, art = art,
|
|
saal = saalId, vorlaufMinuten = vorlauf })
|
|
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'op', prioritaet = 'normal',
|
|
titel = ('Operation geplant · %s'):format(kennung),
|
|
text = ('%s %s — %s%s'):format(person[1].firstname, person[1].lastname, art,
|
|
vorlauf > 0 and (' (in %d Minuten)'):format(vorlauf) or ' (ohne Zeitangabe)'),
|
|
zielTyp = 'operation', zielId = id,
|
|
})
|
|
|
|
Kern.Loese('operation.geplant', id, mitarbeiter)
|
|
|
|
return {
|
|
id = id, kennung = kennung,
|
|
ueberschneidungen = saalId and #Ueberschneidungen(saalId, nil, id) or 0,
|
|
}
|
|
end)
|
|
|
|
--- Zeitpunkt oder Saal einer geplanten Operation ändern
|
|
Kern.RegistriereAktion('operation', 'umplanen', 'operation.planen', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, kennung, status, saal, geplant_fuer FROM medical_operation WHERE id = ?',
|
|
{ operationId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Operation nicht gefunden.' end
|
|
|
|
local operation = zeilen[1]
|
|
if operation.status ~= 'geplant' then
|
|
return nil, 'Nur geplante Operationen lassen sich umplanen.'
|
|
end
|
|
|
|
local grund = Text(daten.grund, 255)
|
|
if not grund then return nil, 'Bitte begründen, warum umgeplant wird.' end
|
|
|
|
local saalId = nil
|
|
if daten.saal and tonumber(daten.saal) then
|
|
local saal = MySQL.query.await(
|
|
"SELECT id, kennung, status FROM medical_zimmer WHERE id = ? AND art = 'op'",
|
|
{ tonumber(daten.saal) })
|
|
if not saal or not saal[1] then return nil, 'Unbekannter OP-Saal.' end
|
|
if saal[1].status == 'gesperrt' then
|
|
return nil, ('OP-Saal %s ist gesperrt.'):format(saal[1].kennung)
|
|
end
|
|
saalId = saal[1].id
|
|
end
|
|
|
|
local vorlauf = math.floor(tonumber(daten.vorlaufMinuten) or 0)
|
|
if vorlauf < 0 or vorlauf > 20160 then
|
|
return nil, 'Der Vorlauf muss zwischen 0 Minuten und 14 Tagen liegen.'
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_operation
|
|
SET saal = ?,
|
|
geplant_fuer = CASE WHEN ? > 0 THEN DATE_ADD(NOW(), INTERVAL ? MINUTE) ELSE NULL END
|
|
WHERE id = ?
|
|
]], { saalId, vorlauf, vorlauf, operationId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'Operation umgeplant',
|
|
{ typ = 'operation', id = operationId },
|
|
{ kennung = operation.kennung, saal = saalId, vorlaufMinuten = vorlauf }, grund)
|
|
|
|
Kern.Loese('operation.umgeplant', operationId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('operation', 'absagen', 'operation.planen', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local grund = Text(daten.grund, 512)
|
|
if not grund or #grund < 5 then return nil, 'Eine Absage erfordert eine Begründung.' end
|
|
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT o.id, o.kennung, o.status, o.art, c.firstname, c.lastname
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
WHERE o.id = ?
|
|
]], { operationId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Operation nicht gefunden.' end
|
|
|
|
local operation = zeilen[1]
|
|
if operation.status == 'abgeschlossen' then
|
|
return nil, 'Eine abgeschlossene Operation lässt sich nicht absagen.'
|
|
end
|
|
if operation.status == 'abgesagt' then
|
|
return nil, 'Diese Operation ist bereits abgesagt.'
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_operation
|
|
SET status = 'abgesagt',
|
|
verlauf = CONCAT(COALESCE(verlauf, ''), ?)
|
|
WHERE id = ?
|
|
]], { '\n[Abgesagt] ' .. grund, operationId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'Operation abgesagt',
|
|
{ typ = 'operation', id = operationId },
|
|
{ kennung = operation.kennung, warLaufend = operation.status == 'laufend' }, grund)
|
|
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'op', prioritaet = 'normal',
|
|
titel = ('Operation abgesagt · %s'):format(operation.kennung),
|
|
text = ('%s %s — %s'):format(operation.firstname, operation.lastname, grund),
|
|
zielTyp = 'operation', zielId = operationId,
|
|
})
|
|
|
|
Kern.Loese('operation.abgeschlossen', operationId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- OP-TEAM
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('operation', 'teamSetzen', 'operation.planen', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
local charakterId = tonumber(daten.charakterId)
|
|
|
|
if not operationId or not charakterId then return nil, 'Unvollständige Angaben.' end
|
|
|
|
local operation = MySQL.query.await(
|
|
'SELECT id, kennung, status FROM medical_operation WHERE id = ?', { operationId })
|
|
if not operation or not operation[1] then return nil, 'Operation nicht gefunden.' end
|
|
if operation[1].status == 'abgeschlossen' or operation[1].status == 'abgesagt' then
|
|
return nil, 'Das Team einer abgeschlossenen Operation lässt sich nicht ändern.'
|
|
end
|
|
|
|
local personal = MySQL.scalar.await(
|
|
'SELECT `character` FROM medical_personal WHERE `character` = ? AND aktiv = 1',
|
|
{ charakterId })
|
|
if not personal then return nil, 'Diese Person gehört nicht zum Personal.' end
|
|
|
|
local funktion = TeamFunktion(daten.funktion)
|
|
|
|
MySQL.query.await([[
|
|
INSERT INTO medical_operation_team (operation, `character`, funktion)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE funktion = VALUES(funktion)
|
|
]], { operationId, charakterId, funktion })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'OP-Team geändert',
|
|
{ typ = 'operation', id = operationId },
|
|
{ kennung = operation[1].kennung, mitglied = charakterId, funktion = funktion })
|
|
|
|
-- Das Teammitglied erfährt davon; teilnehmen muss es trotzdem selbst
|
|
Hinweise.Erstelle({ charakterId = charakterId }, {
|
|
kategorie = 'op', prioritaet = 'normal',
|
|
titel = ('Du bist im OP-Team · %s'):format(operation[1].kennung),
|
|
text = ('Eingeteilt als %s durch %s.')
|
|
:format(funktion, mitarbeiter.name),
|
|
zielTyp = 'operation', zielId = operationId,
|
|
})
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('operation', 'teamEntfernen', 'operation.planen', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
local charakterId = tonumber(daten.charakterId)
|
|
|
|
if not operationId or not charakterId then return nil, 'Unvollständige Angaben.' end
|
|
|
|
MySQL.query.await(
|
|
'DELETE FROM medical_operation_team WHERE operation = ? AND `character` = ?',
|
|
{ operationId, charakterId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'Aus OP-Team entfernt',
|
|
{ typ = 'operation', id = operationId }, { mitglied = charakterId },
|
|
Text(daten.grund, 255))
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
--- Selbst dazustossen oder das Team verlassen
|
|
Kern.RegistriereAktion('operation', 'teamWechsel', 'operation.dokumentieren', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local operation = MySQL.query.await(
|
|
'SELECT id, kennung, status FROM medical_operation WHERE id = ?', { operationId })
|
|
if not operation or not operation[1] then return nil, 'Operation nicht gefunden.' end
|
|
if operation[1].status == 'abgeschlossen' or operation[1].status == 'abgesagt' then
|
|
return nil, 'Diese Operation ist abgeschlossen.'
|
|
end
|
|
|
|
if daten.beitreten == false then
|
|
MySQL.query.await(
|
|
'DELETE FROM medical_operation_team WHERE operation = ? AND `character` = ?',
|
|
{ operationId, mitarbeiter.charakterId })
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'OP-Team verlassen',
|
|
{ typ = 'operation', id = operationId }, { kennung = operation[1].kennung })
|
|
return { ok = true, dabei = false }
|
|
end
|
|
|
|
MySQL.query.await([[
|
|
INSERT INTO medical_operation_team (operation, `character`, funktion)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE funktion = VALUES(funktion)
|
|
]], { operationId, mitarbeiter.charakterId, TeamFunktion(daten.funktion) })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'OP-Team beigetreten',
|
|
{ typ = 'operation', id = operationId },
|
|
{ kennung = operation[1].kennung, funktion = TeamFunktion(daten.funktion) })
|
|
|
|
return { ok = true, dabei = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- DURCHFÜHRUNG
|
|
--
|
|
-- Beginn und Ende trägt das OP-Team ein. Der geplante Zeitpunkt
|
|
-- startet nichts — er erinnert nur.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('operation', 'beginnen', 'operation.dokumentieren', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT o.id, o.kennung, o.status, o.art, o.saal,
|
|
c.firstname, c.lastname
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
WHERE o.id = ?
|
|
]], { operationId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Operation nicht gefunden.' end
|
|
|
|
local operation = zeilen[1]
|
|
if operation.status ~= 'geplant' then
|
|
return nil, 'Nur eine geplante Operation lässt sich beginnen.'
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_operation SET status = 'laufend', beginn = NOW() WHERE id = ?
|
|
]], { operationId })
|
|
|
|
-- Wer den Beginn einträgt, gehört ins Team
|
|
MySQL.query.await([[
|
|
INSERT INTO medical_operation_team (operation, `character`, funktion)
|
|
VALUES (?, ?, 'assistenz')
|
|
ON DUPLICATE KEY UPDATE funktion = funktion
|
|
]], { operationId, mitarbeiter.charakterId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'Operation begonnen',
|
|
{ typ = 'operation', id = operationId }, { kennung = operation.kennung })
|
|
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'op', prioritaet = 'normal',
|
|
titel = ('Operation begonnen · %s'):format(operation.kennung),
|
|
text = ('%s %s — %s'):format(operation.firstname, operation.lastname, operation.art),
|
|
zielTyp = 'operation', zielId = operationId,
|
|
})
|
|
|
|
Kern.Loese('operation.begonnen', operationId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
--- Verlauf, Komplikationen und Nachsorge dokumentieren
|
|
Kern.RegistriereAktion('operation', 'dokumentieren', 'operation.dokumentieren', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT id, kennung, status FROM medical_operation WHERE id = ?', { operationId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Operation nicht gefunden.' end
|
|
|
|
if zeilen[1].status == 'abgesagt' then
|
|
return nil, 'Diese Operation wurde abgesagt.'
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_operation
|
|
SET art = COALESCE(?, art), verlauf = ?, komplikationen = ?, nachsorge = ?
|
|
WHERE id = ?
|
|
]], {
|
|
Text(daten.art, 160),
|
|
Text(daten.verlauf, 8000),
|
|
Text(daten.komplikationen, 4000),
|
|
Text(daten.nachsorge, 4000),
|
|
operationId,
|
|
})
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'OP-Bericht gespeichert',
|
|
{ typ = 'operation', id = operationId }, { kennung = zeilen[1].kennung })
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('operation', 'beenden', 'operation.dokumentieren', function(quelle, mitarbeiter, daten)
|
|
local operationId = tonumber(daten.id)
|
|
if not operationId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT o.id, o.kennung, o.status, o.verlauf, o.beginn,
|
|
c.firstname, c.lastname
|
|
FROM medical_operation o
|
|
JOIN aether_characters c ON c.id = o.patient
|
|
WHERE o.id = ?
|
|
]], { operationId })
|
|
if not zeilen or not zeilen[1] then return nil, 'Operation nicht gefunden.' end
|
|
|
|
local operation = zeilen[1]
|
|
if operation.status ~= 'laufend' then
|
|
return nil, 'Nur eine laufende Operation lässt sich beenden.'
|
|
end
|
|
|
|
-- Ein OP-Bericht ohne Verlauf ist kein Bericht.
|
|
-- Geprüft wird nur, DASS etwas dasteht — nicht, was.
|
|
local verlauf = Text(daten.verlauf, 8000) or operation.verlauf
|
|
if not verlauf then
|
|
return nil, 'Bitte zuerst den Verlauf dokumentieren.'
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_operation
|
|
SET status = 'abgeschlossen', ende = NOW(),
|
|
verlauf = ?, komplikationen = ?, nachsorge = ?
|
|
WHERE id = ?
|
|
]], {
|
|
verlauf,
|
|
Text(daten.komplikationen, 4000),
|
|
Text(daten.nachsorge, 4000),
|
|
operationId,
|
|
})
|
|
|
|
local team = LadeTeam(operationId)
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'operation', 'Operation abgeschlossen',
|
|
{ typ = 'operation', id = operationId },
|
|
{ kennung = operation.kennung, teamGroesse = #team,
|
|
mitKomplikationen = Text(daten.komplikationen, 4000) ~= nil })
|
|
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'op', prioritaet = 'normal',
|
|
titel = ('Operation abgeschlossen · %s'):format(operation.kennung),
|
|
text = ('%s %s — OP-Saal wird frei.')
|
|
:format(operation.firstname, operation.lastname),
|
|
zielTyp = 'operation', zielId = operationId,
|
|
})
|
|
|
|
Kern.Loese('operation.abgeschlossen', operationId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Schnittstelle für andere Ressourcen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Liefert die Operationen eines Patienten
|
|
--- @param patientId number Charakter
|
|
--- @return table
|
|
exports('HoleOperationen', function(patientId)
|
|
if not tonumber(patientId) then return {} end
|
|
return MySQL.query.await([[
|
|
SELECT kennung, art, status, geplant_fuer, beginn, ende,
|
|
verlauf, komplikationen, nachsorge
|
|
FROM medical_operation
|
|
WHERE patient = ?
|
|
ORDER BY id DESC LIMIT 50
|
|
]], { tonumber(patientId) }) or {}
|
|
end)
|