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

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)