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

736 lines
29 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Medizinisches MDT
--
-- Das Herzstück: Patientensuche, Stammdaten und Patientenakte.
--
-- Das MDT zeigt an und verknüpft. Es stellt keine Diagnose, es leitet
-- aus Allergien oder Vorerkrankungen keine Warnung her, die eine
-- Behandlung verhindert, und es bewertet keinen Befund. Ein roter
-- Hinweis bedeutet hier immer: ein Mediziner hat ihn eingetragen.
-- =====================================================================
Kern.RegistriereModul({
id = 'mdt', name = 'MDT', icon = '🩺',
reihenfolge = 2, recht = 'patient.suchen',
beschreibung = 'Patientensuche, Stammdaten und Akten',
})
-- Zulässige Angaben zum Organspendeausweis
local ORGANSTATUS = { ja = 'Spender', nein = 'Kein Spender', unbekannt = 'Unbekannt' }
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
--- Prüft einen Suchbegriff
--- @param begriff any Eingabe der Oberfläche
--- @return boolean
local function SuchbegriffGueltig(begriff)
return type(begriff) == 'string' and #begriff >= 2
end
--- Begrenzt einen Text und liefert nil bei leerer Eingabe
--- @param wert any Eingabe
--- @param laenge number Höchstlänge
--- @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
--- Lädt den Namen 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 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
.. (zeilen[1].personalnummer and (' (' .. zeilen[1].personalnummer .. ')') or '')
end
-- =====================================================================
-- PATIENTENSUCHE
-- =====================================================================
Kern.RegistriereAktion('mdt', 'suche', 'patient.suchen', function(quelle, mitarbeiter, daten)
if not SuchbegriffGueltig(daten.begriff) then
return nil, 'Bitte mindestens zwei Zeichen eingeben.'
end
local muster = '%' .. daten.begriff .. '%'
-- Sucht über Name, Geburtsdatum und Telefonnummer
local treffer = MySQL.query.await([[
SELECT c.id, c.firstname, c.lastname, c.dob, c.gender,
m.blutgruppe, m.allergien, m.hinweis, m.lichtbild,
t.number AS telefon,
(SELECT COUNT(*) FROM medical_aufnahme a
WHERE a.patient = c.id AND a.status = 'aufgenommen') AS imHaus,
(SELECT COUNT(*) FROM medical_aufnahme a
WHERE a.patient = c.id AND a.status = 'wartend') AS wartet
FROM aether_characters c
LEFT JOIN medical_patient m ON m.`character` = c.id
LEFT JOIN aether_phone t ON t.`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 ? OR t.number LIKE ?)
ORDER BY c.lastname, c.firstname
LIMIT 40
]], { muster, muster, muster, muster, muster }) or {}
for _, eintrag in ipairs(treffer) do
eintrag.dob = tostring(eintrag.dob):sub(1, 10)
eintrag.imHaus = tonumber(eintrag.imHaus) > 0
eintrag.wartet = tonumber(eintrag.wartet) > 0
end
return { treffer = treffer }
end)
-- =====================================================================
-- PATIENTENAKTE
-- =====================================================================
Kern.RegistriereAktion('mdt', 'oeffnen', 'patient.suchen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
if not patientId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT c.id, c.firstname, c.lastname, c.dob, c.gender, c.nationality,
c.job, c.is_dead
FROM aether_characters c
WHERE c.id = ? AND c.deleted = 0
]], { patientId })
if not zeilen or not zeilen[1] then return nil, 'Diese Person ist nicht erfasst.' end
local person = zeilen[1]
-- --- Medizinische Stammdaten ---
local stamm = MySQL.query.await(
'SELECT * FROM medical_patient WHERE `character` = ?', { patientId })
stamm = stamm and stamm[1] or {}
-- --- Telefonnummer ---
local telefon = MySQL.scalar.await(
'SELECT number FROM aether_phone WHERE `character` = ?', { patientId })
-- --- Aktuelle Aufnahme, falls vorhanden ---
local aufnahme = MySQL.query.await([[
SELECT a.id, a.grund, a.prioritaet, a.station, a.status, a.aufgenommen_am,
a.aufgenommen_von, 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
]], { patientId })
aufnahme = aufnahme and aufnahme[1] or nil
if aufnahme then
aufnahme.aufgenommenVon = NameVon(aufnahme.aufgenommen_von)
aufnahme.aufgenommen_von = nil
end
-- --- Liegt der Patient auf Intensiv? ---
local intensiv = MySQL.query.await([[
SELECT i.id, i.zustand, i.geraete, i.verantwortlich, i.aufgenommen_am
FROM medical_intensiv i
WHERE i.patient = ? AND i.beendet_am IS NULL
ORDER BY i.id DESC LIMIT 1
]], { patientId })
intensiv = intensiv and intensiv[1] or nil
if intensiv then
intensiv.verantwortlichName = NameVon(intensiv.verantwortlich)
intensiv.verantwortlich = nil
end
-- --- Zählwerte für die Reiter ---
local zaehler = {
behandlungen = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_behandlung WHERE patient = ?', { patientId })) or 0,
operationen = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_operation WHERE patient = ?', { patientId })) or 0,
labor = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_labor WHERE patient = ?', { patientId })) or 0,
akten = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_akte WHERE patient = ?', { patientId })) or 0,
aufnahmen = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_aufnahme WHERE patient = ?', { patientId })) or 0,
notizen = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_patient_notiz WHERE patient = ?', { patientId })) or 0,
}
return {
patient = {
id = person.id,
name = person.firstname .. ' ' .. person.lastname,
vorname = person.firstname,
nachname = person.lastname,
geburt = tostring(person.dob):sub(1, 10),
geschlecht = person.gender,
herkunft = person.nationality,
beruf = person.job,
telefon = telefon,
-- Nur die Tatsache, dass der Charakter verletzt am Boden liegt.
-- Das System leitet daraus nichts ab und stellt nichts fest.
verletzt = person.is_dead == 1,
},
stamm = {
blutgruppe = stamm.blutgruppe,
allergien = stamm.allergien,
vorerkrankungen = stamm.vorerkrankungen,
dauermedikation = stamm.dauermedikation,
implantate = stamm.implantate,
organspender = stamm.organspender or 'unbekannt',
organspenderName = ORGANSTATUS[stamm.organspender or 'unbekannt'],
notfallkontakt = stamm.notfallkontakt,
hinweis = stamm.hinweis,
lichtbild = stamm.lichtbild,
gepflegtVon = stamm.aktualisiert_von and NameVon(stamm.aktualisiert_von) or nil,
gepflegtAm = stamm.aktualisiert_am,
},
aufnahme = aufnahme,
intensiv = intensiv,
zaehler = zaehler,
stationen = Medizin.Config.Stationen,
blutgruppen = Medizin.Config.Blutgruppen,
organstatus = ORGANSTATUS,
rechte = {
stammdaten = Rechte.Hat(mitarbeiter, 'patient.stammdaten'),
hinweis = Rechte.Hat(mitarbeiter, 'patient.hinweis'),
akteLesen = Rechte.Hat(mitarbeiter, 'akte.lesen'),
akteErstellen = Rechte.Hat(mitarbeiter, 'akte.erstellen'),
akteBearbeiten = Rechte.Hat(mitarbeiter, 'akte.bearbeiten'),
akteFreigeben = Rechte.Hat(mitarbeiter, 'akte.freigeben'),
akteSchliessen = Rechte.Hat(mitarbeiter, 'akte.schliessen'),
},
}
end)
-- =====================================================================
-- VERLAUF
--
-- Alle Vorgänge eines Patienten in einer Zeitleiste. Reine Anzeige —
-- aus der Abfolge wird nichts abgeleitet.
-- =====================================================================
Kern.RegistriereAktion('mdt', 'verlauf', 'akte.lesen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
if not patientId then return nil, 'Ungültige Kennung.' end
local eintraege = {}
--- Hängt einen Eintrag an die Zeitleiste
local function Anfuegen(art, zeitpunkt, titel, text, ziel, zielId, marker)
if not zeitpunkt then return end
eintraege[#eintraege + 1] = {
art = art, am = zeitpunkt, titel = titel, text = text,
ziel = ziel, zielId = zielId, marker = marker,
}
end
-- --- Aufnahmen und Entlassungen ---
local aufnahmen = MySQL.query.await([[
SELECT id, grund, station, status, aufgenommen_am, aufgenommen_von,
entlassen_am, entlassen_von, entlass_grund
FROM medical_aufnahme WHERE patient = ?
ORDER BY id DESC LIMIT 40
]], { patientId }) or {}
for _, eintrag in ipairs(aufnahmen) do
Anfuegen('aufnahme', eintrag.aufgenommen_am, 'Aufnahme',
eintrag.grund, 'aufnahme', eintrag.id,
'aufgenommen von ' .. NameVon(eintrag.aufgenommen_von))
if eintrag.entlassen_am then
Anfuegen('entlassung', eintrag.entlassen_am, 'Entlassung',
eintrag.entlass_grund, 'aufnahme', eintrag.id,
'entlassen von ' .. NameVon(eintrag.entlassen_von))
end
end
-- --- Behandlungen ---
local behandlungen = MySQL.query.await([[
SELECT id, kennung, diagnose, massnahmen, status, erstellt_am, erstellt_von
FROM medical_behandlung WHERE patient = ?
ORDER BY id DESC LIMIT 40
]], { patientId }) or {}
for _, eintrag in ipairs(behandlungen) do
Anfuegen('behandlung', eintrag.erstellt_am,
'Behandlung ' .. eintrag.kennung,
eintrag.diagnose or eintrag.massnahmen or 'Noch keine Diagnose eingetragen',
'behandlung', eintrag.id,
(eintrag.status == 'laufend' and 'läuft · ' or '') .. NameVon(eintrag.erstellt_von))
end
-- --- Operationen ---
local operationen = MySQL.query.await([[
SELECT id, kennung, art, status, geplant_fuer, beginn, ende, erstellt_von
FROM medical_operation WHERE patient = ?
ORDER BY id DESC LIMIT 20
]], { patientId }) or {}
for _, eintrag in ipairs(operationen) do
Anfuegen('operation', eintrag.beginn or eintrag.geplant_fuer,
'Operation ' .. eintrag.kennung, eintrag.art,
'operation', eintrag.id,
eintrag.status .. ' · ' .. NameVon(eintrag.erstellt_von))
end
-- --- Laboraufträge ---
local labor = MySQL.query.await([[
SELECT id, kennung, art, status, fragestellung, auftrag_am, auftrag_von, befund
FROM medical_labor WHERE patient = ?
ORDER BY id DESC LIMIT 30
]], { patientId }) or {}
for _, eintrag in ipairs(labor) do
Anfuegen('labor', eintrag.auftrag_am,
'Labor ' .. eintrag.kennung .. ' · ' .. eintrag.art,
eintrag.befund or eintrag.fragestellung or 'Ohne Fragestellung',
'labor', eintrag.id,
eintrag.status .. ' · ' .. NameVon(eintrag.auftrag_von))
end
-- --- Verabreichte Medikamente ---
local gaben = MySQL.query.await([[
SELECT g.id, g.dosierung, g.menge, g.gegeben_am, g.gegeben_von, g.hinweis,
m.name, m.einheit
FROM medical_behandlung_medikament g
JOIN medical_behandlung b ON b.id = g.behandlung
JOIN medical_medikament m ON m.id = g.medikament
WHERE b.patient = ?
ORDER BY g.id DESC LIMIT 40
]], { patientId }) or {}
for _, eintrag in ipairs(gaben) do
Anfuegen('medikament', eintrag.gegeben_am,
'Medikamentengabe: ' .. eintrag.name,
('%s %s%s'):format(eintrag.menge, eintrag.einheit,
eintrag.dosierung and (' · ' .. eintrag.dosierung) or ''),
nil, nil,
'gegeben von ' .. NameVon(eintrag.gegeben_von))
end
-- --- Vermerke ---
local notizen = MySQL.query.await([[
SELECT id, text, von, am FROM medical_patient_notiz
WHERE patient = ? ORDER BY id DESC LIMIT 30
]], { patientId }) or {}
for _, eintrag in ipairs(notizen) do
Anfuegen('notiz', eintrag.am, 'Vermerk', eintrag.text,
nil, nil, NameVon(eintrag.von))
end
-- Jüngstes zuerst
table.sort(eintraege, function(a, b) return tostring(a.am) > tostring(b.am) end)
-- Nur die letzten 60 Einträge übertragen
local gekuerzt = {}
for i = 1, math.min(#eintraege, 60) do gekuerzt[i] = eintraege[i] end
return { eintraege = gekuerzt, gesamt = #eintraege }
end)
-- =====================================================================
-- STAMMDATEN PFLEGEN
--
-- Alles hier trägt ein Mediziner von Hand ein. Die Blutgruppe ist
-- bewusst ein Freitextfeld aus einer Liste — sie wird NICHT aus einem
-- Laborergebnis übernommen, denn das wäre eine Feststellung, die
-- jemand verantworten muss.
-- =====================================================================
Kern.RegistriereAktion('mdt', 'stammdaten', 'patient.stammdaten', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
if not patientId then return nil, 'Ungültige Kennung.' end
local vorhanden = MySQL.scalar.await(
'SELECT id FROM aether_characters WHERE id = ? AND deleted = 0', { patientId })
if not vorhanden then return nil, 'Diese Person ist nicht erfasst.' end
-- Blutgruppe nur aus der bekannten Liste
local blutgruppe = nil
if daten.blutgruppe and daten.blutgruppe ~= '' then
for _, gruppe in ipairs(Medizin.Config.Blutgruppen) do
if gruppe == daten.blutgruppe then blutgruppe = gruppe break end
end
if not blutgruppe then return nil, 'Unbekannte Blutgruppe.' end
end
local organspender = ORGANSTATUS[daten.organspender] and daten.organspender or 'unbekannt'
-- Alten Stand für das Protokoll festhalten
local alt = MySQL.query.await(
'SELECT blutgruppe, allergien, hinweis FROM medical_patient WHERE `character` = ?',
{ patientId })
alt = alt and alt[1] or {}
MySQL.query.await([[
INSERT INTO medical_patient
(`character`, blutgruppe, allergien, vorerkrankungen, dauermedikation,
implantate, organspender, notfallkontakt, lichtbild,
aktualisiert_von, aktualisiert_am)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
blutgruppe = VALUES(blutgruppe),
allergien = VALUES(allergien),
vorerkrankungen = VALUES(vorerkrankungen),
dauermedikation = VALUES(dauermedikation),
implantate = VALUES(implantate),
organspender = VALUES(organspender),
notfallkontakt = VALUES(notfallkontakt),
lichtbild = VALUES(lichtbild),
aktualisiert_von = VALUES(aktualisiert_von),
aktualisiert_am = NOW()
]], {
patientId, blutgruppe,
Text(daten.allergien, 512),
Text(daten.vorerkrankungen, 1000),
Text(daten.dauermedikation, 1000),
Text(daten.implantate, 512),
organspender,
Text(daten.notfallkontakt, 160),
Text(daten.lichtbild, 512),
mitarbeiter.charakterId,
})
Protokoll.Schreibe(mitarbeiter, 'mdt', 'Stammdaten gepflegt',
{ typ = 'patient', id = patientId },
{ blutgruppeAlt = alt.blutgruppe, blutgruppeNeu = blutgruppe })
Kern.Loese('patient.stammdaten', patientId, mitarbeiter)
return { ok = true }
end)
--- Warnhinweis setzen oder entfernen — immer mit Begründung
Kern.RegistriereAktion('mdt', 'hinweisSetzen', 'patient.hinweis', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
if not patientId then return nil, 'Ungültige Kennung.' end
local hinweis = Text(daten.hinweis, 512)
local grund = Text(daten.grund, 255)
if hinweis and not grund then
return nil, 'Bitte begründen, warum der Warnhinweis gesetzt wird.'
end
MySQL.query.await([[
INSERT INTO medical_patient (`character`, hinweis, aktualisiert_von, aktualisiert_am)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
hinweis = VALUES(hinweis),
aktualisiert_von = VALUES(aktualisiert_von),
aktualisiert_am = NOW()
]], { patientId, hinweis, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'mdt',
hinweis and 'Warnhinweis gesetzt' or 'Warnhinweis entfernt',
{ typ = 'patient', id = patientId }, { hinweis = hinweis }, grund)
return { ok = true }
end)
-- =====================================================================
-- VERMERKE
-- =====================================================================
Kern.RegistriereAktion('mdt', 'notizen', 'akte.lesen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
if not patientId then return nil, 'Ungültige Kennung.' end
local notizen = MySQL.query.await([[
SELECT n.id, n.text, n.am, n.von,
c.firstname, c.lastname, p.personalnummer
FROM medical_patient_notiz n
LEFT JOIN aether_characters c ON c.id = n.von
LEFT JOIN medical_personal p ON p.`character` = n.von
WHERE n.patient = ?
ORDER BY n.id DESC LIMIT 60
]], { patientId }) or {}
for _, eintrag in ipairs(notizen) do
eintrag.autor = (eintrag.firstname or 'Unbekannt') .. ' ' .. (eintrag.lastname or '')
eintrag.firstname, eintrag.lastname = nil, nil
eintrag.eigene = eintrag.von == mitarbeiter.charakterId
end
return { notizen = notizen }
end)
Kern.RegistriereAktion('mdt', 'notizAnlegen', 'akte.lesen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
local text = Text(daten.text, 1000)
if not patientId then return nil, 'Ungültige Kennung.' end
if not text or #text < 3 then return nil, 'Der Vermerk ist zu kurz.' end
local id = MySQL.insert.await(
'INSERT INTO medical_patient_notiz (patient, text, von) VALUES (?, ?, ?)',
{ patientId, text, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'mdt', 'Vermerk angelegt',
{ typ = 'patient', id = patientId })
return { id = id }
end)
-- =====================================================================
-- AKTEN
--
-- Jede Änderung erzeugt eine neue Version. Nichts wird überschrieben,
-- die Historie bleibt vollständig einsehbar.
-- =====================================================================
Kern.RegistriereAktion('mdt', 'akten', 'akte.lesen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
if not patientId then return nil, 'Ungültige Kennung.' end
local akten = MySQL.query.await([[
SELECT a.id, a.aktenzeichen, a.typ, a.titel, a.status, a.version,
a.erstellt_am, a.erstellt_von, a.freigabe_am,
c.firstname, c.lastname
FROM medical_akte a
LEFT JOIN aether_characters c ON c.id = a.erstellt_von
WHERE a.patient = ?
ORDER BY a.id DESC LIMIT 60
]], { patientId }) or {}
local artNamen = {}
for _, eintrag in ipairs(Medizin.Config.Aktenarten) do artNamen[eintrag.id] = eintrag.name end
for _, akte in ipairs(akten) do
akte.typName = artNamen[akte.typ] or akte.typ
akte.autor = (akte.firstname or 'Unbekannt') .. ' ' .. (akte.lastname or '')
akte.firstname, akte.lastname = nil, nil
end
return { akten = akten, aktenarten = Medizin.Config.Aktenarten }
end)
Kern.RegistriereAktion('mdt', 'akteOeffnen', 'akte.lesen', function(quelle, mitarbeiter, daten)
local akteId = tonumber(daten.id)
if not akteId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT a.*, c.firstname, c.lastname
FROM medical_akte a
LEFT JOIN aether_characters c ON c.id = a.erstellt_von
WHERE a.id = ?
]], { akteId })
if not zeilen or not zeilen[1] then return nil, 'Akte nicht gefunden.' end
local akte = zeilen[1]
-- Vollständige Versionshistorie
local versionen = MySQL.query.await([[
SELECT v.version, v.geaendert_am, v.geaendert_von, v.kommentar,
c.firstname, c.lastname
FROM medical_akte_version v
LEFT JOIN aether_characters c ON c.id = v.geaendert_von
WHERE v.akte = ?
ORDER BY v.version DESC
]], { akteId }) or {}
for _, version in ipairs(versionen) do
version.autor = (version.firstname or 'Unbekannt') .. ' ' .. (version.lastname or '')
version.firstname, version.lastname = nil, nil
end
return {
akte = {
id = akte.id, aktenzeichen = akte.aktenzeichen, patient = akte.patient,
typ = akte.typ, titel = akte.titel, status = akte.status,
inhalt = akte.inhalt, version = akte.version,
erstelltAm = akte.erstellt_am,
autor = (akte.firstname or 'Unbekannt') .. ' ' .. (akte.lastname or ''),
freigabeAm = akte.freigabe_am,
freigabeVon = akte.freigabe_von and NameVon(akte.freigabe_von) or nil,
},
versionen = versionen,
aktenarten = Medizin.Config.Aktenarten,
rechte = {
bearbeiten = Rechte.Hat(mitarbeiter, 'akte.bearbeiten'),
freigeben = Rechte.Hat(mitarbeiter, 'akte.freigeben'),
schliessen = Rechte.Hat(mitarbeiter, 'akte.schliessen'),
},
}
end)
--- Liefert den Inhalt einer bestimmten Version
Kern.RegistriereAktion('mdt', 'akteVersion', 'akte.lesen', function(quelle, mitarbeiter, daten)
local akteId = tonumber(daten.id)
local version = tonumber(daten.version)
if not akteId or not version then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT v.version, v.inhalt, v.geaendert_am, v.geaendert_von, v.kommentar
FROM medical_akte_version v
WHERE v.akte = ? AND v.version = ?
]], { akteId, version })
if not zeilen or not zeilen[1] then return nil, 'Diese Version gibt es nicht.' end
local eintrag = zeilen[1]
eintrag.autor = NameVon(eintrag.geaendert_von)
eintrag.geaendert_von = nil
return eintrag
end)
Kern.RegistriereAktion('mdt', 'akteAnlegen', 'akte.erstellen', function(quelle, mitarbeiter, daten)
local patientId = tonumber(daten.patient)
local titel = Text(daten.titel, 160)
if not patientId then return nil, 'Kein Patient gewählt.' end
if not titel or #titel < 3 then return nil, 'Bitte einen Titel angeben.' end
-- Aktenart prüfen
local typ = nil
for _, eintrag in ipairs(Medizin.Config.Aktenarten) do
if eintrag.id == daten.typ then typ = eintrag.id break end
end
if not typ then return nil, 'Unbekannte Aktenart.' end
local inhalt = type(daten.inhalt) == 'string' and daten.inhalt:sub(1, 20000) or nil
local aktenzeichen = Kern.ErzeugeKennung('AKT', 'medical_akte', 'aktenzeichen')
local id = MySQL.insert.await([[
INSERT INTO medical_akte
(aktenzeichen, patient, typ, titel, status, inhalt, erstellt_von, version)
VALUES (?, ?, ?, ?, 'entwurf', ?, ?, 1)
]], { aktenzeichen, patientId, typ, titel, inhalt, mitarbeiter.charakterId })
-- Erste Version festhalten
MySQL.insert.await([[
INSERT INTO medical_akte_version (akte, version, inhalt, geaendert_von, kommentar)
VALUES (?, 1, ?, ?, 'Anlage')
]], { id, inhalt, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'mdt', 'Akte angelegt',
{ typ = 'akte', id = id }, { aktenzeichen = aktenzeichen, patient = patientId })
Kern.Loese('akte.angelegt', id, mitarbeiter)
return { id = id, aktenzeichen = aktenzeichen }
end)
Kern.RegistriereAktion('mdt', 'akteSpeichern', 'akte.bearbeiten', function(quelle, mitarbeiter, daten)
local akteId = tonumber(daten.id)
if not akteId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, aktenzeichen, status, version FROM medical_akte WHERE id = ?', { akteId })
if not zeilen or not zeilen[1] then return nil, 'Akte nicht gefunden.' end
local akte = zeilen[1]
if akte.status == 'geschlossen' then
return nil, 'Eine geschlossene Akte lässt sich nicht mehr ändern.'
end
local titel = Text(daten.titel, 160)
if not titel or #titel < 3 then return nil, 'Bitte einen Titel angeben.' end
local inhalt = type(daten.inhalt) == 'string' and daten.inhalt:sub(1, 20000) or nil
local kommentar = Text(daten.kommentar, 255)
if not kommentar then return nil, 'Bitte kurz angeben, was geändert wurde.' end
local neueVersion = akte.version + 1
MySQL.update.await([[
UPDATE medical_akte SET titel = ?, inhalt = ?, version = ? WHERE id = ?
]], { titel, inhalt, neueVersion, akteId })
-- Neue Version anhängen — die alte bleibt unangetastet
MySQL.insert.await([[
INSERT INTO medical_akte_version (akte, version, inhalt, geaendert_von, kommentar)
VALUES (?, ?, ?, ?, ?)
]], { akteId, neueVersion, inhalt, mitarbeiter.charakterId, kommentar })
Protokoll.Schreibe(mitarbeiter, 'mdt', 'Akte bearbeitet',
{ typ = 'akte', id = akteId },
{ aktenzeichen = akte.aktenzeichen, version = neueVersion }, kommentar)
return { version = neueVersion }
end)
Kern.RegistriereAktion('mdt', 'akteStatus', nil, function(quelle, mitarbeiter, daten)
local akteId = tonumber(daten.id)
local status = tostring(daten.status or '')
if not akteId then return nil, 'Ungültige Kennung.' end
-- Welches Recht der Statuswechsel verlangt
local benoetigt = {
offen = 'akte.bearbeiten',
freigegeben = 'akte.freigeben',
geschlossen = 'akte.schliessen',
}
if not benoetigt[status] then return nil, 'Unbekannter Status.' end
if not Rechte.Hat(mitarbeiter, benoetigt[status]) then
return nil, 'Dir fehlt die Berechtigung für diesen Schritt.'
end
local zeilen = MySQL.query.await(
'SELECT id, aktenzeichen, status FROM medical_akte WHERE id = ?', { akteId })
if not zeilen or not zeilen[1] then return nil, 'Akte nicht gefunden.' end
if zeilen[1].status == 'geschlossen' then
return nil, 'Diese Akte ist bereits geschlossen.'
end
local grund = Text(daten.grund, 255)
if status == 'freigegeben' then
MySQL.update.await([[
UPDATE medical_akte SET status = ?, freigabe_von = ?, freigabe_am = NOW()
WHERE id = ?
]], { status, mitarbeiter.charakterId, akteId })
else
MySQL.update.await('UPDATE medical_akte SET status = ? WHERE id = ?', { status, akteId })
end
Protokoll.Schreibe(mitarbeiter, 'mdt', 'Aktenstatus geändert',
{ typ = 'akte', id = akteId },
{ aktenzeichen = zeilen[1].aktenzeichen, von = zeilen[1].status, nach = status }, grund)
Kern.Loese('akte.status', akteId, status, mitarbeiter)
return { ok = true }
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Module
-- ---------------------------------------------------------------------
--- Liefert die Stammdaten eines Patienten, so wie sie eingetragen sind.
--- Ausdrücklich ohne Bewertung: was hier steht, hat ein Mediziner
--- eingetragen, und was fehlt, ist schlicht nicht erhoben.
--- @param patientId number Charakter
--- @return table|nil
exports('HolePatientenStammdaten', function(patientId)
if not tonumber(patientId) then return nil end
local zeilen = MySQL.query.await([[
SELECT blutgruppe, allergien, vorerkrankungen, dauermedikation,
implantate, organspender, notfallkontakt, hinweis
FROM medical_patient WHERE `character` = ?
]], { tonumber(patientId) })
return zeilen and zeilen[1] or nil
end)