- 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>
227 lines
8.4 KiB
Lua
227 lines
8.4 KiB
Lua
-- =====================================================================
|
|
-- aether-medical / Modul: Übersicht (Dashboard)
|
|
--
|
|
-- Zeigt den Stand der Klinik. Jede Zahl wird bei jedem Aufruf frisch
|
|
-- aus der Datenbank gelesen — es gibt keine fest verdrahteten Werte
|
|
-- und keine Kennzahl, die eine Handlung auslöst.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereModul({
|
|
id = 'dashboard', name = 'Übersicht', icon = '⬛',
|
|
reihenfolge = 1,
|
|
beschreibung = 'Lage der Klinik auf einen Blick',
|
|
})
|
|
|
|
--- Zählt und liefert immer eine Zahl zurück
|
|
--- @param sql string Abfrage
|
|
--- @param werte table|nil Parameter
|
|
--- @return number
|
|
local function Zaehle(sql, werte)
|
|
return tonumber(MySQL.scalar.await(sql, werte or {})) or 0
|
|
end
|
|
|
|
Kern.RegistriereAktion('dashboard', 'laden', nil, function(quelle, mitarbeiter)
|
|
local charakterId = mitarbeiter.charakterId
|
|
|
|
-- --- Dienst ---
|
|
local imDienst, notaerzte, aufRtw, imOp = 0, 0, 0, 0
|
|
local leitstelleName = nil
|
|
|
|
for zielQuelle, eintrag in pairs(Dienst.AlleImDienst()) do
|
|
imDienst = imDienst + 1
|
|
if eintrag.funktion == 'notarzt' then notaerzte = notaerzte + 1 end
|
|
if eintrag.funktion == 'rtw' then aufRtw = aufRtw + 1 end
|
|
if eintrag.funktion == 'op' then imOp = imOp + 1 end
|
|
if Dienst.Leitstelle() == zielQuelle then leitstelleName = eintrag.name end
|
|
end
|
|
|
|
-- --- Einsätze ---
|
|
local einsaetze = {
|
|
offen = Zaehle("SELECT COUNT(*) FROM medical_einsatz WHERE status = 'offen'"),
|
|
laufend = Zaehle("SELECT COUNT(*) FROM medical_einsatz WHERE status = 'laufend'"),
|
|
dringend = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_einsatz
|
|
WHERE status IN ('offen', 'laufend') AND prioritaet <= 2
|
|
]]),
|
|
}
|
|
|
|
-- Die jüngsten offenen Einsätze für die Kurzliste
|
|
einsaetze.liste = MySQL.query.await([[
|
|
SELECT id, nummer, titel, kategorie, prioritaet, ort, status, erstellt_am
|
|
FROM medical_einsatz
|
|
WHERE status IN ('offen', 'laufend')
|
|
ORDER BY prioritaet, id DESC
|
|
LIMIT 6
|
|
]]) or {}
|
|
|
|
-- --- Patienten ---
|
|
local patienten = {
|
|
wartend = Zaehle("SELECT COUNT(*) FROM medical_aufnahme WHERE status = 'wartend'"),
|
|
aufgenommen = Zaehle("SELECT COUNT(*) FROM medical_aufnahme WHERE status = 'aufgenommen'"),
|
|
intensiv = Zaehle('SELECT COUNT(*) FROM medical_intensiv WHERE beendet_am IS NULL'),
|
|
kritisch = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_intensiv
|
|
WHERE beendet_am IS NULL AND zustand = 'kritisch'
|
|
]]),
|
|
}
|
|
|
|
-- Die längste Wartezeit in der Notaufnahme
|
|
patienten.laengsteWartezeit = Zaehle([[
|
|
SELECT COALESCE(MAX(TIMESTAMPDIFF(MINUTE, aufgenommen_am, NOW())), 0)
|
|
FROM medical_aufnahme WHERE status = 'wartend'
|
|
]])
|
|
|
|
-- Wartende Patienten für die Kurzliste
|
|
patienten.warteliste = MySQL.query.await([[
|
|
SELECT a.id, a.patient, a.grund, a.prioritaet,
|
|
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'
|
|
ORDER BY a.prioritaet, a.aufgenommen_am
|
|
LIMIT 6
|
|
]]) or {}
|
|
|
|
-- --- Belegung ---
|
|
local bettenGesamt = Zaehle([[
|
|
SELECT COALESCE(SUM(betten), 0) FROM medical_zimmer
|
|
WHERE art = 'bett' AND status <> 'gesperrt'
|
|
]])
|
|
local bettenBelegt = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_belegung b
|
|
JOIN medical_zimmer z ON z.id = b.zimmer
|
|
WHERE b.frei_am IS NULL AND z.art = 'bett'
|
|
]])
|
|
|
|
local belegung = {
|
|
bettenGesamt = bettenGesamt,
|
|
bettenBelegt = bettenBelegt,
|
|
bettenFrei = math.max(0, bettenGesamt - bettenBelegt),
|
|
behandlungFrei = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_zimmer
|
|
WHERE art = 'behandlung' AND status = 'frei'
|
|
]]),
|
|
behandlungGesamt = Zaehle("SELECT COUNT(*) FROM medical_zimmer WHERE art = 'behandlung'"),
|
|
opSaeleGesamt = Zaehle("SELECT COUNT(*) FROM medical_zimmer WHERE art = 'op'"),
|
|
opLaufend = Zaehle("SELECT COUNT(*) FROM medical_operation WHERE status = 'laufend'"),
|
|
opGeplant = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_operation
|
|
WHERE status = 'geplant' AND geplant_fuer >= NOW()
|
|
]]),
|
|
}
|
|
|
|
-- --- Offene Arbeit ---
|
|
local offen = {
|
|
dokumentationen = Zaehle("SELECT COUNT(*) FROM medical_behandlung WHERE status = 'laufend'"),
|
|
eigeneDoku = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_behandlung
|
|
WHERE status = 'laufend' AND erstellt_von = ?
|
|
]], { charakterId }),
|
|
laborOffen = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_labor WHERE status IN ('offen', 'in_arbeit')
|
|
]]),
|
|
laborUnbefundet = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_labor WHERE status = 'fertig' AND befund IS NULL
|
|
]]),
|
|
berichteEntwurf = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_bericht
|
|
WHERE status = 'entwurf' AND erstellt_von = ?
|
|
]], { charakterId }),
|
|
}
|
|
|
|
-- --- Kritische Warnungen ---
|
|
-- Bewusst nur Bestands- und Fristenlage. Kein Patientenzustand,
|
|
-- den das System selbst bewertet hätte.
|
|
local warnungen = {}
|
|
|
|
local leereMedikamente = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_medikament WHERE aktiv = 1 AND bestand <= 0
|
|
]])
|
|
if leereMedikamente > 0 then
|
|
warnungen[#warnungen + 1] = {
|
|
art = 'kritisch', titel = 'Medikamente nicht vorrätig',
|
|
text = ('%d Positionen sind auf null.'):format(leereMedikamente),
|
|
ziel = 'medikament',
|
|
}
|
|
end
|
|
|
|
local knappeMedikamente = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_medikament
|
|
WHERE aktiv = 1 AND bestand > 0 AND bestand <= mindestbestand
|
|
]])
|
|
if knappeMedikamente > 0 then
|
|
warnungen[#warnungen + 1] = {
|
|
art = 'warn', titel = 'Medikamente unter Mindestbestand',
|
|
text = ('%d Positionen sollten nachbestellt werden.'):format(knappeMedikamente),
|
|
ziel = 'medikament',
|
|
}
|
|
end
|
|
|
|
local ablaufendesBlut = Zaehle([[
|
|
SELECT COUNT(*) 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 })
|
|
if ablaufendesBlut > 0 then
|
|
warnungen[#warnungen + 1] = {
|
|
art = 'warn', titel = 'Blutkonserven laufen ab',
|
|
text = ('%d Konserven erreichen bald ihr Ablaufdatum.'):format(ablaufendesBlut),
|
|
ziel = 'blutbank',
|
|
}
|
|
end
|
|
|
|
if patienten.kritisch > 0 then
|
|
warnungen[#warnungen + 1] = {
|
|
art = 'kritisch', titel = 'Kritische Intensivpatienten',
|
|
text = ('%d Patienten sind als kritisch eingetragen.'):format(patienten.kritisch),
|
|
ziel = 'intensiv',
|
|
}
|
|
end
|
|
|
|
if einsaetze.offen > 0 and imDienst == 0 then
|
|
warnungen[#warnungen + 1] = {
|
|
art = 'kritisch', titel = 'Offene Einsätze ohne Besetzung',
|
|
text = 'Es ist niemand im Dienst.',
|
|
ziel = 'leitstelle',
|
|
}
|
|
end
|
|
|
|
if not leitstelleName and einsaetze.offen > 0 then
|
|
warnungen[#warnungen + 1] = {
|
|
art = 'warn', titel = 'Leitstelle unbesetzt',
|
|
text = 'Offene Einsätze werden zurzeit von niemandem verteilt.',
|
|
ziel = 'leitstelle',
|
|
}
|
|
end
|
|
|
|
return {
|
|
mitarbeiter = {
|
|
name = mitarbeiter.name, rolleName = mitarbeiter.rolleName,
|
|
station = mitarbeiter.station, funktion = mitarbeiter.funktion,
|
|
},
|
|
dienst = {
|
|
imDienst = imDienst, notaerzte = notaerzte,
|
|
aufRtw = aufRtw, imOp = imOp,
|
|
leitstelle = leitstelleName,
|
|
liste = Dienst.BaueListe(),
|
|
},
|
|
einsaetze = einsaetze,
|
|
patienten = patienten,
|
|
belegung = belegung,
|
|
offen = offen,
|
|
warnungen = warnungen,
|
|
hinweise = Hinweise.Lade(charakterId, 15),
|
|
stationen = Medizin.Config.Stationen,
|
|
}
|
|
end)
|
|
|
|
--- Markiert einen Hinweis als gelesen
|
|
Kern.RegistriereAktion('dashboard', 'hinweisGelesen', nil, function(quelle, mitarbeiter, daten)
|
|
local id = tonumber(daten.id)
|
|
if not id then return nil, 'Ungültige Kennung.' end
|
|
|
|
Hinweise.MarkiereGelesen(mitarbeiter.charakterId, id)
|
|
return { ok = true }
|
|
end)
|