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

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)