- 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>
652 lines
26 KiB
Lua
652 lines
26 KiB
Lua
-- =====================================================================
|
|
-- aether-medical / Modul: Fuhrpark
|
|
--
|
|
-- Ein Fahrzeugbuch. Es fährt nichts, gibt nichts aus und ruft keine
|
|
-- Werkstatt.
|
|
--
|
|
-- Zwei Sorten von Angaben stehen hier nebeneinander, und sie werden
|
|
-- streng auseinandergehalten:
|
|
--
|
|
-- * GEMELDET — Tankstand, Zustand und Position kommen aus dem Spiel,
|
|
-- während jemand fährt. Das sind Tatsachen, keine Entscheidungen.
|
|
-- Sie werden nur festgeschrieben und mit ihrem Alter angezeigt.
|
|
--
|
|
-- * EINGETRAGEN — Einsatzstatus, Wartungsstatus und Zuweisung setzt
|
|
-- ausschliesslich ein Mensch. Kein gemeldeter Wert ändert sie je:
|
|
-- ein leerer Tank stellt kein Fahrzeug ausser Dienst, und ein
|
|
-- Schaden schickt keines in die Werkstatt.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereModul({
|
|
id = 'fahrzeug', name = 'Fuhrpark', icon = '🚐',
|
|
reihenfolge = 12, recht = 'fahrzeug.sehen',
|
|
beschreibung = 'Fahrzeuge, Status, Zuweisung und Wartung',
|
|
})
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Grundbestand
|
|
--
|
|
-- Damit sich überhaupt etwas zuweisen lässt. Die Pflege übernimmt
|
|
-- danach die Fuhrparkverwaltung.
|
|
-- ---------------------------------------------------------------------
|
|
local GRUNDFAHRZEUGE = {
|
|
-- kennzeichen, modell, bezeichnung, art, station, ausstattung
|
|
{ 'LSMC 001', 'ambulance', 'RTW 1', 'rtw', 'notaufnahme',
|
|
'Defibrillator, Beatmungsgerät, Notfallrucksack, Trage, Sauerstoff' },
|
|
{ 'LSMC 002', 'ambulance', 'RTW 2', 'rtw', 'notaufnahme',
|
|
'Defibrillator, Notfallrucksack, Trage, Sauerstoff' },
|
|
{ 'LSMC 010', 'ambulance', 'NEF 1', 'nef', 'notaufnahme',
|
|
'Notfallrucksack, Notfallmedikamente, Perfusor' },
|
|
{ 'LSMC 020', 'ambulance', 'KTW 1', 'ktw', 'rettung',
|
|
'Trage, Sauerstoff' },
|
|
}
|
|
|
|
CreateThread(function()
|
|
Wait(2500)
|
|
|
|
local vorhanden = tonumber(MySQL.scalar.await('SELECT COUNT(*) FROM medical_fahrzeug')) or 0
|
|
if vorhanden > 0 then return end
|
|
|
|
for _, eintrag in ipairs(GRUNDFAHRZEUGE) do
|
|
MySQL.insert.await([[
|
|
INSERT INTO medical_fahrzeug
|
|
(kennzeichen, modell, bezeichnung, art, station, ausstattung)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
]], { eintrag[1], eintrag[2], eintrag[3], eintrag[4], eintrag[5], eintrag[6] })
|
|
end
|
|
|
|
Aether.Debug('Grundbestand an Fahrzeugen angelegt.')
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfsfunktionen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Begrenzt einen Text und liefert nil bei leerer Eingabe
|
|
--- @param wert any Eingabe
|
|
--- @param laenge number Maximale Zeichenzahl
|
|
--- @return string|nil
|
|
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
|
|
|
|
--- Name eines Charakters für die Anzeige
|
|
--- @param charakterId number|nil Charakter
|
|
--- @return string
|
|
local function NameVon(charakterId)
|
|
if not charakterId then return 'unbekannt' end
|
|
local zeilen = MySQL.query.await(
|
|
'SELECT firstname, lastname FROM aether_characters WHERE id = ?', { charakterId })
|
|
if not zeilen or not zeilen[1] then return 'unbekannt' end
|
|
return zeilen[1].firstname .. ' ' .. zeilen[1].lastname
|
|
end
|
|
|
|
--- Prüft eine Kennung gegen eine Konfigurationsliste
|
|
--- @param liste table Liste aus Medizin.Config
|
|
--- @param wert any Eingabe
|
|
--- @return string|nil Gültige Kennung
|
|
local function AusListe(liste, wert)
|
|
local gesucht = tostring(wert or '')
|
|
for _, eintrag in ipairs(liste) do
|
|
if eintrag.id == gesucht then return eintrag.id end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Bringt ein Kennzeichen auf eine vergleichbare Schreibweise.
|
|
--- Das Spiel liefert Kennzeichen mit Leerzeichen aufgefüllt.
|
|
--- @param wert any Eingabe
|
|
--- @return string|nil
|
|
local function Kennzeichen(wert)
|
|
local sauber = Text(wert, 12)
|
|
if not sauber then return nil end
|
|
return sauber:upper()
|
|
end
|
|
|
|
--- Anzeigename aus einer Konfigurationsliste
|
|
--- @param liste table Liste aus Medizin.Config
|
|
--- @param kennung string Kennung
|
|
--- @return string
|
|
local function NameAusListe(liste, kennung)
|
|
for _, eintrag in ipairs(liste) do
|
|
if eintrag.id == kennung then return eintrag.name end
|
|
end
|
|
return tostring(kennung)
|
|
end
|
|
|
|
--- Lädt ein Fahrzeug
|
|
--- @param fahrzeugId number Fahrzeug
|
|
--- @return table|nil
|
|
local function LadeFahrzeug(fahrzeugId)
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT f.*,
|
|
TIMESTAMPDIFF(MINUTE, f.gemeldet_am, NOW()) AS meldungAlter
|
|
FROM medical_fahrzeug f
|
|
WHERE f.id = ?
|
|
]], { fahrzeugId })
|
|
return zeilen and zeilen[1] or nil
|
|
end
|
|
|
|
--- Bereitet die gemeldeten Werte für die Anzeige auf.
|
|
--- Wichtig ist dabei nicht der Wert allein, sondern sein Alter: eine
|
|
--- alte Meldung sagt nichts über den jetzigen Zustand.
|
|
--- @param eintrag table Fahrzeugzeile
|
|
local function BereiteMeldung(eintrag)
|
|
eintrag.tank = eintrag.tank and math.floor(tonumber(eintrag.tank) + 0.5) or nil
|
|
eintrag.zustand = eintrag.zustand and math.floor(tonumber(eintrag.zustand) + 0.5) or nil
|
|
eintrag.meldungAlter = eintrag.gemeldet_am and (tonumber(eintrag.meldungAlter) or 0) or nil
|
|
eintrag.meldungVeraltet = eintrag.meldungAlter ~= nil
|
|
and eintrag.meldungAlter > Medizin.Config.Erinnerungen.fahrzeugMeldungAltNach
|
|
|
|
eintrag.letzterFahrer = eintrag.letzter_fahrer
|
|
and NameVon(eintrag.letzter_fahrer) or nil
|
|
eintrag.zugewiesenAn = eintrag.zugewiesen_an
|
|
and NameVon(eintrag.zugewiesen_an) or nil
|
|
|
|
eintrag.artName = NameAusListe(Medizin.Config.Fahrzeugarten, eintrag.art)
|
|
eintrag.statusName = NameAusListe(Medizin.Config.Fahrzeugstatus, eintrag.status)
|
|
eintrag.wartungName = NameAusListe(Medizin.Config.Wartungsstatus, eintrag.wartung)
|
|
end
|
|
|
|
-- =====================================================================
|
|
-- ÜBERSICHT
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('fahrzeug', 'uebersicht', nil, function(quelle, mitarbeiter, daten)
|
|
local ordner = tostring(daten.ordner or 'dienst')
|
|
|
|
-- Welche Status gehören in diesen Ordner?
|
|
local bedingung, werte = nil, {}
|
|
if ordner == 'werkstatt' then
|
|
bedingung = "f.status = 'werkstatt' OR f.wartung <> 'ok'"
|
|
elseif ordner == 'ausser' then
|
|
bedingung = "f.status = 'ausser_dienst'"
|
|
else
|
|
bedingung = "f.status IN ('verfuegbar', 'im_einsatz')"
|
|
end
|
|
|
|
if daten.art and daten.art ~= '' then
|
|
bedingung = ('(%s) AND f.art = ?'):format(bedingung)
|
|
werte[#werte + 1] = tostring(daten.art)
|
|
end
|
|
|
|
local liste = MySQL.query.await(([[
|
|
SELECT f.*,
|
|
TIMESTAMPDIFF(MINUTE, f.gemeldet_am, NOW()) AS meldungAlter
|
|
FROM medical_fahrzeug f
|
|
WHERE %s
|
|
ORDER BY f.art, f.bezeichnung
|
|
LIMIT 200
|
|
]]):format(bedingung), werte) or {}
|
|
|
|
for _, eintrag in ipairs(liste) do
|
|
BereiteMeldung(eintrag)
|
|
-- Rohdaten der Position bleiben dem Client erspart, ausser als Wegpunkt
|
|
eintrag.hatPosition = eintrag.pos_x ~= nil and eintrag.pos_y ~= nil
|
|
end
|
|
|
|
local function Zaehle(sql, parameter)
|
|
return tonumber(MySQL.scalar.await(sql, parameter)) or 0
|
|
end
|
|
|
|
local zaehler = {
|
|
verfuegbar = Zaehle("SELECT COUNT(*) FROM medical_fahrzeug WHERE status = 'verfuegbar'"),
|
|
imEinsatz = Zaehle("SELECT COUNT(*) FROM medical_fahrzeug WHERE status = 'im_einsatz'"),
|
|
werkstatt = Zaehle([[
|
|
SELECT COUNT(*) FROM medical_fahrzeug
|
|
WHERE status = 'werkstatt' OR wartung <> 'ok'
|
|
]]),
|
|
ausserDienst = Zaehle(
|
|
"SELECT COUNT(*) FROM medical_fahrzeug WHERE status = 'ausser_dienst'"),
|
|
zugewiesen = Zaehle(
|
|
'SELECT COUNT(*) FROM medical_fahrzeug WHERE zugewiesen_an IS NOT NULL'),
|
|
}
|
|
|
|
return {
|
|
ordner = ordner,
|
|
liste = liste,
|
|
zaehler = zaehler,
|
|
arten = Medizin.Config.Fahrzeugarten,
|
|
status = Medizin.Config.Fahrzeugstatus,
|
|
wartungsstatus = Medizin.Config.Wartungsstatus,
|
|
stationen = Medizin.Config.Stationen,
|
|
ausstattung = Medizin.Config.Fahrzeugausstattung,
|
|
grenzen = {
|
|
tank = Medizin.Config.Erinnerungen.fahrzeugTankUnter,
|
|
zustand = Medizin.Config.Erinnerungen.fahrzeugZustandUnter,
|
|
meldung = Medizin.Config.Erinnerungen.fahrzeugMeldungAltNach,
|
|
},
|
|
rechte = {
|
|
zuweisen = Rechte.Hat(mitarbeiter, 'fahrzeug.zuweisen'),
|
|
wartung = Rechte.Hat(mitarbeiter, 'fahrzeug.wartung'),
|
|
pflegen = Rechte.Hat(mitarbeiter, 'fahrzeug.pflegen'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EINZELNES FAHRZEUG
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('fahrzeug', 'oeffnen', nil, function(quelle, mitarbeiter, daten)
|
|
local fahrzeugId = tonumber(daten.id)
|
|
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local eintrag = LadeFahrzeug(fahrzeugId)
|
|
if not eintrag then return nil, 'Fahrzeug nicht gefunden.' end
|
|
|
|
BereiteMeldung(eintrag)
|
|
|
|
-- Der Weg des Fahrzeugs steht im Protokoll und wird nur gelesen
|
|
local spur = Protokoll.Lies({ zielTyp = 'fahrzeug', zielId = fahrzeugId, limit = 40 })
|
|
|
|
for _, zeile in ipairs(spur) do
|
|
zeile.autor = zeile.firstname
|
|
and (zeile.firstname .. ' ' .. zeile.lastname) or 'System'
|
|
zeile.firstname, zeile.lastname, zeile.details = nil, nil, nil
|
|
end
|
|
|
|
return {
|
|
fahrzeug = {
|
|
id = eintrag.id, kennzeichen = eintrag.kennzeichen,
|
|
modell = eintrag.modell, bezeichnung = eintrag.bezeichnung,
|
|
art = eintrag.art, artName = eintrag.artName,
|
|
station = eintrag.station,
|
|
status = eintrag.status, statusName = eintrag.statusName,
|
|
wartung = eintrag.wartung, wartungName = eintrag.wartungName,
|
|
ausstattung = eintrag.ausstattung, notiz = eintrag.notiz,
|
|
tank = eintrag.tank, zustand = eintrag.zustand,
|
|
gemeldetAm = eintrag.gemeldet_am,
|
|
meldungAlter = eintrag.meldungAlter,
|
|
meldungVeraltet = eintrag.meldungVeraltet,
|
|
letzterFahrer = eintrag.letzterFahrer,
|
|
zugewiesen = eintrag.zugewiesen_an,
|
|
zugewiesenAn = eintrag.zugewiesenAn,
|
|
position = (eintrag.pos_x and eintrag.pos_y)
|
|
and { x = eintrag.pos_x, y = eintrag.pos_y, z = eintrag.pos_z } or nil,
|
|
},
|
|
spur = spur,
|
|
arten = Medizin.Config.Fahrzeugarten,
|
|
status = Medizin.Config.Fahrzeugstatus,
|
|
wartungsstatus = Medizin.Config.Wartungsstatus,
|
|
stationen = Medizin.Config.Stationen,
|
|
ausstattung = Medizin.Config.Fahrzeugausstattung,
|
|
rechte = {
|
|
zuweisen = Rechte.Hat(mitarbeiter, 'fahrzeug.zuweisen'),
|
|
wartung = Rechte.Hat(mitarbeiter, 'fahrzeug.wartung'),
|
|
pflegen = Rechte.Hat(mitarbeiter, 'fahrzeug.pflegen'),
|
|
},
|
|
}
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EINSATZSTATUS
|
|
--
|
|
-- Wird immer eingetragen. Weder Telemetrie noch ein laufender Einsatz
|
|
-- setzt ihn — auch ein Fahrzeug mit leerem Tank bleibt "verfügbar",
|
|
-- bis jemand etwas anderes einträgt.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('fahrzeug', 'status', 'fahrzeug.zuweisen', function(quelle, mitarbeiter, daten)
|
|
local fahrzeugId = tonumber(daten.id)
|
|
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local neu = AusListe(Medizin.Config.Fahrzeugstatus, daten.status)
|
|
if not neu then return nil, 'Unbekannter Fahrzeugstatus.' end
|
|
|
|
local eintrag = LadeFahrzeug(fahrzeugId)
|
|
if not eintrag then return nil, 'Fahrzeug nicht gefunden.' end
|
|
|
|
-- Ein Fahrzeug aus dem Dienst zu nehmen ist Sache der Wartung
|
|
if (neu == 'werkstatt' or neu == 'ausser_dienst')
|
|
and not Rechte.Hat(mitarbeiter, 'fahrzeug.wartung') then
|
|
return nil, 'Fahrzeuge aus dem Dienst nehmen darf nur, wer die Wartung führt.'
|
|
end
|
|
|
|
local grund = Text(daten.grund, 255)
|
|
if (neu == 'werkstatt' or neu == 'ausser_dienst') and not grund then
|
|
return nil, 'Bitte angeben, warum das Fahrzeug nicht mehr fährt.'
|
|
end
|
|
|
|
MySQL.update.await('UPDATE medical_fahrzeug SET status = ? WHERE id = ?',
|
|
{ neu, fahrzeugId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'fahrzeug', 'Fahrzeugstatus gesetzt',
|
|
{ typ = 'fahrzeug', id = fahrzeugId },
|
|
{ kennzeichen = eintrag.kennzeichen, bezeichnung = eintrag.bezeichnung,
|
|
von = eintrag.status, nach = neu }, grund)
|
|
|
|
-- Ein Fahrzeug, das jemandem zugewiesen war, geht nicht stillschweigend
|
|
if (neu == 'werkstatt' or neu == 'ausser_dienst') and eintrag.zugewiesen_an then
|
|
Hinweise.Erstelle({ charakterId = eintrag.zugewiesen_an }, {
|
|
kategorie = 'system', prioritaet = 'normal',
|
|
titel = 'Dein Fahrzeug wurde aus dem Dienst genommen',
|
|
text = ('%s (%s) — %s.'):format(eintrag.bezeichnung, eintrag.kennzeichen,
|
|
grund or 'ohne weitere Angabe'),
|
|
zielTyp = 'fahrzeug', zielId = fahrzeugId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('fahrzeug.status', fahrzeugId, neu, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- ZUWEISUNG
|
|
--
|
|
-- Ein Fahrzeug wird jemandem zugeteilt oder freigegeben. Das öffnet
|
|
-- keine Tür, gibt keinen Schlüssel aus und spawnt nichts.
|
|
-- =====================================================================
|
|
|
|
--- Wer ist gerade im Dienst?
|
|
Kern.RegistriereAktion('fahrzeug', 'kollegen', 'fahrzeug.zuweisen', function(quelle, mitarbeiter, daten)
|
|
local liste = {}
|
|
|
|
for _, eintrag in pairs(Dienst.AlleImDienst()) do
|
|
liste[#liste + 1] = {
|
|
charakterId = eintrag.charakterId,
|
|
name = eintrag.name,
|
|
rolleName = eintrag.rolleName,
|
|
personalnummer = eintrag.personalnummer,
|
|
funktion = eintrag.funktion,
|
|
status = eintrag.status,
|
|
selbst = eintrag.charakterId == mitarbeiter.charakterId,
|
|
}
|
|
end
|
|
|
|
table.sort(liste, function(a, b)
|
|
return tostring(a.personalnummer) < tostring(b.personalnummer)
|
|
end)
|
|
|
|
return { liste = liste }
|
|
end)
|
|
|
|
Kern.RegistriereAktion('fahrzeug', 'zuweisen', 'fahrzeug.zuweisen', function(quelle, mitarbeiter, daten)
|
|
local fahrzeugId = tonumber(daten.id)
|
|
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local eintrag = LadeFahrzeug(fahrzeugId)
|
|
if not eintrag then return nil, 'Fahrzeug nicht gefunden.' end
|
|
|
|
-- Ohne Ziel wird die Zuweisung aufgehoben
|
|
local zielId = tonumber(daten.charakter)
|
|
|
|
if zielId then
|
|
if eintrag.status == 'werkstatt' or eintrag.status == 'ausser_dienst' then
|
|
return nil, ('%s steht auf "%s" und lässt sich nicht zuweisen.')
|
|
:format(eintrag.bezeichnung, eintrag.statusName or eintrag.status)
|
|
end
|
|
|
|
local zielQuelle = Dienst.QuelleVonCharakter(zielId)
|
|
if not zielQuelle or not Dienst.Hole(zielQuelle) then
|
|
return nil, 'Diese Person ist nicht im Dienst.'
|
|
end
|
|
|
|
-- Ein Fahrzeug gehört immer nur einer Person
|
|
if eintrag.zugewiesen_an and eintrag.zugewiesen_an ~= zielId then
|
|
local vorher = NameVon(eintrag.zugewiesen_an)
|
|
if not Text(daten.grund, 255) then
|
|
return nil, ('%s ist bereits %s zugewiesen — bitte einen Grund angeben.')
|
|
:format(eintrag.bezeichnung, vorher)
|
|
end
|
|
end
|
|
else
|
|
local grund = Text(daten.grund, 255)
|
|
if not grund then return nil, 'Bitte angeben, warum die Zuweisung entfällt.' end
|
|
end
|
|
|
|
MySQL.update.await('UPDATE medical_fahrzeug SET zugewiesen_an = ? WHERE id = ?',
|
|
{ zielId, fahrzeugId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'fahrzeug',
|
|
zielId and 'Fahrzeug zugewiesen' or 'Zuweisung aufgehoben',
|
|
{ typ = 'fahrzeug', id = fahrzeugId },
|
|
{ kennzeichen = eintrag.kennzeichen, bezeichnung = eintrag.bezeichnung,
|
|
vorher = eintrag.zugewiesen_an, nachher = zielId },
|
|
Text(daten.grund, 255))
|
|
|
|
-- Die betroffene Person erfährt es unmittelbar
|
|
if zielId and zielId ~= mitarbeiter.charakterId then
|
|
Hinweise.Erstelle({ charakterId = zielId }, {
|
|
kategorie = 'system', prioritaet = 'normal',
|
|
titel = 'Dir wurde ein Fahrzeug zugewiesen',
|
|
text = ('%s (%s) — von %s.'):format(eintrag.bezeichnung,
|
|
eintrag.kennzeichen, mitarbeiter.name),
|
|
zielTyp = 'fahrzeug', zielId = fahrzeugId,
|
|
})
|
|
end
|
|
|
|
-- Wer es vorher hatte, ebenfalls
|
|
if eintrag.zugewiesen_an and eintrag.zugewiesen_an ~= zielId
|
|
and eintrag.zugewiesen_an ~= mitarbeiter.charakterId then
|
|
Hinweise.Erstelle({ charakterId = eintrag.zugewiesen_an }, {
|
|
kategorie = 'system', prioritaet = 'normal',
|
|
titel = 'Deine Fahrzeugzuweisung wurde geändert',
|
|
text = ('%s (%s) ist dir nicht mehr zugewiesen.')
|
|
:format(eintrag.bezeichnung, eintrag.kennzeichen),
|
|
zielTyp = 'fahrzeug', zielId = fahrzeugId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('fahrzeug.zugewiesen', fahrzeugId, zielId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- WARTUNG
|
|
--
|
|
-- Der Wartungsstatus ist eine Beurteilung. Ein gemeldeter Schaden
|
|
-- setzt ihn nicht — er erinnert nur daran, hinzusehen.
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('fahrzeug', 'wartung', 'fahrzeug.wartung', function(quelle, mitarbeiter, daten)
|
|
local fahrzeugId = tonumber(daten.id)
|
|
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local neu = AusListe(Medizin.Config.Wartungsstatus, daten.wartung)
|
|
if not neu then return nil, 'Unbekannter Wartungsstatus.' end
|
|
|
|
local eintrag = LadeFahrzeug(fahrzeugId)
|
|
if not eintrag then return nil, 'Fahrzeug nicht gefunden.' end
|
|
|
|
-- Ein leeres Feld löscht die Notiz, ein fehlendes lässt sie stehen
|
|
local notiz = eintrag.notiz
|
|
if daten.notiz ~= nil then notiz = Text(daten.notiz, 255) end
|
|
|
|
MySQL.update.await('UPDATE medical_fahrzeug SET wartung = ?, notiz = ? WHERE id = ?',
|
|
{ neu, notiz, fahrzeugId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'fahrzeug', 'Wartungsstatus gesetzt',
|
|
{ typ = 'fahrzeug', id = fahrzeugId },
|
|
{ kennzeichen = eintrag.kennzeichen, von = eintrag.wartung, nach = neu }, notiz)
|
|
|
|
-- Fällige Wartung meldet das Haus, damit niemand ahnungslos losfährt
|
|
if neu ~= 'ok' and eintrag.wartung == 'ok' then
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'system', prioritaet = 'normal',
|
|
titel = ('Wartung eingetragen · %s'):format(eintrag.bezeichnung),
|
|
text = ('%s (%s) — %s.'):format(eintrag.bezeichnung, eintrag.kennzeichen,
|
|
NameAusListe(Medizin.Config.Wartungsstatus, neu)),
|
|
zielTyp = 'fahrzeug', zielId = fahrzeugId,
|
|
})
|
|
end
|
|
|
|
Kern.Loese('fahrzeug.wartung', fahrzeugId, neu, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- FUHRPARK PFLEGEN
|
|
-- =====================================================================
|
|
|
|
Kern.RegistriereAktion('fahrzeug', 'speichern', 'fahrzeug.pflegen', function(quelle, mitarbeiter, daten)
|
|
local fahrzeugId = tonumber(daten.id)
|
|
|
|
local kennzeichen = Kennzeichen(daten.kennzeichen)
|
|
local modell = Text(daten.modell, 48)
|
|
local bezeichnung = Text(daten.bezeichnung, 64)
|
|
local art = AusListe(Medizin.Config.Fahrzeugarten, daten.art)
|
|
|
|
if not kennzeichen then return nil, 'Bitte ein Kennzeichen angeben.' end
|
|
if not modell then return nil, 'Bitte das Fahrzeugmodell angeben.' end
|
|
if not bezeichnung then return nil, 'Bitte eine Bezeichnung angeben, z. B. RTW 1.' end
|
|
if not art then return nil, 'Unbekannte Fahrzeugart.' end
|
|
|
|
-- Kennzeichen müssen eindeutig bleiben: die Telemetrie findet das
|
|
-- Fahrzeug ausschliesslich darüber wieder.
|
|
local kollision = MySQL.scalar.await(
|
|
'SELECT id FROM medical_fahrzeug WHERE kennzeichen = ? AND id <> ?',
|
|
{ kennzeichen, fahrzeugId or 0 })
|
|
if kollision then
|
|
return nil, ('Das Kennzeichen %s ist bereits vergeben.'):format(kennzeichen)
|
|
end
|
|
|
|
-- Eine Station ist freiwillig, muss aber eine bekannte sein
|
|
local station = nil
|
|
if Text(daten.station, 32) then
|
|
station = AusListe(Medizin.Config.Stationen, daten.station)
|
|
if not station then return nil, 'Unbekannte Station.' end
|
|
end
|
|
|
|
local ausstattung = Text(daten.ausstattung, 512)
|
|
|
|
if fahrzeugId then
|
|
local vorher = LadeFahrzeug(fahrzeugId)
|
|
if not vorher then return nil, 'Fahrzeug nicht gefunden.' end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_fahrzeug
|
|
SET kennzeichen = ?, modell = ?, bezeichnung = ?, art = ?,
|
|
station = ?, ausstattung = ?
|
|
WHERE id = ?
|
|
]], { kennzeichen, modell, bezeichnung, art, station, ausstattung, fahrzeugId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'fahrzeug', 'Fahrzeug bearbeitet',
|
|
{ typ = 'fahrzeug', id = fahrzeugId },
|
|
{ kennzeichen = kennzeichen, bezeichnung = bezeichnung, art = art,
|
|
vorherigesKennzeichen = vorher.kennzeichen })
|
|
else
|
|
fahrzeugId = MySQL.insert.await([[
|
|
INSERT INTO medical_fahrzeug
|
|
(kennzeichen, modell, bezeichnung, art, station, ausstattung)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
]], { kennzeichen, modell, bezeichnung, art, station, ausstattung })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'fahrzeug', 'Fahrzeug aufgenommen',
|
|
{ typ = 'fahrzeug', id = fahrzeugId },
|
|
{ kennzeichen = kennzeichen, bezeichnung = bezeichnung, art = art })
|
|
end
|
|
|
|
Kern.Loese('fahrzeug.geaendert', fahrzeugId, mitarbeiter)
|
|
|
|
return { id = fahrzeugId }
|
|
end)
|
|
|
|
--- Entfernt ein Fahrzeug endgültig.
|
|
--- Möglich erst, wenn es ausser Dienst steht — ausmustern ist ein
|
|
--- eigener Schritt und keine Nebenwirkung des Löschens.
|
|
Kern.RegistriereAktion('fahrzeug', 'entfernen', 'fahrzeug.pflegen', function(quelle, mitarbeiter, daten)
|
|
local fahrzeugId = tonumber(daten.id)
|
|
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
|
|
|
|
local grund = Text(daten.grund, 255)
|
|
if not grund then return nil, 'Bitte einen Grund angeben.' end
|
|
|
|
local eintrag = LadeFahrzeug(fahrzeugId)
|
|
if not eintrag then return nil, 'Fahrzeug nicht gefunden.' end
|
|
|
|
if eintrag.status ~= 'ausser_dienst' then
|
|
return nil, 'Nur ein Fahrzeug außer Dienst lässt sich aus dem Fuhrpark nehmen.'
|
|
end
|
|
|
|
MySQL.query.await('DELETE FROM medical_fahrzeug WHERE id = ?', { fahrzeugId })
|
|
|
|
Protokoll.Schreibe(mitarbeiter, 'fahrzeug', 'Fahrzeug aus dem Fuhrpark genommen',
|
|
{ typ = 'fahrzeug', id = fahrzeugId },
|
|
{ kennzeichen = eintrag.kennzeichen, bezeichnung = eintrag.bezeichnung }, grund)
|
|
|
|
Kern.Loese('fahrzeug.geaendert', fahrzeugId, mitarbeiter)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- TELEMETRIE
|
|
--
|
|
-- Der Client meldet, was das Spiel hergibt, solange jemand fährt.
|
|
-- Hier landen ausschliesslich Tatsachen — kein gemeldeter Wert ändert
|
|
-- Status, Wartung oder Zuweisung. Wer daraus etwas ableitet, ist ein
|
|
-- Mensch.
|
|
-- =====================================================================
|
|
|
|
RegisterNetEvent('aether:medical:fahrzeugMelden', function(meldung)
|
|
local quelle = source
|
|
|
|
-- Nur wer im Dienst ist, meldet für den Fuhrpark
|
|
local mitarbeiter = Dienst.Hole(quelle)
|
|
if not mitarbeiter then return end
|
|
if type(meldung) ~= 'table' then return end
|
|
|
|
local kennzeichen = Kennzeichen(meldung.kennzeichen)
|
|
if not kennzeichen then return end
|
|
|
|
-- Fremde Fahrzeuge werden stillschweigend verworfen
|
|
local fahrzeug = MySQL.query.await(
|
|
'SELECT id FROM medical_fahrzeug WHERE kennzeichen = ?', { kennzeichen })
|
|
if not fahrzeug or not fahrzeug[1] then return end
|
|
|
|
--- Bringt einen gemeldeten Wert auf 0 bis 100
|
|
--- @param wert any Rohwert aus dem Spiel
|
|
--- @param spanne number Wertebereich, den das Spiel liefert
|
|
--- @return number|nil
|
|
local function Prozent(wert, spanne)
|
|
local zahl = tonumber(wert)
|
|
if not zahl then return nil end
|
|
local anteil = (zahl / spanne) * 100
|
|
return math.max(0, math.min(100, anteil))
|
|
end
|
|
|
|
MySQL.update.await([[
|
|
UPDATE medical_fahrzeug
|
|
SET tank = ?, zustand = ?, pos_x = ?, pos_y = ?, pos_z = ?,
|
|
letzter_fahrer = ?, gemeldet_am = NOW()
|
|
WHERE id = ?
|
|
]], {
|
|
Prozent(meldung.tank, 100), -- Tankstand kommt bereits als Prozent
|
|
Prozent(meldung.zustand, 1000), -- Karosseriezustand reicht von 0 bis 1000
|
|
tonumber(meldung.x), tonumber(meldung.y), tonumber(meldung.z),
|
|
mitarbeiter.charakterId,
|
|
fahrzeug[1].id,
|
|
})
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Schnittstelle für andere Ressourcen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
--- Liefert den Fuhrpark als Auskunft.
|
|
--- Reine Anzeige — keine Ressource kann darüber ein Fahrzeug zuweisen,
|
|
--- seinen Status setzen oder es in die Werkstatt schicken.
|
|
--- @param nurVerfuegbar boolean|nil Nur einsatzbereite Fahrzeuge
|
|
--- @return table Liste
|
|
exports('HoleFuhrpark', function(nurVerfuegbar)
|
|
local bedingung = nurVerfuegbar
|
|
and "WHERE status = 'verfuegbar' AND wartung = 'ok'" or ''
|
|
|
|
local zeilen = MySQL.query.await(([[
|
|
SELECT kennzeichen, bezeichnung, art, station, status, wartung,
|
|
tank, zustand, gemeldet_am
|
|
FROM medical_fahrzeug
|
|
%s
|
|
ORDER BY art, bezeichnung
|
|
]]):format(bedingung)) or {}
|
|
|
|
return zeilen
|
|
end)
|