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

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)