aether-framework/resources/[aether]/aether-police/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

339 lines
14 KiB
Lua

-- =====================================================================
-- aether-police / Modul: Fahrzeugverwaltung
--
-- Das System dokumentiert Tatsachen: Standort, Tankstand, Zustand und
-- wer zuletzt gefahren ist. Es entscheidet nichts — es gibt keine
-- automatische Fahrzeugvergabe und keinen automatischen Statuswechsel.
-- Zuweisung, Freigabe und Wartungsstatus setzt immer ein Beamter.
-- =====================================================================
Kern.RegistriereModul({
id = 'fahrzeug', name = 'Fuhrpark', icon = '🚓',
reihenfolge = 10, recht = 'dienstfahrzeug.sehen',
beschreibung = 'Dienstfahrzeuge und Wartung',
})
-- Zulaessige Werte
local STATUS = {
verfuegbar = 'Verfügbar',
im_einsatz = 'Im Einsatz',
werkstatt = 'In der Werkstatt',
ausser_dienst= 'Außer Dienst',
}
local WARTUNG = { ok = 'In Ordnung', faellig = 'Wartung fällig', werkstatt = 'In Reparatur' }
-- ---------------------------------------------------------------------
-- Übersicht
-- ---------------------------------------------------------------------
Kern.RegistriereAktion('fahrzeug', 'liste', nil, function(quelle, beamter, daten)
local bedingungen, werte = { '1=1' }, {}
if daten.abteilung and daten.abteilung ~= '' then
bedingungen[#bedingungen + 1] = 'f.abteilung = ?'
werte[#werte + 1] = daten.abteilung
end
if daten.status and daten.status ~= '' then
bedingungen[#bedingungen + 1] = 'f.status = ?'
werte[#werte + 1] = daten.status
end
if daten.wartung and daten.wartung ~= '' then
bedingungen[#bedingungen + 1] = 'f.wartung = ?'
werte[#werte + 1] = daten.wartung
end
if type(daten.begriff) == 'string' and #daten.begriff >= 2 then
bedingungen[#bedingungen + 1] = '(f.kennzeichen LIKE ? OR f.bezeichnung LIKE ? OR f.modell LIKE ?)'
local muster = '%' .. daten.begriff .. '%'
werte[#werte + 1] = muster
werte[#werte + 1] = muster
werte[#werte + 1] = muster
end
local fahrzeuge = MySQL.query.await(([[
SELECT f.*,
z.firstname AS z_vorname, z.lastname AS z_nachname, zp.dienstnummer AS z_nummer,
l.firstname AS l_vorname, l.lastname AS l_nachname, lp.dienstnummer AS l_nummer
FROM police_fahrzeug f
LEFT JOIN aether_characters z ON z.id = f.zugewiesen_an
LEFT JOIN police_personal zp ON zp.`character` = f.zugewiesen_an
LEFT JOIN aether_characters l ON l.id = f.letzter_fahrer
LEFT JOIN police_personal lp ON lp.`character` = f.letzter_fahrer
WHERE %s
ORDER BY f.abteilung, f.kennzeichen
LIMIT 100
]]):format(table.concat(bedingungen, ' AND ')), werte) or {}
-- Bestandszahlen
local zahlen = MySQL.query.await(
'SELECT status, COUNT(*) AS anzahl FROM police_fahrzeug GROUP BY status') or {}
local bestand = { verfuegbar = 0, im_einsatz = 0, werkstatt = 0, ausser_dienst = 0 }
for _, zeile in ipairs(zahlen) do bestand[zeile.status] = zeile.anzahl end
local wartungFaellig = MySQL.scalar.await(
"SELECT COUNT(*) FROM police_fahrzeug WHERE wartung <> 'ok'") or 0
return {
fahrzeuge = fahrzeuge,
bestand = bestand,
wartungFaellig= wartungFaellig,
abteilungen = Polizei.Config.Abteilungen,
statusliste = STATUS,
wartungsliste = WARTUNG,
rechte = {
zuweisen = Rechte.Hat(beamter, 'dienstfahrzeug.zuweisen'),
wartung = Rechte.Hat(beamter, 'dienstfahrzeug.wartung'),
},
}
end)
Kern.RegistriereAktion('fahrzeug', 'oeffnen', nil, function(quelle, beamter, daten)
local fahrzeugId = tonumber(daten.id)
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
local zeilen = MySQL.query.await([[
SELECT f.*,
z.firstname AS z_vorname, z.lastname AS z_nachname, zp.dienstnummer AS z_nummer,
l.firstname AS l_vorname, l.lastname AS l_nachname, lp.dienstnummer AS l_nummer
FROM police_fahrzeug f
LEFT JOIN aether_characters z ON z.id = f.zugewiesen_an
LEFT JOIN police_personal zp ON zp.`character` = f.zugewiesen_an
LEFT JOIN aether_characters l ON l.id = f.letzter_fahrer
LEFT JOIN police_personal lp ON lp.`character` = f.letzter_fahrer
WHERE f.id = ?
]], { fahrzeugId })
if not zeilen or not zeilen[1] then return nil, 'Fahrzeug nicht gefunden.' end
return {
fahrzeug = zeilen[1],
historie = Protokoll.Lies({ zielTyp = 'fahrzeug', zielId = fahrzeugId, limit = 30 }),
beamteImDienst= Dienst.BaueListe(),
statusliste = STATUS,
wartungsliste = WARTUNG,
rechte = {
zuweisen = Rechte.Hat(beamter, 'dienstfahrzeug.zuweisen'),
wartung = Rechte.Hat(beamter, 'dienstfahrzeug.wartung'),
},
}
end)
-- ---------------------------------------------------------------------
-- Zuweisung — immer manuell
-- ---------------------------------------------------------------------
Kern.RegistriereAktion('fahrzeug', 'zuweisen', 'dienstfahrzeug.zuweisen', function(quelle, beamter, daten)
local fahrzeugId = tonumber(daten.id)
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
local fahrzeug = MySQL.query.await(
'SELECT kennzeichen, bezeichnung, status, zugewiesen_an FROM police_fahrzeug WHERE id = ?',
{ fahrzeugId })
if not fahrzeug or not fahrzeug[1] then return nil, 'Fahrzeug nicht gefunden.' end
-- Freigeben
if daten.freigeben then
MySQL.update.await('UPDATE police_fahrzeug SET zugewiesen_an = NULL WHERE id = ?',
{ fahrzeugId })
Protokoll.Schreibe(beamter, 'fahrzeug', 'Zuweisung aufgehoben',
{ typ = 'fahrzeug', id = fahrzeugId }, { kennzeichen = fahrzeug[1].kennzeichen })
return { ok = true }
end
local zielCharakter = tonumber(daten.charakterId)
if not zielCharakter then return nil, 'Keine Person ausgewählt.' end
-- Nur Personal der Behörde
local personal = MySQL.query.await(
'SELECT dienstnummer FROM police_personal WHERE `character` = ? AND aktiv = 1',
{ zielCharakter })
if not personal or not personal[1] then
return nil, 'Diese Person gehört nicht zum Personalbestand.'
end
if fahrzeug[1].status == 'werkstatt' or fahrzeug[1].status == 'ausser_dienst' then
return nil, ('Das Fahrzeug ist %s und kann nicht zugewiesen werden.')
:format(STATUS[fahrzeug[1].status])
end
MySQL.update.await('UPDATE police_fahrzeug SET zugewiesen_an = ? WHERE id = ?',
{ zielCharakter, fahrzeugId })
Protokoll.Schreibe(beamter, 'fahrzeug', 'Fahrzeug zugewiesen',
{ typ = 'fahrzeug', id = fahrzeugId },
{ kennzeichen = fahrzeug[1].kennzeichen, an = personal[1].dienstnummer }, daten.grund)
-- Den Betroffenen informieren, falls im Dienst
local zielQuelle = Dienst.QuelleVonCharakter(zielCharakter)
if zielQuelle then
Hinweise.Erstelle({ charakterId = zielCharakter }, {
kategorie = 'system', prioritaet = 'normal',
titel = 'Dienstfahrzeug zugewiesen',
text = ('%s (%s) wurde dir von %s zugewiesen.')
:format(fahrzeug[1].bezeichnung, fahrzeug[1].kennzeichen, beamter.dienstnummer),
zielTyp = 'fahrzeug', zielId = fahrzeugId,
})
end
return { ok = true }
end)
-- ---------------------------------------------------------------------
-- Status und Wartung — ebenfalls manuell
-- ---------------------------------------------------------------------
Kern.RegistriereAktion('fahrzeug', 'status', 'dienstfahrzeug.zuweisen', function(quelle, beamter, daten)
local fahrzeugId = tonumber(daten.id)
local status = tostring(daten.status or '')
if not fahrzeugId or not STATUS[status] then return nil, 'Unbekannter Status.' end
local fahrzeug = MySQL.query.await(
'SELECT kennzeichen FROM police_fahrzeug WHERE id = ?', { fahrzeugId })
if not fahrzeug or not fahrzeug[1] then return nil, 'Fahrzeug nicht gefunden.' end
MySQL.update.await('UPDATE police_fahrzeug SET status = ? WHERE id = ?', { status, fahrzeugId })
Protokoll.Schreibe(beamter, 'fahrzeug', 'Status geändert',
{ typ = 'fahrzeug', id = fahrzeugId },
{ kennzeichen = fahrzeug[1].kennzeichen, status = status }, daten.grund)
return { ok = true }
end)
Kern.RegistriereAktion('fahrzeug', 'wartung', 'dienstfahrzeug.wartung', function(quelle, beamter, daten)
local fahrzeugId = tonumber(daten.id)
local wartung = tostring(daten.wartung or '')
if not fahrzeugId or not WARTUNG[wartung] then return nil, 'Unbekannter Wartungsstatus.' end
local fahrzeug = MySQL.query.await(
'SELECT kennzeichen, wartung FROM police_fahrzeug WHERE id = ?', { fahrzeugId })
if not fahrzeug or not fahrzeug[1] then return nil, 'Fahrzeug nicht gefunden.' end
MySQL.update.await('UPDATE police_fahrzeug SET wartung = ? WHERE id = ?', { wartung, fahrzeugId })
Protokoll.Schreibe(beamter, 'fahrzeug', 'Wartungsstatus gesetzt',
{ typ = 'fahrzeug', id = fahrzeugId },
{ kennzeichen = fahrzeug[1].kennzeichen, von = fahrzeug[1].wartung, nach = wartung },
daten.vermerk)
-- Bei fälliger Wartung die Fuhrparkverantwortlichen erinnern
if wartung ~= 'ok' then
Hinweise.Erstelle(nil, {
kategorie = 'system', prioritaet = 'niedrig',
titel = 'Wartung: ' .. fahrzeug[1].kennzeichen,
text = ('%s — %s'):format(WARTUNG[wartung], daten.vermerk or 'ohne Vermerk'),
zielTyp = 'fahrzeug', zielId = fahrzeugId,
})
end
return { ok = true }
end)
-- ---------------------------------------------------------------------
-- Fuhrpark pflegen
-- ---------------------------------------------------------------------
Kern.RegistriereAktion('fahrzeug', 'anlegen', 'dienstfahrzeug.wartung', function(quelle, beamter, daten)
local kennzeichen = type(daten.kennzeichen) == 'string'
and daten.kennzeichen:gsub('%s', ''):upper() or ''
if #kennzeichen < 2 or #kennzeichen > 16 then
return nil, 'Das Kennzeichen muss 2 bis 16 Zeichen lang sein.'
end
local vorhanden = MySQL.scalar.await(
'SELECT id FROM police_fahrzeug WHERE kennzeichen = ?', { kennzeichen })
if vorhanden then return nil, 'Dieses Kennzeichen ist bereits im Fuhrpark.' end
local modell = type(daten.modell) == 'string' and daten.modell:gsub('%s', '') or ''
if #modell < 2 then return nil, 'Bitte das Fahrzeugmodell angeben.' end
local bezeichnung = type(daten.bezeichnung) == 'string'
and daten.bezeichnung:gsub('^%s+', ''):gsub('%s+$', '') or ''
if #bezeichnung < 2 then return nil, 'Bitte eine Bezeichnung angeben.' end
-- Abteilung prüfen
local abteilung = 'streife'
for _, eintrag in ipairs(Polizei.Config.Abteilungen) do
if eintrag.id == daten.abteilung then abteilung = daten.abteilung break end
end
local fahrzeugId = MySQL.insert.await([[
INSERT INTO police_fahrzeug (kennzeichen, modell, bezeichnung, abteilung)
VALUES (?, ?, ?, ?)
]], { kennzeichen, modell:sub(1, 48), bezeichnung:sub(1, 64), abteilung })
Protokoll.Schreibe(beamter, 'fahrzeug', 'Fahrzeug aufgenommen',
{ typ = 'fahrzeug', id = fahrzeugId },
{ kennzeichen = kennzeichen, modell = modell, abteilung = abteilung })
return { id = fahrzeugId }
end)
Kern.RegistriereAktion('fahrzeug', 'entfernen', 'dienstfahrzeug.wartung', function(quelle, beamter, daten)
local fahrzeugId = tonumber(daten.id)
if not fahrzeugId then return nil, 'Ungültige Kennung.' end
local grund = type(daten.grund) == 'string' and daten.grund:sub(1, 255) or ''
if #grund < 3 then return nil, 'Bitte einen Grund für die Ausmusterung angeben.' end
local fahrzeug = MySQL.query.await(
'SELECT kennzeichen, bezeichnung FROM police_fahrzeug WHERE id = ?', { fahrzeugId })
if not fahrzeug or not fahrzeug[1] then return nil, 'Fahrzeug nicht gefunden.' end
MySQL.update.await('DELETE FROM police_fahrzeug WHERE id = ?', { fahrzeugId })
Protokoll.Schreibe(beamter, 'fahrzeug', 'Fahrzeug ausgemustert',
{ typ = 'fahrzeug', id = fahrzeugId },
{ kennzeichen = fahrzeug[1].kennzeichen, bezeichnung = fahrzeug[1].bezeichnung }, grund)
return { ok = true }
end)
-- =====================================================================
-- TELEMETRIE
--
-- Der Client meldet Tatsachen: wo das Fahrzeug steht, wie voll der Tank
-- ist, wie es um den Zustand steht und wer zuletzt gefahren ist.
-- Das ist Dokumentation, keine Entscheidung — Status und Wartung
-- bleiben davon unberührt.
-- =====================================================================
RegisterNetEvent('aether:police:fahrzeugMelden', function(daten)
local quelle = source
local beamter = Dienst.Hole(quelle)
if not beamter then return end
if type(daten) ~= 'table' or type(daten.kennzeichen) ~= 'string' then return end
local kennzeichen = daten.kennzeichen:gsub('%s', ''):upper()
local fahrzeug = MySQL.scalar.await(
'SELECT id FROM police_fahrzeug WHERE kennzeichen = ?', { kennzeichen })
if not fahrzeug then return end -- kein Dienstfahrzeug
local tank = math.max(0, math.min(100, tonumber(daten.tank) or 0))
local zustand = math.max(0, math.min(1000, tonumber(daten.zustand) or 1000))
MySQL.update([[
UPDATE police_fahrzeug
SET tank = ?, zustand = ?, letzter_fahrer = ?, pos_x = ?, pos_y = ?, pos_z = ?
WHERE id = ?
]], {
tank, zustand, beamter.charakterId,
daten.x, daten.y, daten.z, fahrzeug,
})
end)
-- =====================================================================
-- EXPORT
-- =====================================================================
--- Ist ein Kennzeichen ein Dienstfahrzeug?
exports('IstDienstfahrzeug', function(kennzeichen)
if type(kennzeichen) ~= 'string' then return false end
local treffer = MySQL.scalar.await(
'SELECT id FROM police_fahrzeug WHERE kennzeichen = ?',
{ kennzeichen:gsub('%s', ''):upper() })
return treffer ~= nil
end)