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

634 lines
25 KiB
Lua

-- =====================================================================
-- aether-medical / Modul: Leitstelle und Einsatzverwaltung
--
-- Ein Notruf erzeugt einen OFFENEN Einsatz. Mehr nicht.
--
-- Das System alarmiert niemanden gezielt, schickt keine Einheit los
-- und schliesst keinen Einsatz ab. Die Leitstelle entscheidet, wen sie
-- anfragt — und selbst eine Anfrage ist nur eine Bitte: die Kraft muss
-- dem Einsatz weiterhin selbst beitreten.
--
-- Der Status „laufend" ist keine Entscheidung, sondern die Aufzeichnung
-- einer Tatsache: es ist jemand gebunden. Abgeschlossen wird immer
-- von Hand.
-- =====================================================================
Kern.RegistriereModul({
id = 'einsatz', name = 'Leitstelle', icon = '🚑',
reihenfolge = 4, recht = 'einsatz.erstellen',
beschreibung = 'Notrufe, Einsätze und Disposition',
})
-- ---------------------------------------------------------------------
-- 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 Prioritätsstufe
local function Prioritaet(wert)
local stufe = tonumber(wert) or 3
if stufe < 1 or stufe > 4 then return 3 end
return math.floor(stufe)
end
--- Prüft eine Einsatzkategorie
--- @param kennung any Eingabe
--- @return string
local function Kategorie(kennung)
for _, eintrag in ipairs(Medizin.Config.Einsatzkategorien) do
if eintrag.id == kennung then return eintrag.id end
end
return 'sonstiges'
end
--- Name eines Mitarbeiters für die Anzeige
local function NameVon(charakterId)
if not charakterId then return 'System' 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
end
--- Wie viele Kräfte sind einem Einsatz gerade zugeordnet?
--- @param einsatzId number Einsatz
--- @return number
local function AnzahlKraefte(einsatzId)
return tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_einsatz_kraft WHERE einsatz = ? AND bis IS NULL',
{ einsatzId })) or 0
end
--- Hält den Status im Einklang mit der Tatsache, ob jemand gebunden ist.
--- Das ist Buchführung, keine Entscheidung: ein abgeschlossener Einsatz
--- wird hier nie wieder geöffnet.
--- @param einsatzId number Einsatz
local function StatusNachKraeften(einsatzId)
local zeilen = MySQL.query.await(
'SELECT status FROM medical_einsatz WHERE id = ?', { einsatzId })
if not zeilen or not zeilen[1] then return end
if zeilen[1].status == 'abgeschlossen' then return end
local neu = AnzahlKraefte(einsatzId) > 0 and 'laufend' or 'offen'
if neu ~= zeilen[1].status then
MySQL.update.await('UPDATE medical_einsatz SET status = ? WHERE id = ?',
{ neu, einsatzId })
end
end
--- Meldet allen im Dienst, dass sich etwas geändert hat
local function Melden()
for quelle in pairs(Dienst.AlleImDienst()) do
TriggerClientEvent('aether:medical:einsatzAenderung', quelle)
end
end
-- =====================================================================
-- EINGANG
--
-- Einziger Weg, auf dem ein Einsatz von aussen entsteht. Global, damit
-- server/main.lua und andere Ressourcen ihn erreichen.
-- =====================================================================
--- Nimmt einen Notfall entgegen und legt ihn als offenen Einsatz ab
--- @param daten table { titel, kategorie?, prioritaet?, ort?, beschreibung?, position?, quelle? }
--- @return number|nil Einsatz-ID
function EinsatzEingang(daten)
if type(daten) ~= 'table' or not daten.titel then return nil end
local nummer = Kern.ErzeugeKennung('EIN', 'medical_einsatz', 'nummer')
local position = daten.position or {}
local prioritaet = Prioritaet(daten.prioritaet)
local kategorie = Kategorie(daten.kategorie)
local id = MySQL.insert.await([[
INSERT INTO medical_einsatz
(nummer, titel, kategorie, prioritaet, ort, pos_x, pos_y, pos_z,
beschreibung, status, quelle, erstellt_von)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'offen', ?, ?)
]], {
nummer,
tostring(daten.titel):sub(1, 160),
kategorie, prioritaet,
daten.ort and tostring(daten.ort):sub(1, 160) or nil,
position.x, position.y, position.z,
daten.beschreibung and tostring(daten.beschreibung):sub(1, 4000) or nil,
daten.quelle or 'system',
daten.erstelltVon,
})
-- Alle im Dienst erfahren davon. Ausgewählt wird niemand.
Hinweise.Erstelle(nil, {
kategorie = 'einsatz',
prioritaet = prioritaet == 1 and 'kritisch' or prioritaet == 2 and 'hoch' or 'normal',
titel = ('Neuer Einsatz · %s'):format(nummer),
text = ('%s%s'):format(daten.titel,
daten.ort and (' — ' .. daten.ort) or ''),
zielTyp = 'einsatz', zielId = id,
})
Kern.Loese('einsatz.eingegangen', id, kategorie)
Melden()
return id
end
-- ---------------------------------------------------------------------
-- Notruf aus dem Verletztenzustand (aether-death)
--
-- Ein am Boden liegender Spieler setzt einen Notruf ab. Daraus wird
-- ein offener Einsatz — niemand wird alarmiert oder hingeschickt.
-- ---------------------------------------------------------------------
AddEventHandler('aether:death:notruf', function(quelle, daten)
if type(daten) ~= 'table' then return end
EinsatzEingang({
titel = 'Bewusstlose Person',
kategorie = 'bewusstlos',
prioritaet = 1,
ort = 'Position des Anrufers',
beschreibung = ('%s liegt verletzt am Boden und hat den Rettungsdienst gerufen.')
:format(daten.name or 'Eine Person'),
position = daten.position,
quelle = 'notruf',
})
end)
-- =====================================================================
-- ÜBERSICHT UND LEITSTELLE
-- =====================================================================
Kern.RegistriereAktion('einsatz', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
local ordner = tostring(daten.ordner or 'aktuell')
local bedingung
if ordner == 'archiv' then
bedingung = "e.status = 'abgeschlossen'"
else
bedingung = "e.status IN ('offen', 'laufend')"
end
local einsaetze = MySQL.query.await(([[
SELECT e.id, e.nummer, e.titel, e.kategorie, e.prioritaet, e.ort,
e.pos_x, e.pos_y, e.pos_z, e.status, e.quelle,
e.erstellt_am, e.erstellt_von, e.geschlossen_am, e.geschlossen_von,
TIMESTAMPDIFF(MINUTE, e.erstellt_am, NOW()) AS offenSeit,
(SELECT COUNT(*) FROM medical_einsatz_kraft k
WHERE k.einsatz = e.id AND k.bis IS NULL) AS kraefte,
(SELECT COUNT(*) FROM medical_einsatz_kraft k
WHERE k.einsatz = e.id AND k.bis IS NULL AND k.`character` = ?) AS ichDabei
FROM medical_einsatz e
WHERE %s
ORDER BY e.prioritaet, e.id DESC
LIMIT 120
]]):format(bedingung), { mitarbeiter.charakterId }) or {}
local kategorieNamen, kategorieFarben = {}, {}
for _, eintrag in ipairs(Medizin.Config.Einsatzkategorien) do
kategorieNamen[eintrag.id] = eintrag.name
kategorieFarben[eintrag.id] = eintrag.farbe
end
for _, einsatz in ipairs(einsaetze) do
einsatz.kategorieName = kategorieNamen[einsatz.kategorie] or einsatz.kategorie
einsatz.farbe = kategorieFarben[einsatz.kategorie]
einsatz.kraefte = tonumber(einsatz.kraefte) or 0
einsatz.ichDabei = tonumber(einsatz.ichDabei) > 0
einsatz.erstelltVon = NameVon(einsatz.erstellt_von)
einsatz.geschlossenVon = einsatz.geschlossen_von
and NameVon(einsatz.geschlossen_von) or nil
einsatz.erstellt_von, einsatz.geschlossen_von = nil, nil
end
-- Verfügbare Kräfte für die Disposition
local kraefte = {}
for zielQuelle, eintrag in pairs(Dienst.AlleImDienst()) do
-- Woran ist diese Kraft gerade gebunden?
local gebunden = MySQL.query.await([[
SELECT e.id, e.nummer FROM medical_einsatz_kraft k
JOIN medical_einsatz e ON e.id = k.einsatz
WHERE k.`character` = ? AND k.bis IS NULL AND e.status <> 'abgeschlossen'
LIMIT 1
]], { eintrag.charakterId })
kraefte[#kraefte + 1] = {
quelle = zielQuelle,
charakterId = eintrag.charakterId,
name = eintrag.name,
personalnummer = eintrag.personalnummer,
rolleName = eintrag.rolleName,
station = eintrag.station,
funktion = eintrag.funktion,
status = eintrag.status,
leitstelle = Dienst.Leitstelle() == zielQuelle,
gebundenAn = gebunden and gebunden[1] and gebunden[1].nummer or nil,
gebundenId = gebunden and gebunden[1] and gebunden[1].id or nil,
}
end
table.sort(kraefte, function(a, b) return a.personalnummer < b.personalnummer end)
return {
ordner = ordner,
einsaetze = einsaetze,
kraefte = kraefte,
kategorien = Medizin.Config.Einsatzkategorien,
status = Medizin.Config.Dienststatus,
funktionen = Medizin.Config.Dienstfunktionen,
istLeitstelle = Dienst.Leitstelle() == quelle,
leitstelleName = (function()
local leit = Dienst.Leitstelle()
local eintrag = leit and Dienst.Hole(leit)
return eintrag and eintrag.name or nil
end)(),
zaehler = {
offen = tonumber(MySQL.scalar.await(
"SELECT COUNT(*) FROM medical_einsatz WHERE status = 'offen'")) or 0,
laufend = tonumber(MySQL.scalar.await(
"SELECT COUNT(*) FROM medical_einsatz WHERE status = 'laufend'")) or 0,
heute = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM medical_einsatz
WHERE status = 'abgeschlossen' AND DATE(geschlossen_am) = CURDATE()
]])) or 0,
},
rechte = {
erstellen = Rechte.Hat(mitarbeiter, 'einsatz.erstellen'),
zuweisen = Rechte.Hat(mitarbeiter, 'einsatz.zuweisen'),
schliessen = Rechte.Hat(mitarbeiter, 'einsatz.schliessen'),
leitstelle = Rechte.Hat(mitarbeiter, 'dienst.leitstelle'),
},
}
end)
Kern.RegistriereAktion('einsatz', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
if not einsatzId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await('SELECT * FROM medical_einsatz WHERE id = ?', { einsatzId })
if not zeilen or not zeilen[1] then return nil, 'Einsatz nicht gefunden.' end
local einsatz = zeilen[1]
local kategorieName = einsatz.kategorie
for _, eintrag in ipairs(Medizin.Config.Einsatzkategorien) do
if eintrag.id == einsatz.kategorie then kategorieName = eintrag.name end
end
-- Beteiligte Kräfte, auch die bereits abgerückten
local kraefte = MySQL.query.await([[
SELECT k.`character`, k.seit, k.bis,
c.firstname, c.lastname, p.personalnummer, p.rolle
FROM medical_einsatz_kraft k
LEFT JOIN aether_characters c ON c.id = k.`character`
LEFT JOIN medical_personal p ON p.`character` = k.`character`
WHERE k.einsatz = ?
ORDER BY k.seit
]], { einsatzId }) or {}
local ichDabei = false
for _, kraft in ipairs(kraefte) do
kraft.name = (kraft.firstname or 'Unbekannt') .. ' ' .. (kraft.lastname or '')
local rolle = Medizin.HoleRolle(kraft.rolle)
kraft.rolleName = rolle and rolle.name or kraft.rolle
kraft.aktiv = kraft.bis == nil
kraft.firstname, kraft.lastname = nil, nil
if kraft.aktiv and kraft.character == mitarbeiter.charakterId then ichDabei = true end
end
-- Notizen zum Einsatzverlauf
local notizen = MySQL.query.await([[
SELECT n.id, n.text, n.am, n.von, c.firstname, c.lastname
FROM medical_einsatz_notiz n
LEFT JOIN aether_characters c ON c.id = n.von
WHERE n.einsatz = ?
ORDER BY n.id DESC LIMIT 60
]], { einsatzId }) or {}
for _, notiz in ipairs(notizen) do
notiz.autor = (notiz.firstname or 'Unbekannt') .. ' ' .. (notiz.lastname or '')
notiz.firstname, notiz.lastname = nil, nil
end
-- Dokumentation, die auf diesen Einsatz verweist
local behandlungen = MySQL.query.await([[
SELECT b.id, b.kennung, b.status, b.diagnose, b.erstellt_am,
c.firstname, c.lastname
FROM medical_behandlung b
LEFT JOIN aether_characters c ON c.id = b.patient
WHERE b.einsatz = ?
ORDER BY b.id DESC LIMIT 20
]], { einsatzId }) or {}
for _, behandlung in ipairs(behandlungen) do
behandlung.patientName = (behandlung.firstname or 'Unbekannt')
.. ' ' .. (behandlung.lastname or '')
behandlung.firstname, behandlung.lastname = nil, nil
end
return {
einsatz = {
id = einsatz.id, nummer = einsatz.nummer, titel = einsatz.titel,
kategorie = einsatz.kategorie, kategorieName = kategorieName,
prioritaet = einsatz.prioritaet, ort = einsatz.ort,
beschreibung = einsatz.beschreibung, status = einsatz.status,
quelle = einsatz.quelle,
position = einsatz.pos_x and
{ x = einsatz.pos_x, y = einsatz.pos_y, z = einsatz.pos_z } or nil,
erstelltAm = einsatz.erstellt_am,
erstelltVon = NameVon(einsatz.erstellt_von),
geschlossenAm = einsatz.geschlossen_am,
geschlossenVon = einsatz.geschlossen_von
and NameVon(einsatz.geschlossen_von) or nil,
},
kraefte = kraefte,
notizen = notizen,
behandlungen = behandlungen,
ichDabei = ichDabei,
rechte = {
zuweisen = Rechte.Hat(mitarbeiter, 'einsatz.zuweisen'),
schliessen = Rechte.Hat(mitarbeiter, 'einsatz.schliessen'),
},
}
end)
-- =====================================================================
-- EINSATZ ANLEGEN
-- =====================================================================
Kern.RegistriereAktion('einsatz', 'erstellen', 'einsatz.erstellen', function(quelle, mitarbeiter, daten)
local titel = Text(daten.titel, 160)
if not titel or #titel < 3 then return nil, 'Bitte einen Einsatztitel angeben.' end
local id = EinsatzEingang({
titel = titel,
kategorie = daten.kategorie,
prioritaet = daten.prioritaet,
ort = Text(daten.ort, 160),
beschreibung = Text(daten.beschreibung, 4000),
position = daten.position,
quelle = 'manuell',
erstelltVon = mitarbeiter.charakterId,
})
if not id then return nil, 'Der Einsatz konnte nicht angelegt werden.' end
Protokoll.Schreibe(mitarbeiter, 'einsatz', 'Einsatz angelegt',
{ typ = 'einsatz', id = id },
{ kategorie = Kategorie(daten.kategorie), prioritaet = Prioritaet(daten.prioritaet) })
return { id = id }
end)
-- =====================================================================
-- BEITRETEN UND VERLASSEN
--
-- Jede Kraft entscheidet selbst. Auch eine Anfrage der Leitstelle
-- bindet niemanden — sie muss angenommen werden.
-- =====================================================================
Kern.RegistriereAktion('einsatz', 'beitreten', nil, function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
if not einsatzId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, nummer, titel, status FROM medical_einsatz WHERE id = ?', { einsatzId })
if not zeilen or not zeilen[1] then return nil, 'Einsatz nicht gefunden.' end
if zeilen[1].status == 'abgeschlossen' then
return nil, 'Dieser Einsatz ist bereits abgeschlossen.'
end
-- Erneutes Beitreten nach dem Abrücken ist erlaubt
MySQL.query.await([[
INSERT INTO medical_einsatz_kraft (einsatz, `character`) VALUES (?, ?)
ON DUPLICATE KEY UPDATE bis = NULL, seit = NOW()
]], { einsatzId, mitarbeiter.charakterId })
StatusNachKraeften(einsatzId)
Protokoll.Schreibe(mitarbeiter, 'einsatz', 'Einsatz übernommen',
{ typ = 'einsatz', id = einsatzId }, { nummer = zeilen[1].nummer })
Kern.Loese('einsatz.beigetreten', einsatzId, mitarbeiter)
Melden()
return { ok = true, position = true }
end)
Kern.RegistriereAktion('einsatz', 'verlassen', nil, function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
if not einsatzId then return nil, 'Ungültige Kennung.' end
MySQL.update.await([[
UPDATE medical_einsatz_kraft SET bis = NOW()
WHERE einsatz = ? AND `character` = ? AND bis IS NULL
]], { einsatzId, mitarbeiter.charakterId })
StatusNachKraeften(einsatzId)
Protokoll.Schreibe(mitarbeiter, 'einsatz', 'Einsatz verlassen',
{ typ = 'einsatz', id = einsatzId }, nil, Text(daten.grund, 255))
Kern.Loese('einsatz.verlassen', einsatzId, mitarbeiter)
Melden()
return { ok = true }
end)
-- =====================================================================
-- DISPOSITION
--
-- Die Leitstelle FRAGT eine Kraft an. Sie zwingt niemanden — der
-- Einsatz erscheint beim Angefragten mit Wegpunkt, beitreten muss
-- er selbst.
-- =====================================================================
Kern.RegistriereAktion('einsatz', 'anfragen', 'einsatz.zuweisen', function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
local zielQuelle = tonumber(daten.quelle)
if not einsatzId or not zielQuelle then return nil, 'Unvollständige Angaben.' end
local ziel = Dienst.Hole(zielQuelle)
if not ziel then return nil, 'Diese Kraft ist nicht im Dienst.' end
local zeilen = MySQL.query.await(
'SELECT id, nummer, titel, ort, pos_x, pos_y, pos_z, status FROM medical_einsatz WHERE id = ?',
{ einsatzId })
if not zeilen or not zeilen[1] then return nil, 'Einsatz nicht gefunden.' end
local einsatz = zeilen[1]
if einsatz.status == 'abgeschlossen' then
return nil, 'Dieser Einsatz ist bereits abgeschlossen.'
end
-- Bereits dabei? Dann ist eine Anfrage sinnlos
local dabei = MySQL.scalar.await([[
SELECT 1 FROM medical_einsatz_kraft
WHERE einsatz = ? AND `character` = ? AND bis IS NULL
]], { einsatzId, ziel.charakterId })
if dabei then return nil, 'Diese Kraft ist dem Einsatz bereits zugeordnet.' end
TriggerClientEvent('aether:medical:einsatzZuweisung', zielQuelle, {
id = einsatz.id,
nummer = einsatz.nummer,
titel = einsatz.titel,
ort = einsatz.ort,
von = mitarbeiter.name,
position = einsatz.pos_x and
{ x = einsatz.pos_x, y = einsatz.pos_y, z = einsatz.pos_z } or nil,
})
Hinweise.Erstelle({ charakterId = ziel.charakterId }, {
kategorie = 'einsatz', prioritaet = 'hoch',
titel = ('Anfrage der Leitstelle · %s'):format(einsatz.nummer),
text = ('%s bittet dich um Übernahme: %s%s')
:format(mitarbeiter.name, einsatz.titel,
einsatz.ort and (' — ' .. einsatz.ort) or ''),
zielTyp = 'einsatz', zielId = einsatz.id,
})
Protokoll.Schreibe(mitarbeiter, 'einsatz', 'Kraft angefragt',
{ typ = 'einsatz', id = einsatzId },
{ nummer = einsatz.nummer, ziel = ziel.personalnummer })
return { ok = true, name = ziel.name }
end)
-- =====================================================================
-- VERLAUF UND ABSCHLUSS
-- =====================================================================
Kern.RegistriereAktion('einsatz', 'notiz', nil, function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
local text = Text(daten.text, 1000)
if not einsatzId then return nil, 'Ungültige Kennung.' end
if not text or #text < 2 then return nil, 'Die Notiz ist zu kurz.' end
local vorhanden = MySQL.scalar.await(
'SELECT id FROM medical_einsatz WHERE id = ?', { einsatzId })
if not vorhanden then return nil, 'Einsatz nicht gefunden.' end
local id = MySQL.insert.await(
'INSERT INTO medical_einsatz_notiz (einsatz, text, von) VALUES (?, ?, ?)',
{ einsatzId, text, mitarbeiter.charakterId })
Melden()
return { id = id }
end)
--- Priorität nachträglich ändern — immer mit Begründung
Kern.RegistriereAktion('einsatz', 'prioritaet', 'einsatz.zuweisen', function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
if not einsatzId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await(
'SELECT id, nummer, prioritaet, status FROM medical_einsatz WHERE id = ?', { einsatzId })
if not zeilen or not zeilen[1] then return nil, 'Einsatz nicht gefunden.' end
if zeilen[1].status == 'abgeschlossen' then
return nil, 'Dieser Einsatz ist abgeschlossen.'
end
local neu = Prioritaet(daten.prioritaet)
local grund = Text(daten.grund, 255)
if not grund then return nil, 'Bitte begründen, warum sich die Priorität ändert.' end
MySQL.update.await('UPDATE medical_einsatz SET prioritaet = ? WHERE id = ?',
{ neu, einsatzId })
Protokoll.Schreibe(mitarbeiter, 'einsatz', 'Priorität geändert',
{ typ = 'einsatz', id = einsatzId },
{ nummer = zeilen[1].nummer, von = zeilen[1].prioritaet, nach = neu }, grund)
Melden()
return { ok = true }
end)
Kern.RegistriereAktion('einsatz', 'schliessen', 'einsatz.schliessen', function(quelle, mitarbeiter, daten)
local einsatzId = tonumber(daten.id)
if not einsatzId then return nil, 'Ungültige Kennung.' end
local vermerk = Text(daten.vermerk, 4000)
if not vermerk or #vermerk < 5 then
return nil, 'Ein Abschluss erfordert einen Abschlussvermerk.'
end
local zeilen = MySQL.query.await(
'SELECT id, nummer, titel, status FROM medical_einsatz WHERE id = ?', { einsatzId })
if not zeilen or not zeilen[1] then return nil, 'Einsatz nicht gefunden.' end
local einsatz = zeilen[1]
if einsatz.status == 'abgeschlossen' then
return nil, 'Dieser Einsatz ist bereits abgeschlossen.'
end
-- Der Abschlussvermerk wird als Notiz Teil des Verlaufs
MySQL.insert.await(
'INSERT INTO medical_einsatz_notiz (einsatz, text, von) VALUES (?, ?, ?)',
{ einsatzId, 'Abschluss: ' .. vermerk, mitarbeiter.charakterId })
-- Noch gebundene Kräfte werden abgemeldet — der Einsatz ist vorbei
MySQL.update.await(
'UPDATE medical_einsatz_kraft SET bis = NOW() WHERE einsatz = ? AND bis IS NULL',
{ einsatzId })
MySQL.update.await([[
UPDATE medical_einsatz
SET status = 'abgeschlossen', geschlossen_von = ?, geschlossen_am = NOW()
WHERE id = ?
]], { mitarbeiter.charakterId, einsatzId })
-- Gibt es schon eine Dokumentation zu diesem Einsatz?
local dokumentiert = tonumber(MySQL.scalar.await(
'SELECT COUNT(*) FROM medical_behandlung WHERE einsatz = ?', { einsatzId })) or 0
Protokoll.Schreibe(mitarbeiter, 'einsatz', 'Einsatz abgeschlossen',
{ typ = 'einsatz', id = einsatzId },
{ nummer = einsatz.nummer, dokumentationen = dokumentiert }, vermerk)
-- Erinnern, nicht anlegen: das System erzeugt keine Behandlungsdoku
if dokumentiert == 0 then
Hinweise.Erstelle({ charakterId = mitarbeiter.charakterId }, {
kategorie = 'doku', prioritaet = 'normal',
titel = 'Dokumentation fehlt',
text = ('Zu %s (%s) ist noch keine Behandlung dokumentiert.')
:format(einsatz.nummer, einsatz.titel),
zielTyp = 'einsatz', zielId = einsatzId,
})
end
Kern.Loese('einsatz.abgeschlossen', einsatzId, mitarbeiter)
Melden()
return { ok = true, dokumentiert = dokumentiert }
end)
-- ---------------------------------------------------------------------
-- Schnittstelle für andere Ressourcen
-- ---------------------------------------------------------------------
--- Liefert die offenen und laufenden Einsätze
--- @return table
exports('HoleOffeneEinsaetze', function()
return MySQL.query.await([[
SELECT id, nummer, titel, kategorie, prioritaet, ort, status,
pos_x, pos_y, pos_z, erstellt_am
FROM medical_einsatz
WHERE status IN ('offen', 'laufend')
ORDER BY prioritaet, id DESC
]]) or {}
end)