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

244 lines
7.9 KiB
Lua

-- =====================================================================
-- aether-police — Dienstsystem
--
-- Speichert ausschliesslich Statusinformationen. Es gibt keine
-- automatische Uniform und keine automatische Fahrzeugausgabe.
-- =====================================================================
Dienst = {}
-- Beamte im Dienst: [quelle] = beamter
local imDienst = {}
-- Wer hält die Leitstelle? (Server-ID oder nil)
local leitstelle = nil
local Core = exports['aether-core']
-- ---------------------------------------------------------------------
-- Zugriff
-- ---------------------------------------------------------------------
--- Liefert die Beamtendaten eines Spielers im Dienst
--- @param quelle number Server-ID
--- @return table|nil
function Dienst.Hole(quelle)
return imDienst[quelle]
end
--- Liefert alle Beamten im Dienst
--- @return table [quelle] = beamter
function Dienst.AlleImDienst()
return imDienst
end
--- Sucht die Server-ID zu einer Charakter-ID
--- @param charakterId number Charakter
--- @return number|nil
function Dienst.QuelleVonCharakter(charakterId)
for quelle, beamter in pairs(imDienst) do
if beamter.charakterId == charakterId then return quelle end
end
return nil
end
--- Wer hält die Leitstelle?
--- @return number|nil Server-ID
function Dienst.Leitstelle()
return leitstelle
end
-- ---------------------------------------------------------------------
-- Personalakte laden
-- ---------------------------------------------------------------------
--- Lädt den Personaldatensatz eines Charakters und baut daraus den Beamten
--- @param quelle number Server-ID
--- @param charakterId number Charakter
--- @return table|nil Beamter, string|nil Fehler
function Dienst.LadeBeamten(quelle, charakterId)
local zeilen = MySQL.query.await(
'SELECT * FROM police_personal WHERE `character` = ? AND aktiv = 1', { charakterId })
if not zeilen or not zeilen[1] then
return nil, 'Du bist nicht im Personalbestand der Behörde.'
end
local personal = zeilen[1]
local person = Core:HoleSpielerInfo(quelle)
local rolle = Polizei.HoleRolle(personal.rolle)
return {
quelle = quelle,
charakterId = charakterId,
name = ((person and person.vorname or '') .. ' ' .. (person and person.nachname or '')),
dienstnummer = personal.dienstnummer,
rolle = personal.rolle,
rolleName = rolle and rolle.name or personal.rolle,
stufe = Polizei.Stufe(personal.rolle),
abteilung = personal.abteilung,
rechte = Rechte.Berechne(personal),
status = 'verfuegbar',
seit = os.time(),
dienstId = nil,
}
end
-- ---------------------------------------------------------------------
-- Dienst antreten und beenden
-- ---------------------------------------------------------------------
--- Meldet einen Beamten zum Dienst an
--- @param quelle number Server-ID
--- @return table|nil Beamter, string|nil Fehler
function Dienst.Antreten(quelle)
if imDienst[quelle] then return imDienst[quelle] end
local charakterId = Core:HoleCharakterId(quelle)
if not charakterId then return nil, 'Kein Charakter geladen.' end
local beamter, fehler = Dienst.LadeBeamten(quelle, charakterId)
if not beamter then return nil, fehler end
if not Rechte.Hat(beamter, 'dienst.antreten') then
return nil, 'Dir fehlt die Berechtigung, den Dienst anzutreten.'
end
-- Dienstzeit protokollieren
beamter.dienstId = MySQL.insert.await(
'INSERT INTO police_dienst (`character`, abteilung) VALUES (?, ?)',
{ charakterId, beamter.abteilung })
imDienst[quelle] = beamter
Protokoll.Schreibe(beamter, 'dienst', 'Dienst angetreten')
Kern.Loese('dienst.angetreten', quelle, beamter)
Dienst.Verteile()
return beamter
end
--- Meldet einen Beamten vom Dienst ab
--- @param quelle number Server-ID
function Dienst.Beenden(quelle)
local beamter = imDienst[quelle]
if not beamter then return end
if beamter.dienstId then
MySQL.update('UPDATE police_dienst SET ende = NOW(), sekunden = ? WHERE id = ?',
{ os.time() - beamter.seit, beamter.dienstId })
end
imDienst[quelle] = nil
-- Leitstelle freigeben, wenn der Inhaber geht
if leitstelle == quelle then
leitstelle = nil
end
Protokoll.Schreibe(beamter, 'dienst', 'Dienst beendet')
Kern.Loese('dienst.beendet', quelle, beamter)
Dienst.Verteile()
end
--- Setzt den Status eines Beamten (verfügbar, Einsatz, Pause ...)
--- @param quelle number Server-ID
--- @param status string Statuskennung
--- @return boolean
function Dienst.SetzeStatus(quelle, status)
local beamter = imDienst[quelle]
if not beamter then return false end
local gueltig = false
for _, eintrag in ipairs(Polizei.Config.Dienststatus) do
if eintrag.id == status then gueltig = true break end
end
if not gueltig then return false end
beamter.status = status
Kern.Loese('dienst.status', quelle, beamter, status)
Dienst.Verteile()
return true
end
--- Übernimmt oder überträgt die Leitstelle
--- @param quelle number Handelnder
--- @param zielQuelle number|nil Ziel (nil = selbst übernehmen)
--- @return boolean, string|nil
function Dienst.SetzeLeitstelle(quelle, zielQuelle)
local beamter = imDienst[quelle]
if not beamter then return false end
if not Rechte.Hat(beamter, 'dienst.leitstelle') then
return false, 'Dir fehlt die Berechtigung für die Leitstelle.'
end
if zielQuelle and zielQuelle ~= quelle then
-- Zuweisen an andere setzt voraus, dass man über ihnen steht
local ziel = imDienst[zielQuelle]
if not ziel then return false, 'Diese Person ist nicht im Dienst.' end
if not Rechte.StehtUeber(beamter, ziel.rolle) then
return false, 'Du kannst die Leitstelle nur an rangniedrigere Beamte übertragen.'
end
leitstelle = zielQuelle
Protokoll.Schreibe(beamter, 'dienst', 'Leitstelle übertragen',
{ typ = 'person', id = ziel.charakterId })
else
leitstelle = quelle
Protokoll.Schreibe(beamter, 'dienst', 'Leitstelle übernommen')
end
Kern.Loese('dienst.leitstelle', leitstelle)
Dienst.Verteile()
return true
end
-- ---------------------------------------------------------------------
-- Verteilung an die Clients
-- ---------------------------------------------------------------------
--- Baut die Liste der Beamten im Dienst (für Dashboard und Karte)
--- @return table
function Dienst.BaueListe()
local liste = {}
for quelle, beamter in pairs(imDienst) do
local position = GetEntityCoords(GetPlayerPed(quelle))
liste[#liste + 1] = {
quelle = quelle,
charakterId = beamter.charakterId,
name = beamter.name,
dienstnummer = beamter.dienstnummer,
rolle = beamter.rolle,
rolleName = beamter.rolleName,
abteilung = beamter.abteilung,
status = beamter.status,
leitstelle = leitstelle == quelle,
seit = beamter.seit,
position = { x = position.x, y = position.y, z = position.z },
}
end
table.sort(liste, function(a, b) return a.dienstnummer < b.dienstnummer end)
return liste
end
--- Schickt die Dienstliste an alle Beamten im Dienst
function Dienst.Verteile()
local liste = Dienst.BaueListe()
for quelle in pairs(imDienst) do
TriggerClientEvent('aether:police:dienstliste', quelle, liste)
end
end
-- Positionen regelmäßig auffrischen (für die Einsatzkarte)
CreateThread(function()
while true do
Wait(5000)
if next(imDienst) then Dienst.Verteile() end
end
end)
-- Beim Verlassen sauber abmelden
AddEventHandler('playerDropped', function()
Dienst.Beenden(source)
end)