-- ===================================================================== -- aether-medical — Hinweise und Fristenüberwachung -- -- Dies ist der einzige Teil des Systems, der von selbst tätig wird — -- und er tut ausschliesslich eines: ERINNERN. -- -- Kein Hinweis nimmt einen Patienten auf, entlässt ihn, gibt ein -- Medikament aus, wirft eine Blutkonserve weg oder schliesst eine -- Dokumentation ab. Jede Frist läuft ab, ohne dass etwas passiert, -- bis ein Mensch entscheidet. -- ===================================================================== Hinweise = {} -- Bereits gemeldete Fristen, damit nicht im Minutentakt gewarnt wird: -- [schluessel] = { [stufe] = true } local gemeldet = {} --- Erstellt einen Hinweis --- @param ziel table|nil { charakterId? } — ohne charakterId geht er an alle im Dienst --- @param daten table { kategorie, prioritaet?, titel, text?, zielTyp?, zielId? } function Hinweise.Erstelle(ziel, daten) local id = MySQL.insert.await([[ INSERT INTO medical_hinweis (`character`, kategorie, prioritaet, titel, text, ziel_typ, ziel_id) VALUES (?, ?, ?, ?, ?, ?, ?) ]], { ziel and ziel.charakterId or nil, daten.kategorie or 'system', daten.prioritaet or 'normal', daten.titel, daten.text, daten.zielTyp, daten.zielId and tostring(daten.zielId) or nil, }) -- Betroffene Clients sofort informieren. -- Die Feldnamen entsprechen bewusst denen aus Hinweise.Lade, damit ein -- frisch gemeldeter Hinweis genauso anklickbar ist wie ein geladener. local paket = { id = id, kategorie = daten.kategorie or 'system', prioritaet = daten.prioritaet or 'normal', titel = daten.titel, text = daten.text, zielTyp = daten.zielTyp, zielId = daten.zielId, } if ziel and ziel.charakterId then local quelle = Dienst.QuelleVonCharakter(ziel.charakterId) if quelle then TriggerClientEvent('aether:medical:hinweis', quelle, paket) end else for quelle in pairs(Dienst.AlleImDienst()) do TriggerClientEvent('aether:medical:hinweis', quelle, paket) end end return id end --- Lädt die letzten Hinweise eines Mitarbeiters --- @param charakterId number Charakter --- @param limit number|nil Anzahl --- @return table function Hinweise.Lade(charakterId, limit) local zeilen = MySQL.query.await([[ SELECT * FROM medical_hinweis WHERE (`character` = ? OR `character` IS NULL) ORDER BY id DESC LIMIT ? ]], { charakterId, limit or 30 }) or {} -- Sprungziel auf die Schreibweise der Oberfläche bringen for _, eintrag in ipairs(zeilen) do eintrag.zielTyp = eintrag.ziel_typ eintrag.zielId = eintrag.ziel_id eintrag.ziel_typ, eintrag.ziel_id = nil, nil end return zeilen end --- Markiert einen Hinweis als gelesen function Hinweise.MarkiereGelesen(charakterId, hinweisId) MySQL.update([[ UPDATE medical_hinweis SET gelesen = 1 WHERE id = ? AND (`character` = ? OR `character` IS NULL) ]], { hinweisId, charakterId }) end --- Meldet eine Frist genau einmal je Stufe --- @param schluessel string Eindeutige Kennung des Vorgangs --- @param stufe any Stufe der Erinnerung --- @return boolean true, wenn jetzt gemeldet werden soll local function ErstmalsMelden(schluessel, stufe) gemeldet[schluessel] = gemeldet[schluessel] or {} if gemeldet[schluessel][stufe] then return false end gemeldet[schluessel][stufe] = true return true end --- Merkt eine Erinnerung als bereits erledigt vor. --- Ein Modul, das selbst sofort benachrichtigt, verhindert damit, dass --- die Fristenprüfung die gleiche Sache kurz darauf noch einmal meldet. --- @param schluessel string Eindeutige Kennung des Vorgangs --- @param stufe any Stufe der Erinnerung function Hinweise.AlsGemeldet(schluessel, stufe) gemeldet[schluessel] = gemeldet[schluessel] or {} gemeldet[schluessel][stufe] = true end -- --------------------------------------------------------------------- -- Fristenüberwachung -- -- Läuft im Hintergrund. Jeder Block hier endet mit einem Hinweis — -- niemals mit einem UPDATE auf einen medizinischen Vorgang. -- --------------------------------------------------------------------- CreateThread(function() Wait(5000) while true do Wait(Medizin.Config.Erinnerungen.pruefIntervall) -- --- Wartende Patienten --- local wartend = MySQL.query.await([[ SELECT a.id, a.patient, a.prioritaet, a.grund, TIMESTAMPDIFF(MINUTE, a.aufgenommen_am, NOW()) AS wartet, c.firstname, c.lastname FROM medical_aufnahme a JOIN aether_characters c ON c.id = a.patient WHERE a.status = 'wartend' ]]) or {} for _, eintrag in ipairs(wartend) do local wartet = tonumber(eintrag.wartet) or 0 local schluessel = 'warte_' .. eintrag.id -- Höchste erreichte Stufe melden, nicht jede einzeln local stufe = nil for _, grenze in ipairs(Medizin.Config.Erinnerungen.warteZeitStufen) do if wartet >= grenze then stufe = grenze end end if stufe and ErstmalsMelden(schluessel, stufe) then Hinweise.Erstelle(nil, { kategorie = 'patient', prioritaet = tonumber(eintrag.prioritaet) <= 2 and 'hoch' or 'normal', titel = ('Patient wartet seit %d Minuten'):format(stufe), text = ('%s %s — %s'):format(eintrag.firstname, eintrag.lastname, tostring(eintrag.grund):sub(1, 80)), zielTyp = 'aufnahme', zielId = eintrag.id, }) end end -- --- Geplante Operationen --- local operationen = MySQL.query.await([[ SELECT o.id, o.kennung, o.art, o.patient, TIMESTAMPDIFF(MINUTE, NOW(), o.geplant_fuer) AS rest, c.firstname, c.lastname FROM medical_operation o JOIN aether_characters c ON c.id = o.patient WHERE o.status = 'geplant' AND o.geplant_fuer IS NOT NULL AND o.geplant_fuer > DATE_SUB(NOW(), INTERVAL 2 HOUR) ]]) or {} for _, operation in ipairs(operationen) do local rest = tonumber(operation.rest) or 0 local schluessel = 'op_' .. operation.id for _, vorlauf in ipairs(Medizin.Config.Erinnerungen.opVorlauf) do if rest <= vorlauf and rest > 0 and ErstmalsMelden(schluessel, vorlauf) then Hinweise.Erstelle(nil, { kategorie = 'op', prioritaet = 'normal', titel = ('Operation beginnt in %d Minuten'):format(vorlauf), text = ('%s — %s (%s %s)'):format(operation.kennung, operation.art, operation.firstname, operation.lastname), zielTyp = 'operation', zielId = operation.id, }) break end end -- Der geplante Zeitpunkt ist erreicht — die OP startet trotzdem nicht if rest <= 0 and ErstmalsMelden(schluessel, 'faellig') then Hinweise.Erstelle(nil, { kategorie = 'op', prioritaet = 'hoch', titel = 'Geplante Operation ist fällig', text = ('%s — %s. Beginn muss vom OP-Team eingetragen werden.') :format(operation.kennung, operation.art), zielTyp = 'operation', zielId = operation.id, }) end end -- --- Offene Behandlungsdokumentationen --- local offeneDoku = MySQL.query.await([[ SELECT b.id, b.kennung, b.erstellt_von, c.firstname, c.lastname FROM medical_behandlung b JOIN aether_characters c ON c.id = b.patient WHERE b.status = 'laufend' AND b.erstellt_am < DATE_SUB(NOW(), INTERVAL ? MINUTE) ]], { Medizin.Config.Erinnerungen.dokuOffenNach }) or {} for _, doku in ipairs(offeneDoku) do if ErstmalsMelden('doku_' .. doku.id, 'offen') then Hinweise.Erstelle({ charakterId = doku.erstellt_von }, { kategorie = 'doku', prioritaet = 'normal', titel = 'Dokumentation fehlt', text = ('%s (%s %s) ist noch nicht abgeschlossen.') :format(doku.kennung, doku.firstname, doku.lastname), zielTyp = 'behandlung', zielId = doku.id, }) end end -- --- Intensivpatienten ohne frischen Verlaufseintrag --- -- -- Erinnert daran, dass jemand nachschauen sollte. Das System -- schaut nicht nach, misst nichts und ändert keinen Zustand. local intensiv = MySQL.query.await([[ SELECT i.id, i.patient, i.zustand, i.verantwortlich, c.firstname, c.lastname, TIMESTAMPDIFF(MINUTE, COALESCE( (SELECT MAX(v.am) FROM medical_intensiv_verlauf v WHERE v.intensiv = i.id), i.aufgenommen_am), NOW()) AS stillSeit, (SELECT COUNT(*) FROM medical_intensiv_verlauf v WHERE v.intensiv = i.id) AS eintraege FROM medical_intensiv i JOIN aether_characters c ON c.id = i.patient WHERE i.beendet_am IS NULL ]]) or {} for _, aufenthalt in ipairs(intensiv) do local still = tonumber(aufenthalt.stillSeit) or 0 -- Die Stufe enthält die Anzahl der Einträge: nach jedem neuen -- Eintrag beginnt die Erinnerung von vorn. local stufe = 'still_' .. tostring(aufenthalt.eintraege) if still >= Medizin.Config.Erinnerungen.intensivStilleNach and ErstmalsMelden('intensiv_' .. aufenthalt.id, stufe) then -- Wer zuständig ist, wird persönlich erinnert; sonst das ganze Haus local ziel = aufenthalt.verantwortlich and { charakterId = aufenthalt.verantwortlich } or nil Hinweise.Erstelle(ziel, { kategorie = 'patient', prioritaet = aufenthalt.zustand == 'kritisch' and 'hoch' or 'normal', titel = ('Intensivverlauf seit %d Minuten ohne Eintrag'):format(still), text = ('%s %s — zuletzt dokumentiert vor %d Minuten.') :format(aufenthalt.firstname, aufenthalt.lastname, still), zielTyp = 'intensiv', zielId = aufenthalt.id, }) end end -- --- Laborergebnisse ohne Bewertung --- local befunde = MySQL.query.await([[ SELECT l.id, l.kennung, l.art, l.auftrag_von, c.firstname, c.lastname FROM medical_labor l JOIN aether_characters c ON c.id = l.patient WHERE l.status = 'fertig' AND l.befund IS NULL ]]) or {} for _, labor in ipairs(befunde) do if ErstmalsMelden('labor_' .. labor.id, 'fertig') then Hinweise.Erstelle({ charakterId = labor.auftrag_von }, { kategorie = 'labor', prioritaet = 'normal', titel = 'Laborergebnis verfügbar', text = ('%s (%s) für %s %s — Bewertung steht aus.') :format(labor.kennung, labor.art, labor.firstname, labor.lastname), zielTyp = 'labor', zielId = labor.id, }) end end -- --- Medikamente: Mindestbestand und Verfall --- local medikamente = MySQL.query.await([[ SELECT id, kennung, name, bestand, mindestbestand, einheit, DATEDIFF(verfaellt_am, CURDATE()) AS tage FROM medical_medikament WHERE aktiv = 1 AND (bestand <= mindestbestand OR (verfaellt_am IS NOT NULL AND verfaellt_am <= DATE_ADD(CURDATE(), INTERVAL ? DAY))) ]], { Medizin.Config.Erinnerungen.medikamentVorlaufTage }) or {} for _, medikament in ipairs(medikamente) do local schluessel = 'med_' .. medikament.id if medikament.bestand <= medikament.mindestbestand and ErstmalsMelden(schluessel, 'bestand') then Hinweise.Erstelle(nil, { kategorie = 'medikament', prioritaet = medikament.bestand <= 0 and 'hoch' or 'normal', titel = medikament.bestand <= 0 and 'Medikament nicht vorrätig' or 'Medikament fast leer', text = ('%s — Bestand %d %s (Mindestbestand %d).') :format(medikament.name, medikament.bestand, medikament.einheit, medikament.mindestbestand), zielTyp = 'medikament', zielId = medikament.id, }) end local tage = tonumber(medikament.tage) if tage and tage <= Medizin.Config.Erinnerungen.medikamentVorlaufTage and ErstmalsMelden(schluessel, 'verfall') then Hinweise.Erstelle(nil, { kategorie = 'medikament', prioritaet = 'normal', titel = tage <= 0 and 'Medikament abgelaufen' or 'Medikament läuft ab', text = ('%s — %s. Entscheidung über die Entnahme trifft die Apotheke.') :format(medikament.name, tage <= 0 and 'Verfallsdatum überschritten' or ('noch ' .. tage .. ' Tage')), zielTyp = 'medikament', zielId = medikament.id, }) end end -- --- Blutkonserven kurz vor Ablauf --- local konserven = MySQL.query.await([[ SELECT id, konserve, blutgruppe, DATEDIFF(verfaellt_am, CURDATE()) AS tage FROM medical_blut WHERE status IN ('vorraetig', 'reserviert') AND verfaellt_am IS NOT NULL AND verfaellt_am <= DATE_ADD(CURDATE(), INTERVAL ? DAY) ]], { Medizin.Config.Erinnerungen.blutVorlaufTage }) or {} for _, konserve in ipairs(konserven) do local tage = tonumber(konserve.tage) or 0 if ErstmalsMelden('blut_' .. konserve.id, tage <= 0 and 'abgelaufen' or 'bald') then Hinweise.Erstelle(nil, { kategorie = 'blut', prioritaet = tage <= 0 and 'hoch' or 'normal', titel = tage <= 0 and 'Blutkonserve abgelaufen' or 'Blutkonserve läuft ab', text = ('%s (%s) — %s. Verwerfen muss jemand von Hand.') :format(konserve.konserve, konserve.blutgruppe, tage <= 0 and 'Ablaufdatum überschritten' or ('noch ' .. tage .. ' Tage')), zielTyp = 'blut', zielId = konserve.id, }) end end -- --- Blutgruppen mit knappem Bestand --- -- -- Erinnert, dass nachbestellt werden sollte. Das System -- bestellt nichts und gibt nichts frei. for _, gruppe in ipairs(Medizin.Config.Blutgruppen) do local vorraetig = tonumber(MySQL.scalar.await([[ SELECT COUNT(*) FROM medical_blut WHERE blutgruppe = ? AND status = 'vorraetig' ]], { gruppe })) or 0 -- Die Stufe ist der Bestand selbst: jede neue Tiefe meldet sich -- einmal, ein wieder aufgefüllter Bestand meldet sich nicht. if vorraetig <= Medizin.Config.Erinnerungen.blutMindestbestand and ErstmalsMelden('blutbestand_' .. gruppe, tostring(vorraetig)) then Hinweise.Erstelle(nil, { kategorie = 'blut', prioritaet = vorraetig <= 0 and 'hoch' or 'normal', titel = vorraetig <= 0 and ('Blutgruppe %s nicht vorrätig'):format(gruppe) or ('Blutgruppe %s wird knapp'):format(gruppe), text = ('Noch %d Konserve(n) im Bestand (Mindestbestand %d).') :format(vorraetig, Medizin.Config.Erinnerungen.blutMindestbestand), }) end end -- --- Fahrzeuge mit gemeldeten Auffälligkeiten --- -- -- Erinnert an das, was der Fahrer gemeldet hat. Kein Hinweis -- setzt den Status, schickt ein Fahrzeug in die Werkstatt oder -- nimmt es aus dem Dienst — das trägt ein Mensch ein. local fahrzeuge = MySQL.query.await([[ SELECT id, kennzeichen, bezeichnung, tank, zustand FROM medical_fahrzeug WHERE status IN ('verfuegbar', 'im_einsatz') AND gemeldet_am IS NOT NULL AND gemeldet_am > DATE_SUB(NOW(), INTERVAL 1 HOUR) ]]) or {} for _, fahrzeug in ipairs(fahrzeuge) do local tank = tonumber(fahrzeug.tank) local zustand = tonumber(fahrzeug.zustand) -- Die Stufe ist ein Zehnerband: so meldet sich ein sinkender -- Wert höchstens zweimal und nicht bei jedem Prozentpunkt. if tank and tank <= Medizin.Config.Erinnerungen.fahrzeugTankUnter and ErstmalsMelden('fahrzeug_' .. fahrzeug.id, 'tank_' .. tostring(math.floor(tank / 10))) then Hinweise.Erstelle(nil, { kategorie = 'system', prioritaet = tank <= 5 and 'hoch' or 'normal', titel = ('Tankstand gemeldet · %s'):format(fahrzeug.bezeichnung), text = ('%s (%s) — zuletzt %d %% gemeldet.') :format(fahrzeug.bezeichnung, fahrzeug.kennzeichen, math.floor(tank)), zielTyp = 'fahrzeug', zielId = fahrzeug.id, }) end if zustand and zustand <= Medizin.Config.Erinnerungen.fahrzeugZustandUnter and ErstmalsMelden('fahrzeug_' .. fahrzeug.id, 'zustand_' .. tostring(math.floor(zustand / 10))) then Hinweise.Erstelle(nil, { kategorie = 'system', prioritaet = zustand <= 25 and 'hoch' or 'normal', titel = ('Fahrzeugschaden gemeldet · %s'):format(fahrzeug.bezeichnung), text = ('%s (%s) — Zustand zuletzt %d %%. Ob es in die Werkstatt muss, entscheidet die Wartung.') :format(fahrzeug.bezeichnung, fahrzeug.kennzeichen, math.floor(zustand)), zielTyp = 'fahrzeug', zielId = fahrzeug.id, }) end end -- --- Aushänge mit Kenntnisnahmefrist --- local fristen = MySQL.query.await([[ SELECT a.id, a.titel, a.station, TIMESTAMPDIFF(MINUTE, NOW(), a.frist) AS rest FROM medical_aushang a WHERE a.aktiv = 1 AND a.kenntnispflicht = 1 AND a.frist IS NOT NULL AND a.frist > DATE_SUB(NOW(), INTERVAL 1 DAY) ]]) or {} for _, aushang in ipairs(fristen) do local rest = tonumber(aushang.rest) or 0 local stufe = rest <= 0 and 'abgelaufen' or (rest <= Medizin.Config.Erinnerungen.aushangVorwarnung and 'bald') or nil if stufe and ErstmalsMelden('aushang_' .. aushang.id, stufe) then -- Nur ansprechen, wer noch nicht bestätigt hat for _, mitarbeiter in pairs(Dienst.AlleImDienst()) do local passt = aushang.station == nil or aushang.station == mitarbeiter.station if passt then local schon = MySQL.scalar.await([[ SELECT 1 FROM medical_aushang_kenntnis WHERE aushang = ? AND `character` = ? ]], { aushang.id, mitarbeiter.charakterId }) if not schon then Hinweise.Erstelle({ charakterId = mitarbeiter.charakterId }, { kategorie = 'system', prioritaet = stufe == 'abgelaufen' and 'hoch' or 'normal', titel = stufe == 'abgelaufen' and 'Kenntnisnahme überfällig' or 'Kenntnisnahme offen', text = ('„%s" — bitte zur Kenntnis nehmen.'):format(aushang.titel), zielTyp = 'aushang', zielId = aushang.id, }) end end end end end end end) -- --------------------------------------------------------------------- -- Aufräumen abgeschlossener Vorgänge -- --------------------------------------------------------------------- Kern.Abonniere('aufnahme.entlassen', function(aufnahmeId) gemeldet['warte_' .. tostring(aufnahmeId)] = nil end) Kern.Abonniere('aufnahme.aufgenommen', function(aufnahmeId) gemeldet['warte_' .. tostring(aufnahmeId)] = nil end) Kern.Abonniere('operation.abgeschlossen', function(operationId) gemeldet['op_' .. tostring(operationId)] = nil end) Kern.Abonniere('behandlung.abgeschlossen', function(behandlungId) gemeldet['doku_' .. tostring(behandlungId)] = nil end) Kern.Abonniere('labor.befundet', function(laborId) gemeldet['labor_' .. tostring(laborId)] = nil end)