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

783 lines
30 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Berichte
--
-- Ein Bericht ist ein Dokument mit einer Unterschrift darunter.
--
-- Das System schreibt keinen Satz davon. Es formuliert nichts vor,
-- fasst nichts zusammen und leitet aus keiner Behandlung selbst eine
-- Diagnose ab. Wer Text aus einer Behandlung übernehmen will, löst
-- das ausdrücklich aus — und was dann im Bericht steht, verantwortet
-- er mit seinem Namen.
--
-- Der Weg vom Entwurf zur Freigabe wird Schritt für Schritt von
-- Menschen gegangen. Kein Bericht reift von selbst, keine Frist gibt
-- ihn frei, und ein freigegebener Bericht wird nie wieder geändert.
-- =====================================================================
-- Lesen darf, wer Akten lesen darf. Schreiben und Freigeben verlangen
-- die jeweils eigenen Rechte.
Kern.RegistriereModul({
id = 'bericht', name = 'Berichte', icon = '📄',
reihenfolge = 14, recht = 'akte.lesen',
beschreibung = 'Berichtsentwürfe, Freigabe und Unterschriften',
})
-- Die inhaltlichen Felder eines Berichts, in der Reihenfolge des Bogens
local FELDER = {
{ id = 'symptome', name = 'Symptome und Schilderung' },
{ id = 'untersuchung', name = 'Untersuchungsbefund' },
{ id = 'diagnose', name = 'Diagnose' },
{ id = 'massnahmen', name = 'Maßnahmen' },
{ id = 'empfehlung', name = 'Empfehlung' },
}
-- ---------------------------------------------------------------------
-- 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 Charakters 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
--- Liest eine JSON-Liste aus der Datenbank
--- @param roh string|nil JSON
--- @return table Liste
local function AlsListe(roh)
if not roh then return {} end
local erfolg, liste = pcall(json.decode, roh)
if erfolg and type(liste) == 'table' then return liste end
return {}
end
--- Zeitstempel für Einträge in JSON-Feldern
--- @return string
local function Jetzt()
return os.date('%Y-%m-%d %H:%M:%S')
end
--- Baut einen Unterschrifteneintrag
--- @param mitarbeiter table Unterzeichnender
--- @param art string verfasser/freigabe/gegenzeichnung
--- @return table
local function Unterschrift(mitarbeiter, art)
return {
charakterId = mitarbeiter.charakterId,
name = mitarbeiter.name,
personalnummer = mitarbeiter.personalnummer,
rolle = mitarbeiter.rolleName,
art = art,
am = Jetzt(),
}
end
--- Prüft eine Adresse für eine Anlage
--- @param wert any Eingabe
--- @return string|nil
local function Adresse(wert)
local roh = Text(wert, 500)
if not roh then return nil end
if not roh:match('^https?://') then return nil end
return roh
end
--- Lädt einen Bericht mit seinen Begleitdaten
--- @param berichtId number Bericht
--- @return table|nil
local function LadeBericht(berichtId)
local zeilen = MySQL.query.await([[
SELECT b.*,
c.firstname, c.lastname, c.dob,
beh.kennung AS behandlungKennung,
ein.nummer AS einsatzNummer, ein.titel AS einsatzTitel
FROM medical_bericht b
LEFT JOIN aether_characters c ON c.id = b.patient
LEFT JOIN medical_behandlung beh ON beh.id = b.behandlung
LEFT JOIN medical_einsatz ein ON ein.id = b.einsatz
WHERE b.id = ?
]], { berichtId })
return zeilen and zeilen[1] or nil
end
--- Darf dieser Mitarbeiter den Inhalt ändern?
--- Nur der Verfasser, und nur solange der Bericht ein Entwurf ist.
--- @param eintrag table Berichtszeile
--- @param mitarbeiter table Handelnder
--- @return boolean, string|nil Grund der Ablehnung
local function DarfAendern(eintrag, mitarbeiter)
if eintrag.status == 'freigegeben' then
return false, 'Ein freigegebener Bericht wird nicht mehr geändert.'
end
if eintrag.status == 'eingereicht' then
return false, 'Der Bericht liegt zur Freigabe vor. Dafür erst zurückziehen.'
end
if eintrag.erstellt_von ~= mitarbeiter.charakterId then
return false, 'Diesen Entwurf hat jemand anderes geschrieben.'
end
return true, nil
end
-- =====================================================================
-- ÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('bericht', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
local ordner = tostring(daten.ordner or 'meine')
local ich = mitarbeiter.charakterId
local bedingung, werte
if ordner == 'freigabe' then
bedingung = "b.status = 'eingereicht'"
werte = {}
elseif ordner == 'freigegeben' then
bedingung = "b.status = 'freigegeben'"
werte = {}
elseif ordner == 'alle' then
bedingung = '1 = 1'
werte = {}
else
-- Eigene Entwürfe: was noch niemand ausser dem Verfasser gesehen hat
bedingung = "b.status = 'entwurf' AND b.erstellt_von = ?"
werte = { ich }
end
-- Freitextsuche über Nummer, Titel und Patientenname
local begriff = Text(daten.suche, 60)
if begriff then
local muster = '%' .. begriff .. '%'
bedingung = ('(%s) AND (b.nummer LIKE ? OR b.titel LIKE ? OR CONCAT(c.firstname, " ", c.lastname) LIKE ?)')
:format(bedingung)
werte[#werte + 1] = muster
werte[#werte + 1] = muster
werte[#werte + 1] = muster
end
local liste = MySQL.query.await(([[
SELECT b.id, b.nummer, b.titel, b.patient, b.behandlung, b.einsatz,
b.status, b.erstellt_von, b.erstellt_am, b.diagnose,
b.unterschriften,
c.firstname, c.lastname,
beh.kennung AS behandlungKennung,
ein.nummer AS einsatzNummer
FROM medical_bericht b
LEFT JOIN aether_characters c ON c.id = b.patient
LEFT JOIN medical_behandlung beh ON beh.id = b.behandlung
LEFT JOIN medical_einsatz ein ON ein.id = b.einsatz
WHERE %s
ORDER BY b.id DESC
LIMIT 150
]]):format(bedingung), werte) or {}
for _, eintrag in ipairs(liste) do
eintrag.patientName = eintrag.firstname
and (eintrag.firstname .. ' ' .. eintrag.lastname) or nil
eintrag.autor = NameVon(eintrag.erstellt_von)
eintrag.vonMir = eintrag.erstellt_von == ich
eintrag.unterschriften = #AlsListe(eintrag.unterschriften)
-- Nur ein Hinweis darauf, ob überhaupt eine Diagnose im Bericht steht
eintrag.hatDiagnose = eintrag.diagnose ~= nil and eintrag.diagnose ~= ''
eintrag.firstname, eintrag.lastname = nil, nil
eintrag.erstellt_von, eintrag.diagnose = nil, nil
end
local function Zaehle(sql, parameter)
return tonumber(MySQL.scalar.await(sql, parameter)) or 0
end
local zaehler = {
meine = Zaehle([[
SELECT COUNT(*) FROM medical_bericht
WHERE status = 'entwurf' AND erstellt_von = ?
]], { ich }),
freigabe = Zaehle("SELECT COUNT(*) FROM medical_bericht WHERE status = 'eingereicht'"),
freigegeben = Zaehle([[
SELECT COUNT(*) FROM medical_bericht
WHERE status = 'freigegeben' AND DATE(erstellt_am) = CURDATE()
]]),
-- Eigene Berichte, die noch bei der Freigabe liegen
meineOffen = Zaehle([[
SELECT COUNT(*) FROM medical_bericht
WHERE status = 'eingereicht' AND erstellt_von = ?
]], { ich }),
}
return {
ordner = ordner,
liste = liste,
zaehler = zaehler,
status = Medizin.Config.Berichtstatus,
felder = FELDER,
rechte = {
erstellen = Rechte.Hat(mitarbeiter, 'bericht.erstellen'),
freigeben = Rechte.Hat(mitarbeiter, 'bericht.freigeben'),
},
}
end)
-- =====================================================================
-- EINZELNER BERICHT
-- =====================================================================
Kern.RegistriereAktion('bericht', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
-- Ein fremder Entwurf ist noch niemandes Dokument ausser dem des Verfassers
if eintrag.status == 'entwurf' and eintrag.erstellt_von ~= mitarbeiter.charakterId then
return nil, 'Dieser Entwurf ist noch nicht eingereicht.'
end
-- Schreiben darf nur, wer den Entwurf verfasst hat und das Recht dazu hat
local aenderbar = DarfAendern(eintrag, mitarbeiter)
local darfAendern = aenderbar and Rechte.Hat(mitarbeiter, 'bericht.erstellen')
local unterschriften = AlsListe(eintrag.unterschriften)
local anlagen = AlsListe(eintrag.anlagen)
-- Steht Verfasser und Freigabe in einer Person? Das wird gezeigt,
-- nicht verhindert.
local selbstFreigabe = false
for _, zeile in ipairs(unterschriften) do
if zeile.art == 'freigabe' and zeile.charakterId == eintrag.erstellt_von then
selbstFreigabe = true
end
end
local spur = Protokoll.Lies({ zielTyp = 'bericht', zielId = berichtId, limit = 40 })
for _, zeile in ipairs(spur) do
zeile.autor = zeile.firstname
and (zeile.firstname .. ' ' .. zeile.lastname) or 'System'
zeile.firstname, zeile.lastname, zeile.details = nil, nil, nil
end
return {
bericht = {
id = eintrag.id, nummer = eintrag.nummer, titel = eintrag.titel,
status = eintrag.status,
patient = eintrag.patient,
patientName = eintrag.firstname
and (eintrag.firstname .. ' ' .. eintrag.lastname) or nil,
patientGeburt = eintrag.dob and tostring(eintrag.dob):sub(1, 10) or nil,
behandlung = eintrag.behandlung,
behandlungKennung = eintrag.behandlungKennung,
einsatz = eintrag.einsatz,
einsatzNummer = eintrag.einsatzNummer,
einsatzTitel = eintrag.einsatzTitel,
symptome = eintrag.symptome, untersuchung = eintrag.untersuchung,
diagnose = eintrag.diagnose, massnahmen = eintrag.massnahmen,
empfehlung = eintrag.empfehlung,
autor = NameVon(eintrag.erstellt_von),
vonMir = eintrag.erstellt_von == mitarbeiter.charakterId,
erstelltAm = eintrag.erstellt_am,
selbstFreigabe = selbstFreigabe,
},
unterschriften = unterschriften,
anlagen = anlagen,
spur = spur,
felder = FELDER,
status = Medizin.Config.Berichtstatus,
rechte = {
erstellen = Rechte.Hat(mitarbeiter, 'bericht.erstellen'),
freigeben = Rechte.Hat(mitarbeiter, 'bericht.freigeben'),
aendern = darfAendern,
},
}
end)
-- =====================================================================
-- ANLEGEN UND SCHREIBEN
-- =====================================================================
--- Patientensuche und die Vorgänge, an die ein Bericht hängen kann
Kern.RegistriereAktion('bericht', 'quellen', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local begriff = type(daten.begriff) == 'string' and daten.begriff or ''
local patientId = tonumber(daten.patient)
local treffer = {}
if #begriff >= 2 then
local muster = '%' .. begriff .. '%'
treffer = MySQL.query.await([[
SELECT c.id, c.firstname, c.lastname, c.dob, p.blutgruppe
FROM aether_characters c
LEFT JOIN medical_patient p ON p.`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.name = eintrag.firstname .. ' ' .. eintrag.lastname
eintrag.dob = tostring(eintrag.dob):sub(1, 10)
eintrag.firstname, eintrag.lastname = nil, nil
end
end
-- Behandlungen und Einsätze des gewählten Patienten
local behandlungen, einsaetze = {}, {}
if patientId then
behandlungen = MySQL.query.await([[
SELECT id, kennung, diagnose, status, erstellt_am
FROM medical_behandlung
WHERE patient = ?
ORDER BY id DESC LIMIT 25
]], { patientId }) or {}
einsaetze = MySQL.query.await([[
SELECT e.id, e.nummer, e.titel, e.kategorie, e.erstellt_am
FROM medical_einsatz e
WHERE EXISTS (SELECT 1 FROM medical_behandlung b
WHERE b.einsatz = e.id AND b.patient = ?)
ORDER BY e.id DESC LIMIT 15
]], { patientId }) or {}
end
return { treffer = treffer, behandlungen = behandlungen, einsaetze = einsaetze }
end)
Kern.RegistriereAktion('bericht', 'anlegen', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local titel = Text(daten.titel, 160)
if not titel then return nil, 'Bitte einen Titel angeben.' end
-- Patient, Behandlung und Einsatz sind freiwillig — nicht jeder
-- Bericht gehört zu einem einzelnen Vorgang
local patientId = tonumber(daten.patient)
if patientId then
local person = MySQL.scalar.await(
'SELECT id FROM aether_characters WHERE id = ? AND deleted = 0', { patientId })
if not person then return nil, 'Diese Person ist nicht erfasst.' end
end
local behandlungId = tonumber(daten.behandlung)
if behandlungId then
local behandlung = MySQL.query.await(
'SELECT id, patient FROM medical_behandlung WHERE id = ?', { behandlungId })
if not behandlung or not behandlung[1] then return nil, 'Unbekannte Behandlung.' end
-- Die Behandlung bestimmt den Patienten, wenn keiner gewählt wurde
patientId = patientId or behandlung[1].patient
end
local einsatzId = tonumber(daten.einsatz)
if einsatzId then
local einsatz = MySQL.scalar.await(
'SELECT id FROM medical_einsatz WHERE id = ?', { einsatzId })
if not einsatz then return nil, 'Unbekannter Einsatz.' end
end
local nummer = Kern.ErzeugeKennung('BER', 'medical_bericht', 'nummer')
local berichtId = MySQL.insert.await([[
INSERT INTO medical_bericht
(nummer, patient, behandlung, einsatz, titel, status, erstellt_von)
VALUES (?, ?, ?, ?, ?, 'entwurf', ?)
]], { nummer, patientId, behandlungId, einsatzId, titel, mitarbeiter.charakterId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Bericht angelegt',
{ typ = 'bericht', id = berichtId },
{ nummer = nummer, titel = titel, patient = patientId,
behandlung = behandlungId, einsatz = einsatzId })
Kern.Loese('bericht.angelegt', berichtId, mitarbeiter)
return { id = berichtId, nummer = nummer }
end)
Kern.RegistriereAktion('bericht', 'speichern', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
local darf, fehler = DarfAendern(eintrag, mitarbeiter)
if not darf then return nil, fehler end
local titel = Text(daten.titel, 160) or eintrag.titel
MySQL.update.await([[
UPDATE medical_bericht
SET titel = ?, symptome = ?, untersuchung = ?, diagnose = ?,
massnahmen = ?, empfehlung = ?
WHERE id = ?
]], {
titel,
Text(daten.symptome, 4000),
Text(daten.untersuchung, 4000),
Text(daten.diagnose, 4000),
Text(daten.massnahmen, 4000),
Text(daten.empfehlung, 4000),
berichtId,
})
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Entwurf gespeichert',
{ typ = 'bericht', id = berichtId }, { nummer = eintrag.nummer })
return { ok = true }
end)
--- Übernimmt den Wortlaut einer Behandlung in den Entwurf.
---
--- Das ist ein Kopiervorgang, den ein Mensch auslöst — das System
--- fasst nichts zusammen und formuliert nichts um. Vorhandener Text
--- im Bericht wird nicht überschrieben, sondern bleibt stehen.
Kern.RegistriereAktion('bericht', 'uebernehmen', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
local behandlungId = tonumber(daten.behandlung)
if not berichtId then return nil, 'Ungültige Kennung.' end
if not behandlungId then return nil, 'Keine Behandlung gewählt.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
local darf, fehler = DarfAendern(eintrag, mitarbeiter)
if not darf then return nil, fehler end
local zeilen = MySQL.query.await([[
SELECT id, kennung, patient, symptome, untersuchung, diagnose,
massnahmen, empfehlung
FROM medical_behandlung WHERE id = ?
]], { behandlungId })
if not zeilen or not zeilen[1] then return nil, 'Behandlung nicht gefunden.' end
local behandlung = zeilen[1]
-- Nur leere Felder füllen: was der Verfasser geschrieben hat, bleibt
local uebernommen = {}
local werte = {}
for _, feld in ipairs(FELDER) do
local vorhanden = Text(eintrag[feld.id], 4000)
local quelle_ = Text(behandlung[feld.id], 4000)
if not vorhanden and quelle_ then
werte[feld.id] = quelle_
uebernommen[#uebernommen + 1] = feld.name
else
werte[feld.id] = vorhanden
end
end
if #uebernommen == 0 then
return nil, 'Es gibt nichts zu übernehmen — alle Felder sind bereits gefüllt oder die Behandlung ist leer.'
end
MySQL.update.await([[
UPDATE medical_bericht
SET symptome = ?, untersuchung = ?, diagnose = ?, massnahmen = ?,
empfehlung = ?, behandlung = COALESCE(behandlung, ?),
patient = COALESCE(patient, ?)
WHERE id = ?
]], { werte.symptome, werte.untersuchung, werte.diagnose, werte.massnahmen,
werte.empfehlung, behandlungId, behandlung.patient, berichtId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Wortlaut aus Behandlung übernommen',
{ typ = 'bericht', id = berichtId },
{ nummer = eintrag.nummer, behandlung = behandlung.kennung,
felder = uebernommen })
return { ok = true, uebernommen = uebernommen }
end)
-- =====================================================================
-- ANLAGEN
-- =====================================================================
Kern.RegistriereAktion('bericht', 'anlage', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
local darf, fehler = DarfAendern(eintrag, mitarbeiter)
if not darf then return nil, fehler end
local anlagen = AlsListe(eintrag.anlagen)
local entfernen = tonumber(daten.entfernen)
if entfernen then
-- Entfernen ist nur im Entwurf möglich; ab der Einreichung
-- gehört die Anlage zum Dokument
if entfernen < 1 or entfernen > #anlagen then
return nil, 'Diese Anlage gibt es nicht.'
end
local weg = table.remove(anlagen, entfernen)
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Anlage entfernt',
{ typ = 'bericht', id = berichtId },
{ nummer = eintrag.nummer, titel = weg and weg.titel or nil })
else
local titel = Text(daten.titel, 120)
local adresse = Adresse(daten.adresse)
if not titel then return nil, 'Bitte die Anlage benennen.' end
if not adresse then return nil, 'Die Adresse muss mit http:// oder https:// beginnen.' end
if #anlagen >= 12 then return nil, 'Mehr als zwölf Anlagen sind nicht vorgesehen.' end
anlagen[#anlagen + 1] = {
titel = titel, adresse = adresse,
von = mitarbeiter.name, am = Jetzt(),
}
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Anlage hinzugefügt',
{ typ = 'bericht', id = berichtId },
{ nummer = eintrag.nummer, titel = titel })
end
MySQL.update.await('UPDATE medical_bericht SET anlagen = ? WHERE id = ?',
{ #anlagen > 0 and json.encode(anlagen) or nil, berichtId })
return { ok = true, anzahl = #anlagen }
end)
-- =====================================================================
-- EINREICHEN UND ZURÜCKZIEHEN
-- =====================================================================
Kern.RegistriereAktion('bericht', 'einreichen', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
local darf, fehler = DarfAendern(eintrag, mitarbeiter)
if not darf then return nil, fehler end
-- Ein leerer Bericht ist kein Bericht. Welche Felder fehlen, wird
-- nur benannt — die Einreichung hält das nicht auf.
local gefuellt, leer = 0, {}
for _, feld in ipairs(FELDER) do
if Text(eintrag[feld.id], 4000) then gefuellt = gefuellt + 1
else leer[#leer + 1] = feld.name end
end
if gefuellt == 0 then
return nil, 'Der Bericht enthält noch keinen Text.'
end
local unterschriften = AlsListe(eintrag.unterschriften)
unterschriften[#unterschriften + 1] = Unterschrift(mitarbeiter, 'verfasser')
MySQL.update.await([[
UPDATE medical_bericht SET status = 'eingereicht', unterschriften = ?
WHERE id = ?
]], { json.encode(unterschriften), berichtId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Bericht eingereicht',
{ typ = 'bericht', id = berichtId },
{ nummer = eintrag.nummer, leereFelder = leer })
-- Wer freigeben darf, erfährt davon
for _, person in pairs(Dienst.AlleImDienst()) do
if Rechte.Hat(person, 'bericht.freigeben')
and person.charakterId ~= mitarbeiter.charakterId then
Hinweise.Erstelle({ charakterId = person.charakterId }, {
kategorie = 'doku', prioritaet = 'normal',
titel = 'Bericht liegt zur Freigabe',
text = ('%s — %s, von %s.')
:format(eintrag.nummer, eintrag.titel, mitarbeiter.name),
zielTyp = 'bericht', zielId = berichtId,
})
end
end
Kern.Loese('bericht.eingereicht', berichtId, mitarbeiter)
return { ok = true, leereFelder = leer }
end)
Kern.RegistriereAktion('bericht', 'zurueckziehen', 'bericht.erstellen', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local grund = Text(daten.grund, 255)
if not grund then return nil, 'Bitte angeben, warum der Bericht zurückgeht.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
if eintrag.status ~= 'eingereicht' then
return nil, 'Nur ein eingereichter Bericht lässt sich zurückziehen.'
end
if eintrag.erstellt_von ~= mitarbeiter.charakterId then
return nil, 'Zurückziehen kann nur, wer den Bericht geschrieben hat.'
end
-- Die geleistete Unterschrift bleibt stehen: sie wurde geleistet.
-- Dass der Bericht danach wieder geändert wurde, steht im Protokoll.
MySQL.update.await("UPDATE medical_bericht SET status = 'entwurf' WHERE id = ?",
{ berichtId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Bericht zurückgezogen',
{ typ = 'bericht', id = berichtId }, { nummer = eintrag.nummer }, grund)
Kern.Loese('bericht.zurueckgezogen', berichtId, mitarbeiter)
return { ok = true }
end)
-- =====================================================================
-- FREIGABE
--
-- Die Freigabe ist eine Unterschrift. Sie geschieht nie automatisch,
-- nie nach Ablauf einer Frist und nie durch das System.
-- =====================================================================
Kern.RegistriereAktion('bericht', 'freigeben', 'bericht.freigeben', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
if eintrag.status ~= 'eingereicht' then
return nil, 'Nur ein eingereichter Bericht wird freigegeben.'
end
local unterschriften = AlsListe(eintrag.unterschriften)
unterschriften[#unterschriften + 1] = Unterschrift(mitarbeiter, 'freigabe')
-- Verfasser und Freigabe in einer Person ist möglich und wird
-- festgehalten — verhindert wird es nicht.
local selbstFreigabe = eintrag.erstellt_von == mitarbeiter.charakterId
MySQL.update.await([[
UPDATE medical_bericht SET status = 'freigegeben', unterschriften = ?
WHERE id = ?
]], { json.encode(unterschriften), berichtId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Bericht freigegeben',
{ typ = 'bericht', id = berichtId },
{ nummer = eintrag.nummer, selbstFreigabe = selbstFreigabe },
Text(daten.vermerk, 255))
if not selbstFreigabe then
Hinweise.Erstelle({ charakterId = eintrag.erstellt_von }, {
kategorie = 'doku', prioritaet = 'normal',
titel = 'Dein Bericht wurde freigegeben',
text = ('%s — %s, freigegeben von %s.')
:format(eintrag.nummer, eintrag.titel, mitarbeiter.name),
zielTyp = 'bericht', zielId = berichtId,
})
end
Kern.Loese('bericht.freigegeben', berichtId, mitarbeiter)
return { ok = true, selbstFreigabe = selbstFreigabe }
end)
Kern.RegistriereAktion('bericht', 'zurueckweisen', 'bericht.freigeben', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local grund = Text(daten.grund, 500)
if not grund or #grund < 5 then
return nil, 'Eine Zurückweisung braucht eine Begründung.'
end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
if eintrag.status ~= 'eingereicht' then
return nil, 'Nur ein eingereichter Bericht lässt sich zurückweisen.'
end
MySQL.update.await("UPDATE medical_bericht SET status = 'entwurf' WHERE id = ?",
{ berichtId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Bericht zurückgewiesen',
{ typ = 'bericht', id = berichtId }, { nummer = eintrag.nummer }, grund)
Hinweise.Erstelle({ charakterId = eintrag.erstellt_von }, {
kategorie = 'doku', prioritaet = 'hoch',
titel = 'Bericht zurückgewiesen',
text = ('%s — %s: %s'):format(eintrag.nummer, mitarbeiter.name, grund:sub(1, 120)),
zielTyp = 'bericht', zielId = berichtId,
})
Kern.Loese('bericht.zurueckgewiesen', berichtId, mitarbeiter)
return { ok = true }
end)
--- Zeichnet einen Bericht gegen.
--- Eine Gegenzeichnung ändert nichts am Wortlaut — sie fügt nur eine
--- weitere Unterschrift hinzu. Zurücknehmen lässt sie sich nicht.
Kern.RegistriereAktion('bericht', 'gegenzeichnen', 'bericht.freigeben', function(quelle, mitarbeiter, daten)
local berichtId = tonumber(daten.id)
if not berichtId then return nil, 'Ungültige Kennung.' end
local eintrag = LadeBericht(berichtId)
if not eintrag then return nil, 'Bericht nicht gefunden.' end
if eintrag.status == 'entwurf' then
return nil, 'Ein Entwurf wird nicht gegengezeichnet.'
end
local unterschriften = AlsListe(eintrag.unterschriften)
-- Zweimal dieselbe Unterschrift sagt nichts Zusätzliches
for _, zeile in ipairs(unterschriften) do
if zeile.charakterId == mitarbeiter.charakterId
and zeile.art == 'gegenzeichnung' then
return nil, 'Du hast diesen Bericht bereits gegengezeichnet.'
end
end
unterschriften[#unterschriften + 1] = Unterschrift(mitarbeiter, 'gegenzeichnung')
MySQL.update.await('UPDATE medical_bericht SET unterschriften = ? WHERE id = ?',
{ json.encode(unterschriften), berichtId })
Protokoll.Schreibe(mitarbeiter, 'bericht', 'Bericht gegengezeichnet',
{ typ = 'bericht', id = berichtId }, { nummer = eintrag.nummer },
Text(daten.vermerk, 255))
return { ok = true }
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Ressourcen
-- ---------------------------------------------------------------------
--- Liefert die freigegebenen Berichte eines Patienten.
--- Reine Auskunft — keine Ressource kann darüber einen Bericht
--- schreiben, ändern oder freigeben. Entwürfe bleiben aussen vor.
--- @param patientId number Charakter
--- @return table Liste
exports('HoleBerichte', function(patientId)
if not tonumber(patientId) then return {} end
return MySQL.query.await([[
SELECT nummer, titel, diagnose, erstellt_am
FROM medical_bericht
WHERE patient = ? AND status = 'freigegeben'
ORDER BY id DESC
LIMIT 50
]], { tonumber(patientId) }) or {}
end)