- 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>
634 lines
25 KiB
Lua
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)
|