- 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>
339 lines
14 KiB
Lua
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)
|