- 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>
220 lines
8.9 KiB
Lua
220 lines
8.9 KiB
Lua
-- =====================================================================
|
|
-- aether-police — Benachrichtigungen und Fristenüberwachung
|
|
--
|
|
-- WICHTIG: Dieses Modul ERINNERT ausschliesslich. Es entlässt niemanden,
|
|
-- schliesst nichts ab und ergreift keine Maßnahme. Jede Meldung endet
|
|
-- in einer Aufforderung an einen Beamten, selbst zu entscheiden.
|
|
-- =====================================================================
|
|
|
|
Hinweise = {}
|
|
|
|
-- Bereits gemeldete Fristen, damit nicht im Minutentakt gewarnt wird:
|
|
-- [gewahrsamId] = { [stufe] = true }
|
|
local gemeldet = {}
|
|
|
|
--- Erstellt einen Hinweis
|
|
--- @param ziel table { charakterId? } — ohne charakterId geht er an alle im Dienst
|
|
--- @param daten table { kategorie, prioritaet?, titel, text?, zielTyp?, zielId? }
|
|
function Hinweise.Erstelle(ziel, daten)
|
|
local id = MySQL.insert.await([[
|
|
INSERT INTO police_hinweis
|
|
(`character`, kategorie, prioritaet, titel, text, ziel_typ, ziel_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
]], {
|
|
ziel and ziel.charakterId or nil,
|
|
daten.kategorie or 'system',
|
|
daten.prioritaet or 'normal',
|
|
daten.titel,
|
|
daten.text,
|
|
daten.zielTyp,
|
|
daten.zielId and tostring(daten.zielId) or nil,
|
|
})
|
|
|
|
-- Betroffene Clients sofort informieren.
|
|
-- Die Feldnamen entsprechen bewusst denen aus Hinweise.Lade, damit ein
|
|
-- frisch gemeldeter Hinweis genauso anklickbar ist wie ein geladener.
|
|
local paket = {
|
|
id = id,
|
|
kategorie = daten.kategorie or 'system',
|
|
prioritaet = daten.prioritaet or 'normal',
|
|
titel = daten.titel,
|
|
text = daten.text,
|
|
zielTyp = daten.zielTyp,
|
|
zielId = daten.zielId,
|
|
}
|
|
|
|
if ziel and ziel.charakterId then
|
|
local quelle = Dienst.QuelleVonCharakter(ziel.charakterId)
|
|
if quelle then
|
|
TriggerClientEvent('aether:police:hinweis', quelle, paket)
|
|
end
|
|
else
|
|
for quelle in pairs(Dienst.AlleImDienst()) do
|
|
TriggerClientEvent('aether:police:hinweis', quelle, paket)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Lädt ungelesene Hinweise eines Beamten
|
|
--- @param charakterId number Charakter
|
|
--- @param limit number|nil Anzahl
|
|
--- @return table
|
|
function Hinweise.Lade(charakterId, limit)
|
|
local zeilen = MySQL.query.await([[
|
|
SELECT * FROM police_hinweis
|
|
WHERE (`character` = ? OR `character` IS NULL)
|
|
ORDER BY id DESC LIMIT ?
|
|
]], { charakterId, limit or 30 }) or {}
|
|
|
|
-- Sprungziel auf die Schreibweise der Oberfläche bringen
|
|
for _, eintrag in ipairs(zeilen) do
|
|
eintrag.zielTyp = eintrag.ziel_typ
|
|
eintrag.zielId = eintrag.ziel_id
|
|
eintrag.ziel_typ, eintrag.ziel_id = nil, nil
|
|
end
|
|
|
|
return zeilen
|
|
end
|
|
|
|
--- Markiert einen Hinweis als gelesen
|
|
function Hinweise.MarkiereGelesen(charakterId, hinweisId)
|
|
MySQL.update('UPDATE police_hinweis SET gelesen = 1 WHERE id = ? AND (`character` = ? OR `character` IS NULL)',
|
|
{ hinweisId, charakterId })
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Fristenüberwachung
|
|
--
|
|
-- Läuft im Hintergrund und erzeugt NUR Erinnerungen. Ein Gewahrsam
|
|
-- endet niemals von selbst — er bleibt offen, bis ein Beamter die
|
|
-- Entlassung ausführt.
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
Wait(5000)
|
|
|
|
while true do
|
|
Wait(Polizei.Config.Erinnerungen.pruefIntervall)
|
|
|
|
-- --- Gewahrsam: auslaufende Fristen ---
|
|
local offene = MySQL.query.await([[
|
|
SELECT g.id, g.character, g.faellig_am, g.grund,
|
|
TIMESTAMPDIFF(MINUTE, NOW(), g.faellig_am) AS rest,
|
|
c.firstname, c.lastname
|
|
FROM police_gewahrsam g
|
|
JOIN aether_characters c ON c.id = g.character
|
|
WHERE g.entlassen_am IS NULL
|
|
]]) or {}
|
|
|
|
for _, eintrag in ipairs(offene) do
|
|
local rest = tonumber(eintrag.rest) or 0
|
|
gemeldet[eintrag.id] = gemeldet[eintrag.id] or {}
|
|
|
|
for _, stufe in ipairs(Polizei.Config.Erinnerungen.gewahrsamStufen) do
|
|
-- Erinnern, sobald die Restzeit die Stufe erreicht oder unterschreitet
|
|
if rest <= stufe and not gemeldet[eintrag.id][stufe] then
|
|
gemeldet[eintrag.id][stufe] = true
|
|
|
|
local name = eintrag.firstname .. ' ' .. eintrag.lastname
|
|
local titel, text, prio
|
|
|
|
if stufe > 0 then
|
|
titel = ('Gewahrsam: noch %d Minuten'):format(stufe)
|
|
text = ('%s — Haftzeit endet bald. Entlassung muss manuell erfolgen.')
|
|
:format(name)
|
|
prio = 'normal'
|
|
else
|
|
titel = 'Haftzeit abgelaufen'
|
|
text = ('%s — die festgelegte Haftzeit ist abgelaufen. Bitte über die Entlassung entscheiden.')
|
|
:format(name)
|
|
prio = 'hoch'
|
|
end
|
|
|
|
Hinweise.Erstelle(nil, {
|
|
kategorie = 'gewahrsam', prioritaet = prio,
|
|
titel = titel, text = text,
|
|
zielTyp = 'gewahrsam', zielId = eintrag.id,
|
|
})
|
|
break -- pro Durchlauf nur eine Stufe melden
|
|
end
|
|
end
|
|
end
|
|
|
|
-- --- Berichte, die zu lange im Entwurf liegen ---
|
|
local entwuerfe = MySQL.query.await([[
|
|
SELECT b.id, b.nummer, b.titel, b.erstellt_von
|
|
FROM police_bericht b
|
|
WHERE b.status = 'entwurf'
|
|
AND b.erstellt_am < DATE_SUB(NOW(), INTERVAL ? MINUTE)
|
|
]], { Polizei.Config.Erinnerungen.berichtOffenNach }) or {}
|
|
|
|
for _, bericht in ipairs(entwuerfe) do
|
|
-- Nur einmal je Bericht erinnern
|
|
local schluessel = 'bericht_' .. bericht.id
|
|
if not gemeldet[schluessel] then
|
|
gemeldet[schluessel] = true
|
|
Hinweise.Erstelle({ charakterId = bericht.erstellt_von }, {
|
|
kategorie = 'bericht', prioritaet = 'normal',
|
|
titel = 'Offener Bericht',
|
|
text = ('%s „%s" liegt noch als Entwurf vor.'):format(bericht.nummer, bericht.titel),
|
|
zielTyp = 'bericht', zielId = bericht.id,
|
|
})
|
|
end
|
|
end
|
|
|
|
-- --- Aushänge mit Kenntnisnahmefrist ---
|
|
-- Das System erinnert nur; es bestätigt keine Kenntnisnahme selbst.
|
|
local fristen = MySQL.query.await([[
|
|
SELECT a.id, a.titel, a.abteilung,
|
|
TIMESTAMPDIFF(MINUTE, NOW(), a.frist) AS rest
|
|
FROM police_aushang a
|
|
WHERE a.aktiv = 1 AND a.kenntnispflicht = 1 AND a.frist IS NOT NULL
|
|
AND a.frist > DATE_SUB(NOW(), INTERVAL 1 DAY)
|
|
]]) or {}
|
|
|
|
for _, aushang in ipairs(fristen) do
|
|
local rest = tonumber(aushang.rest) or 0
|
|
local schluessel = 'aushang_' .. aushang.id
|
|
|
|
-- Einmal vorwarnen, einmal beim Ablauf
|
|
local stufe = rest <= 0 and 'abgelaufen'
|
|
or (rest <= Polizei.Config.Erinnerungen.aushangVorwarnung and 'bald')
|
|
or nil
|
|
|
|
if stufe then
|
|
gemeldet[schluessel] = gemeldet[schluessel] or {}
|
|
if not gemeldet[schluessel][stufe] then
|
|
gemeldet[schluessel][stufe] = true
|
|
|
|
-- Nur Beamte ansprechen, die noch nicht bestätigt haben
|
|
for quelle, beamter in pairs(Dienst.AlleImDienst()) do
|
|
local passt = aushang.abteilung == nil
|
|
or aushang.abteilung == beamter.abteilung
|
|
if passt then
|
|
local schon = MySQL.scalar.await([[
|
|
SELECT 1 FROM police_aushang_kenntnis
|
|
WHERE aushang = ? AND `character` = ?
|
|
]], { aushang.id, beamter.charakterId })
|
|
|
|
if not schon then
|
|
Hinweise.Erstelle({ charakterId = beamter.charakterId }, {
|
|
kategorie = 'system',
|
|
prioritaet = stufe == 'abgelaufen' and 'hoch' or 'normal',
|
|
titel = stufe == 'abgelaufen'
|
|
and 'Kenntnisnahme überfällig' or 'Kenntnisnahme offen',
|
|
text = ('„%s" — bitte zur Kenntnis nehmen.'):format(aushang.titel),
|
|
zielTyp = 'aushang', zielId = aushang.id,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
--- Räumt gemeldete Fristen auf, wenn ein Gewahrsam abgeschlossen wurde
|
|
Kern.Abonniere('gewahrsam.entlassen', function(gewahrsamId)
|
|
gemeldet[gewahrsamId] = nil
|
|
end)
|