-- ===================================================================== -- aether-medical / Modul: Labor -- -- Das Labor speichert AUSSCHLIESSLICH Ergebnisse. -- -- Deshalb sind zwei Dinge strikt getrennt: -- * `ergebnis` — die gemessenen Werte. Trägt das Labor ein. -- * `befund` — die Bewertung. Trägt ein Mediziner ein. -- -- Das System vergleicht keine Werte mit Referenzbereichen, markiert -- nichts als auffällig und leitet aus keinem Ergebnis eine Diagnose -- ab. Ein Messwert ist eine Zahl, keine Aussage. -- -- Auch eine im Labor bestimmte Blutgruppe wandert NICHT von selbst in -- die Stammdaten — das muss ein Mediziner ausdrücklich übernehmen. -- ===================================================================== Kern.RegistriereModul({ id = 'labor', name = 'Labor', icon = '🧪', reihenfolge = 6, -- Ein Recht daraus genügt: Auftraggeber, Labor und Befunder teilen sich das Modul recht = { 'labor.beauftragen', 'labor.bearbeiten', 'labor.befunden' }, beschreibung = 'Laboraufträge, Ergebnisse und Befunde', }) -- Schreibhilfen je Probenart. Reine Textvorlagen — sie enthalten keine -- Referenzbereiche und bewerten nichts, sie geben nur die Struktur vor. local VORLAGEN = { blut = 'Hb:\nLeukozyten:\nThrombozyten:\nCRP:\nKreatinin:\nNatrium:\nKalium:\n' .. 'Blutgruppe:\n\nBemerkung des Labors:', urin = 'Farbe:\nEiweiß:\nGlukose:\nBlut:\nLeukozyten:\nNitrit:\n\nBemerkung des Labors:', dna = 'Probenart:\nProfil vollständig (ja/nein):\nÜbereinstimmung:\n\nBemerkung des Labors:', gewebe = 'Entnahmeort:\nMakroskopisch:\nMikroskopisch:\n\nBemerkung des Labors:', toxikologie = 'Alkohol:\nOpiate:\nAmphetamine:\nCannabinoide:\nBenzodiazepine:\n' .. '\nBemerkung des Labors:', } -- --------------------------------------------------------------------- -- 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 --- Prüft eine Probenart gegen die Konfiguration --- @param kennung any Eingabe --- @return string|nil local function Probenart(kennung) for _, eintrag in ipairs(Medizin.Config.Laborarten) do if eintrag.id == kennung then return eintrag.id end end return nil 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 c.firstname, c.lastname FROM aether_characters c WHERE c.id = ? ]], { charakterId }) if not zeilen or not zeilen[1] then return 'unbekannt' end return zeilen[1].firstname .. ' ' .. zeilen[1].lastname end -- ===================================================================== -- ÜBERSICHT -- ===================================================================== Kern.RegistriereAktion('labor', 'uebersicht', nil, function(quelle, mitarbeiter, daten) local ordner = tostring(daten.ordner or 'offen') local bedingungen, werte = {}, {} if ordner == 'meine' then bedingungen[#bedingungen + 1] = 'l.auftrag_von = ?' werte[#werte + 1] = mitarbeiter.charakterId elseif ordner == 'befund' then -- Ergebnis liegt vor, die Bewertung fehlt noch bedingungen[#bedingungen + 1] = "l.status = 'fertig' AND l.befund IS NULL" elseif ordner == 'archiv' then bedingungen[#bedingungen + 1] = "(l.befund IS NOT NULL OR l.status = 'verworfen')" else bedingungen[#bedingungen + 1] = "l.status IN ('offen', 'in_arbeit')" end if daten.art and daten.art ~= '' then bedingungen[#bedingungen + 1] = 'l.art = ?' werte[#werte + 1] = daten.art end if type(daten.begriff) == 'string' and #daten.begriff >= 2 then bedingungen[#bedingungen + 1] = '(l.kennung LIKE ? OR c.firstname LIKE ? OR c.lastname LIKE ?)' local muster = '%' .. daten.begriff .. '%' for _ = 1, 3 do werte[#werte + 1] = muster end end local liste = MySQL.query.await(([[ SELECT l.id, l.kennung, l.patient, l.art, l.fragestellung, l.dringend, l.status, l.auftrag_am, l.auftrag_von, l.bearbeitet_am, l.ergebnis IS NOT NULL AS hatErgebnis, l.befund IS NOT NULL AS hatBefund, TIMESTAMPDIFF(MINUTE, l.auftrag_am, NOW()) AS offenSeit, c.firstname, c.lastname, c.dob FROM medical_labor l JOIN aether_characters c ON c.id = l.patient WHERE %s ORDER BY l.dringend DESC, l.id DESC LIMIT 120 ]]):format(table.concat(bedingungen, ' AND ')), werte) or {} local artNamen = {} for _, eintrag in ipairs(Medizin.Config.Laborarten) do artNamen[eintrag.id] = eintrag.name end for _, eintrag in ipairs(liste) do eintrag.patientName = eintrag.firstname .. ' ' .. eintrag.lastname eintrag.geburt = tostring(eintrag.dob):sub(1, 10) eintrag.artName = artNamen[eintrag.art] or eintrag.art eintrag.auftragVon = NameVon(eintrag.auftrag_von) eintrag.eigene = eintrag.auftrag_von == mitarbeiter.charakterId eintrag.dringend = eintrag.dringend == 1 eintrag.hatErgebnis = tonumber(eintrag.hatErgebnis) == 1 eintrag.hatBefund = tonumber(eintrag.hatBefund) == 1 eintrag.firstname, eintrag.lastname, eintrag.dob = nil, nil, nil eintrag.auftrag_von = nil end return { ordner = ordner, liste = liste, arten = Medizin.Config.Laborarten, zaehler = { offen = tonumber(MySQL.scalar.await( "SELECT COUNT(*) FROM medical_labor WHERE status IN ('offen', 'in_arbeit')")) or 0, befund = tonumber(MySQL.scalar.await( "SELECT COUNT(*) FROM medical_labor WHERE status = 'fertig' AND befund IS NULL")) or 0, meine = tonumber(MySQL.scalar.await([[ SELECT COUNT(*) FROM medical_labor WHERE auftrag_von = ? AND status <> 'verworfen' ]], { mitarbeiter.charakterId })) or 0, dringend = tonumber(MySQL.scalar.await([[ SELECT COUNT(*) FROM medical_labor WHERE dringend = 1 AND status IN ('offen', 'in_arbeit') ]])) or 0, }, rechte = { beauftragen = Rechte.Hat(mitarbeiter, 'labor.beauftragen'), bearbeiten = Rechte.Hat(mitarbeiter, 'labor.bearbeiten'), befunden = Rechte.Hat(mitarbeiter, 'labor.befunden'), stammdaten = Rechte.Hat(mitarbeiter, 'patient.stammdaten'), }, } end) Kern.RegistriereAktion('labor', 'oeffnen', nil, function(quelle, mitarbeiter, daten) local laborId = tonumber(daten.id) if not laborId then return nil, 'Ungültige Kennung.' end local zeilen = MySQL.query.await([[ SELECT l.*, c.firstname, c.lastname, c.dob, m.blutgruppe, m.allergien, m.hinweis FROM medical_labor l JOIN aether_characters c ON c.id = l.patient LEFT JOIN medical_patient m ON m.`character` = l.patient WHERE l.id = ? ]], { laborId }) if not zeilen or not zeilen[1] then return nil, 'Laborauftrag nicht gefunden.' end local l = zeilen[1] local artName = l.art for _, eintrag in ipairs(Medizin.Config.Laborarten) do if eintrag.id == l.art then artName = eintrag.name end end -- Frühere Aufträge desselben Patienten zum Vergleich local frueher = MySQL.query.await([[ SELECT id, kennung, art, status, auftrag_am, ergebnis IS NOT NULL AS hatErgebnis FROM medical_labor WHERE patient = ? AND id <> ? ORDER BY id DESC LIMIT 10 ]], { l.patient, laborId }) or {} for _, eintrag in ipairs(frueher) do eintrag.hatErgebnis = tonumber(eintrag.hatErgebnis) == 1 end return { auftrag = { id = l.id, kennung = l.kennung, patient = l.patient, patientName = l.firstname .. ' ' .. l.lastname, geburt = tostring(l.dob):sub(1, 10), art = l.art, artName = artName, fragestellung = l.fragestellung, dringend = l.dringend == 1, status = l.status, auftragAm = l.auftrag_am, auftragVon = NameVon(l.auftrag_von), istMeinAuftrag = l.auftrag_von == mitarbeiter.charakterId, bearbeitetAm = l.bearbeitet_am, bearbeitetVon = l.bearbeitet_von and NameVon(l.bearbeitet_von) or nil, ergebnis = l.ergebnis, befund = l.befund, befundAm = l.befund_am, befundVon = l.befund_von and NameVon(l.befund_von) or nil, }, stamm = { blutgruppe = l.blutgruppe, allergien = l.allergien, hinweis = l.hinweis, }, frueher = frueher, vorlage = VORLAGEN[l.art], rechte = { bearbeiten = Rechte.Hat(mitarbeiter, 'labor.bearbeiten'), befunden = Rechte.Hat(mitarbeiter, 'labor.befunden'), stammdaten = Rechte.Hat(mitarbeiter, 'patient.stammdaten'), }, blutgruppen = Medizin.Config.Blutgruppen, } end) -- ===================================================================== -- AUFTRAG STELLEN -- ===================================================================== Kern.RegistriereAktion('labor', 'patientSuche', 'labor.beauftragen', function(quelle, mitarbeiter, daten) local begriff = type(daten.begriff) == 'string' and daten.begriff or '' if #begriff < 2 then return { treffer = {} } end local muster = '%' .. begriff .. '%' local treffer = MySQL.query.await([[ SELECT c.id, c.firstname, c.lastname, c.dob, m.blutgruppe, m.hinweis 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 return { treffer = treffer } end) Kern.RegistriereAktion('labor', 'beauftragen', 'labor.beauftragen', 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 = Probenart(daten.art) if not art then return nil, 'Unbekannte Probenart.' end local fragestellung = Text(daten.fragestellung, 512) if not fragestellung then return nil, 'Bitte angeben, wonach gesucht werden soll.' end local dringend = daten.dringend == true local kennung = Kern.ErzeugeKennung('LAB', 'medical_labor', 'kennung') local id = MySQL.insert.await([[ INSERT INTO medical_labor (kennung, patient, art, fragestellung, dringend, status, auftrag_von) VALUES (?, ?, ?, ?, ?, 'offen', ?) ]], { kennung, patientId, art, fragestellung, dringend and 1 or 0, mitarbeiter.charakterId }) -- Optionale Verknüpfung mit einer Behandlung local behandlungId = tonumber(daten.behandlung) if behandlungId then local gueltig = MySQL.scalar.await( 'SELECT id FROM medical_behandlung WHERE id = ? AND patient = ?', { behandlungId, patientId }) if gueltig then MySQL.insert.await([[ INSERT INTO medical_verknuepfung (von_typ, von_id, zu_typ, zu_id, von) VALUES ('labor', ?, 'behandlung', ?, ?) ]], { tostring(id), tostring(behandlungId), mitarbeiter.charakterId }) end end Protokoll.Schreibe(mitarbeiter, 'labor', 'Laborauftrag gestellt', { typ = 'labor', id = id }, { kennung = kennung, art = art, patient = patientId, dringend = dringend }) -- Das Labor erfährt davon. Bearbeitet wird nichts von selbst. Hinweise.Erstelle(nil, { kategorie = 'labor', prioritaet = dringend and 'hoch' or 'normal', titel = dringend and ('Dringender Laborauftrag · %s'):format(kennung) or ('Neuer Laborauftrag · %s'):format(kennung), text = ('%s %s — %s'):format(person[1].firstname, person[1].lastname, fragestellung), zielTyp = 'labor', zielId = id, }) Kern.Loese('labor.beauftragt', id, mitarbeiter) return { id = id, kennung = kennung } end) -- ===================================================================== -- BEARBEITUNG IM LABOR -- ===================================================================== Kern.RegistriereAktion('labor', 'annehmen', 'labor.bearbeiten', function(quelle, mitarbeiter, daten) local laborId = tonumber(daten.id) if not laborId then return nil, 'Ungültige Kennung.' end local zeilen = MySQL.query.await( 'SELECT id, kennung, status FROM medical_labor WHERE id = ?', { laborId }) if not zeilen or not zeilen[1] then return nil, 'Laborauftrag nicht gefunden.' end if zeilen[1].status ~= 'offen' then return nil, 'Dieser Auftrag ist bereits in Arbeit oder abgeschlossen.' end MySQL.update.await([[ UPDATE medical_labor SET status = 'in_arbeit', bearbeitet_von = ?, bearbeitet_am = NOW() WHERE id = ? ]], { mitarbeiter.charakterId, laborId }) Protokoll.Schreibe(mitarbeiter, 'labor', 'Auftrag in Arbeit genommen', { typ = 'labor', id = laborId }, { kennung = zeilen[1].kennung }) return { ok = true } end) --- Trägt die gemessenen Werte ein. Bewertet wird hier nichts. Kern.RegistriereAktion('labor', 'ergebnis', 'labor.bearbeiten', function(quelle, mitarbeiter, daten) local laborId = tonumber(daten.id) if not laborId then return nil, 'Ungültige Kennung.' end local ergebnis = Text(daten.ergebnis, 8000) if not ergebnis or #ergebnis < 3 then return nil, 'Bitte ein Ergebnis eintragen.' end local zeilen = MySQL.query.await([[ SELECT l.id, l.kennung, l.status, l.dringend, l.auftrag_von, l.art, c.firstname, c.lastname FROM medical_labor l JOIN aether_characters c ON c.id = l.patient WHERE l.id = ? ]], { laborId }) if not zeilen or not zeilen[1] then return nil, 'Laborauftrag nicht gefunden.' end local auftrag = zeilen[1] if auftrag.status == 'verworfen' then return nil, 'Dieser Auftrag wurde verworfen.' end MySQL.update.await([[ UPDATE medical_labor SET ergebnis = ?, status = 'fertig', bearbeitet_von = COALESCE(bearbeitet_von, ?), bearbeitet_am = COALESCE(bearbeitet_am, NOW()) WHERE id = ? ]], { ergebnis, mitarbeiter.charakterId, laborId }) Protokoll.Schreibe(mitarbeiter, 'labor', 'Ergebnis eingetragen', { typ = 'labor', id = laborId }, { kennung = auftrag.kennung, art = auftrag.art }) -- Den Auftraggeber sofort informieren. Bewertet ist damit nichts. Hinweise.Erstelle({ charakterId = auftrag.auftrag_von }, { kategorie = 'labor', prioritaet = auftrag.dringend == 1 and 'hoch' or 'normal', titel = ('Laborergebnis verfügbar · %s'):format(auftrag.kennung), text = ('%s %s — die Bewertung steht noch aus.') :format(auftrag.firstname, auftrag.lastname), zielTyp = 'labor', zielId = laborId, }) -- Die Fristenprüfung soll das nicht gleich noch einmal melden Hinweise.AlsGemeldet('labor_' .. laborId, 'fertig') Kern.Loese('labor.fertig', laborId, mitarbeiter) return { ok = true } end) --- Probe unbrauchbar: Auftrag verwerfen, immer mit Begründung Kern.RegistriereAktion('labor', 'verwerfen', 'labor.bearbeiten', function(quelle, mitarbeiter, daten) local laborId = tonumber(daten.id) if not laborId then return nil, 'Ungültige Kennung.' end local grund = Text(daten.grund, 512) if not grund then return nil, 'Bitte begründen, warum der Auftrag verworfen wird.' end local zeilen = MySQL.query.await([[ SELECT l.id, l.kennung, l.status, l.auftrag_von, c.firstname, c.lastname FROM medical_labor l JOIN aether_characters c ON c.id = l.patient WHERE l.id = ? ]], { laborId }) if not zeilen or not zeilen[1] then return nil, 'Laborauftrag nicht gefunden.' end local auftrag = zeilen[1] if auftrag.status == 'verworfen' then return nil, 'Dieser Auftrag ist bereits verworfen.' end MySQL.update.await([[ UPDATE medical_labor SET status = 'verworfen', ergebnis = ?, bearbeitet_von = COALESCE(bearbeitet_von, ?), bearbeitet_am = COALESCE(bearbeitet_am, NOW()) WHERE id = ? ]], { 'Verworfen: ' .. grund, mitarbeiter.charakterId, laborId }) Protokoll.Schreibe(mitarbeiter, 'labor', 'Auftrag verworfen', { typ = 'labor', id = laborId }, { kennung = auftrag.kennung }, grund) Hinweise.Erstelle({ charakterId = auftrag.auftrag_von }, { kategorie = 'labor', prioritaet = 'normal', titel = ('Laborauftrag verworfen · %s'):format(auftrag.kennung), text = ('%s %s — %s. Ein neuer Auftrag muss von Hand gestellt werden.') :format(auftrag.firstname, auftrag.lastname, grund), zielTyp = 'labor', zielId = laborId, }) Hinweise.AlsGemeldet('labor_' .. laborId, 'fertig') return { ok = true } end) -- ===================================================================== -- BEFUND -- -- Die Bewertung eines Ergebnisses. Sie stammt immer von einem -- Mediziner — das System schreibt hier nie etwas hinein. -- ===================================================================== Kern.RegistriereAktion('labor', 'befund', 'labor.befunden', function(quelle, mitarbeiter, daten) local laborId = tonumber(daten.id) if not laborId then return nil, 'Ungültige Kennung.' end local befund = Text(daten.befund, 8000) if not befund or #befund < 3 then return nil, 'Bitte einen Befund eintragen.' end local zeilen = MySQL.query.await( 'SELECT id, kennung, status, ergebnis FROM medical_labor WHERE id = ?', { laborId }) if not zeilen or not zeilen[1] then return nil, 'Laborauftrag nicht gefunden.' end local auftrag = zeilen[1] if not auftrag.ergebnis then return nil, 'Es liegt noch kein Ergebnis vor, das sich bewerten ließe.' end MySQL.update.await([[ UPDATE medical_labor SET befund = ?, befund_von = ?, befund_am = NOW() WHERE id = ? ]], { befund, mitarbeiter.charakterId, laborId }) Protokoll.Schreibe(mitarbeiter, 'labor', 'Befund erstellt', { typ = 'labor', id = laborId }, { kennung = auftrag.kennung }) Kern.Loese('labor.befundet', laborId, mitarbeiter) return { ok = true } end) -- ===================================================================== -- BLUTGRUPPE ÜBERNEHMEN -- -- Bewusst ein eigener, ausdrücklicher Schritt. Das Labor kann eine -- Blutgruppe bestimmt haben — in die Stammdaten trägt sie ein -- Mediziner ein, weil er dafür geradesteht. -- ===================================================================== Kern.RegistriereAktion('labor', 'blutgruppeUebernehmen', 'patient.stammdaten', function(quelle, mitarbeiter, daten) local laborId = tonumber(daten.id) if not laborId then return nil, 'Ungültige Kennung.' end local zeilen = MySQL.query.await([[ SELECT l.id, l.kennung, l.patient, l.ergebnis, c.firstname, c.lastname FROM medical_labor l JOIN aether_characters c ON c.id = l.patient WHERE l.id = ? ]], { laborId }) if not zeilen or not zeilen[1] then return nil, 'Laborauftrag nicht gefunden.' end local auftrag = zeilen[1] if not auftrag.ergebnis then return nil, 'Zu diesem Auftrag liegt kein Ergebnis vor.' end -- Die Blutgruppe wird nicht aus dem Text gelesen, sondern gewählt local blutgruppe = nil for _, gruppe in ipairs(Medizin.Config.Blutgruppen) do if gruppe == daten.blutgruppe then blutgruppe = gruppe break end end if not blutgruppe then return nil, 'Bitte eine gültige Blutgruppe wählen.' end local alt = MySQL.scalar.await( 'SELECT blutgruppe FROM medical_patient WHERE `character` = ?', { auftrag.patient }) MySQL.query.await([[ INSERT INTO medical_patient (`character`, blutgruppe, aktualisiert_von, aktualisiert_am) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE blutgruppe = VALUES(blutgruppe), aktualisiert_von = VALUES(aktualisiert_von), aktualisiert_am = NOW() ]], { auftrag.patient, blutgruppe, mitarbeiter.charakterId }) Protokoll.Schreibe(mitarbeiter, 'labor', 'Blutgruppe in Stammdaten übernommen', { typ = 'patient', id = auftrag.patient }, { quelle = auftrag.kennung, von = alt, nach = blutgruppe }) Kern.Loese('patient.stammdaten', auftrag.patient, mitarbeiter) return { ok = true, blutgruppe = blutgruppe, vorher = alt } end) -- --------------------------------------------------------------------- -- Schnittstelle für andere Ressourcen -- --------------------------------------------------------------------- --- Liefert die Laboraufträge eines Patienten, roh und unbewertet --- @param patientId number Charakter --- @return table exports('HoleLaborwerte', function(patientId) if not tonumber(patientId) then return {} end return MySQL.query.await([[ SELECT kennung, art, status, fragestellung, ergebnis, befund, auftrag_am, befund_am FROM medical_labor WHERE patient = ? ORDER BY id DESC LIMIT 50 ]], { tonumber(patientId) }) or {} end)