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

321 lines
13 KiB
Lua

-- =====================================================================
-- aether-police / Modul: Asservatenkammer
--
-- Jedes Objekt hat eine Kennung, einen Lagerort, einen Verantwortlichen
-- und eine lückenlose Historie. Nichts wird automatisch eingelagert,
-- ausgegeben oder vernichtet — jeder Schritt ist eine Handlung eines
-- Beamten und wird mit Namen festgehalten.
-- =====================================================================
Kern.RegistriereModul({
id = 'asservat', name = 'Asservate', icon = '📦',
reihenfolge = 7, recht = 'asservat.einlagern',
beschreibung = 'Asservatenkammer und Verwahrkette',
})
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
local function KategorieGueltig(kategorie)
for _, eintrag in ipairs(Polizei.Config.Asservatkategorien) do
if eintrag.id == kategorie then return true end
end
return false
end
--- Schreibt einen Historieneintrag zu einem Asservat
--- @param asservatId number
--- @param aktion string
--- @param beamter table
--- @param notiz string|nil
local function Historie(asservatId, aktion, beamter, notiz)
MySQL.insert('INSERT INTO police_asservat_historie (asservat, aktion, von, notiz) VALUES (?, ?, ?, ?)',
{ asservatId, aktion, beamter.charakterId, notiz })
end
-- =====================================================================
-- ÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('asservat', 'liste', nil, function(quelle, beamter, daten)
local bedingungen, werte = { '1=1' }, {}
if daten.kategorie and daten.kategorie ~= '' then
bedingungen[#bedingungen + 1] = 'a.kategorie = ?'
werte[#werte + 1] = daten.kategorie
end
if daten.status and daten.status ~= '' then
bedingungen[#bedingungen + 1] = 'a.status = ?'
werte[#werte + 1] = daten.status
end
if type(daten.begriff) == 'string' and #daten.begriff >= 2 then
bedingungen[#bedingungen + 1] =
'(a.bezeichnung LIKE ? OR a.kennung LIKE ? OR a.lagerort LIKE ?)'
local muster = '%' .. daten.begriff .. '%'
werte[#werte + 1] = muster
werte[#werte + 1] = muster
werte[#werte + 1] = muster
end
local asservate = MySQL.query.await(([[
SELECT a.id, a.kennung, a.kategorie, a.bezeichnung, a.menge, a.lagerort,
a.status, a.eingelagert_am, a.akte, a.beweis,
c.firstname, c.lastname, p.dienstnummer,
k.aktenzeichen
FROM police_asservat a
LEFT JOIN aether_characters c ON c.id = a.verantwortlich
LEFT JOIN police_personal p ON p.`character` = a.verantwortlich
LEFT JOIN police_akte k ON k.id = a.akte
WHERE %s
ORDER BY
CASE a.status WHEN 'eingelagert' THEN 0 WHEN 'ausgegeben' THEN 1 ELSE 2 END,
a.id DESC
LIMIT 80
]]):format(table.concat(bedingungen, ' AND ')), werte) or {}
-- Bestandszahlen für die Übersicht
local zahlen = MySQL.query.await([[
SELECT status, COUNT(*) AS anzahl FROM police_asservat GROUP BY status
]]) or {}
local bestand = { eingelagert = 0, ausgegeben = 0, vernichtet = 0, zurueckgegeben = 0 }
for _, zeile in ipairs(zahlen) do
bestand[zeile.status] = zeile.anzahl
end
return {
asservate = asservate,
kategorien = Polizei.Config.Asservatkategorien,
bestand = bestand,
rechte = {
einlagern = Rechte.Hat(beamter, 'asservat.einlagern'),
ausgeben = Rechte.Hat(beamter, 'asservat.ausgeben'),
vernichten = Rechte.Hat(beamter, 'asservat.vernichten'),
},
}
end)
Kern.RegistriereAktion('asservat', 'oeffnen', nil, function(quelle, beamter, daten)
local asservatId = tonumber(daten.id)
if not asservatId then return nil, 'Ungültige Kennung.' end
local asservat = MySQL.query.await([[
SELECT a.*, c.firstname, c.lastname, p.dienstnummer,
k.aktenzeichen, k.titel AS akte_titel,
b.kennung AS beweis_kennung, b.bezeichnung AS beweis_bezeichnung
FROM police_asservat a
LEFT JOIN aether_characters c ON c.id = a.verantwortlich
LEFT JOIN police_personal p ON p.`character` = a.verantwortlich
LEFT JOIN police_akte k ON k.id = a.akte
LEFT JOIN police_beweis b ON b.id = a.beweis
WHERE a.id = ?
]], { asservatId })
if not asservat or not asservat[1] then return nil, 'Asservat nicht gefunden.' end
-- Lückenlose Verwahrkette
local historie = MySQL.query.await([[
SELECT h.aktion, h.am, h.notiz, c.firstname, c.lastname, p.dienstnummer
FROM police_asservat_historie h
LEFT JOIN aether_characters c ON c.id = h.von
LEFT JOIN police_personal p ON p.`character` = h.von
WHERE h.asservat = ?
ORDER BY h.id ASC
]], { asservatId }) or {}
return {
asservat = asservat[1],
historie = historie,
rechte = {
ausgeben = Rechte.Hat(beamter, 'asservat.ausgeben'),
vernichten = Rechte.Hat(beamter, 'asservat.vernichten'),
},
}
end)
-- =====================================================================
-- EINLAGERN
-- =====================================================================
Kern.RegistriereAktion('asservat', 'einlagern', 'asservat.einlagern', function(quelle, beamter, daten)
if not KategorieGueltig(daten.kategorie) then return nil, 'Unbekannte Kategorie.' end
local bezeichnung = type(daten.bezeichnung) == 'string'
and daten.bezeichnung:gsub('^%s+', ''):gsub('%s+$', '') or ''
if #bezeichnung < 3 or #bezeichnung > 160 then
return nil, 'Die Bezeichnung muss 3 bis 160 Zeichen lang sein.'
end
local lagerort = type(daten.lagerort) == 'string' and daten.lagerort:sub(1, 64) or ''
if #lagerort < 2 then return nil, 'Bitte einen Lagerort angeben.' end
local kennung = Kern.ErzeugeKennung('AS', 'police_asservat', 'kennung')
local asservatId = MySQL.insert.await([[
INSERT INTO police_asservat
(kennung, kategorie, bezeichnung, menge, lagerort, verantwortlich, akte, beweis, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'eingelagert')
]], {
kennung, daten.kategorie, bezeichnung,
type(daten.menge) == 'string' and daten.menge:sub(1, 48) or nil,
lagerort, beamter.charakterId,
tonumber(daten.akte), tonumber(daten.beweis),
})
Historie(asservatId, 'Eingelagert', beamter,
type(daten.notiz) == 'string' and daten.notiz:sub(1, 255) or nil)
Protokoll.Schreibe(beamter, 'asservat', 'Asservat eingelagert',
{ typ = 'asservat', id = asservatId },
{ kennung = kennung, kategorie = daten.kategorie, lagerort = lagerort })
Kern.Loese('asservat.eingelagert', asservatId, beamter)
return { id = asservatId, kennung = kennung }
end)
-- =====================================================================
-- AUSGEBEN
--
-- Wer etwas herausgibt, übernimmt die Verantwortung dafür — deshalb
-- sind Empfänger und Grund Pflicht.
-- =====================================================================
Kern.RegistriereAktion('asservat', 'ausgeben', 'asservat.ausgeben', function(quelle, beamter, daten)
local asservatId = tonumber(daten.id)
if not asservatId then return nil, 'Ungültige Kennung.' end
local asservat = MySQL.query.await(
'SELECT kennung, bezeichnung, status FROM police_asservat WHERE id = ?', { asservatId })
if not asservat or not asservat[1] then return nil, 'Asservat nicht gefunden.' end
if asservat[1].status ~= 'eingelagert' then
return nil, 'Nur eingelagerte Asservate können ausgegeben werden.'
end
local empfaenger = type(daten.empfaenger) == 'string'
and daten.empfaenger:gsub('^%s+', ''):gsub('%s+$', '') or ''
if #empfaenger < 2 then return nil, 'Bitte den Empfänger angeben.' 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 Ausgabe angeben.' end
MySQL.update.await("UPDATE police_asservat SET status = 'ausgegeben' WHERE id = ?", { asservatId })
Historie(asservatId, 'Ausgegeben an ' .. empfaenger, beamter, grund)
Protokoll.Schreibe(beamter, 'asservat', 'Asservat ausgegeben',
{ typ = 'asservat', id = asservatId },
{ kennung = asservat[1].kennung, empfaenger = empfaenger }, grund)
return { ok = true }
end)
--- Rückgabe in die Kammer
Kern.RegistriereAktion('asservat', 'zurueckgeben', 'asservat.ausgeben', function(quelle, beamter, daten)
local asservatId = tonumber(daten.id)
if not asservatId then return nil, 'Ungültige Kennung.' end
local asservat = MySQL.query.await(
'SELECT kennung, status FROM police_asservat WHERE id = ?', { asservatId })
if not asservat or not asservat[1] then return nil, 'Asservat nicht gefunden.' end
if asservat[1].status ~= 'ausgegeben' then
return nil, 'Nur ausgegebene Asservate können zurückgenommen werden.'
end
local lagerort = type(daten.lagerort) == 'string' and daten.lagerort:sub(1, 64) or nil
MySQL.update.await([[
UPDATE police_asservat
SET status = 'eingelagert', lagerort = COALESCE(?, lagerort), verantwortlich = ?
WHERE id = ?
]], { lagerort, beamter.charakterId, asservatId })
Historie(asservatId, 'Zurückgenommen', beamter,
type(daten.notiz) == 'string' and daten.notiz:sub(1, 255) or nil)
Protokoll.Schreibe(beamter, 'asservat', 'Asservat zurückgenommen',
{ typ = 'asservat', id = asservatId }, { kennung = asservat[1].kennung })
return { ok = true }
end)
-- =====================================================================
-- VERNICHTEN
--
-- Endgültig und nur mit eigenem Recht. Der Datensatz bleibt bestehen —
-- vernichtet wird der Gegenstand, nicht seine Dokumentation.
-- =====================================================================
Kern.RegistriereAktion('asservat', 'vernichten', 'asservat.vernichten', function(quelle, beamter, daten)
local asservatId = tonumber(daten.id)
if not asservatId then return nil, 'Ungültige Kennung.' end
local grund = type(daten.grund) == 'string' and daten.grund:sub(1, 255) or ''
if #grund < 5 then
return nil, 'Die Vernichtung erfordert eine ausführliche Begründung.'
end
local zeuge = type(daten.zeuge) == 'string' and daten.zeuge:sub(1, 64) or ''
if #zeuge < 2 then
return nil, 'Bitte einen zweiten Beamten als Zeugen angeben.'
end
local asservat = MySQL.query.await(
'SELECT kennung, bezeichnung, status FROM police_asservat WHERE id = ?', { asservatId })
if not asservat or not asservat[1] then return nil, 'Asservat nicht gefunden.' end
if asservat[1].status == 'vernichtet' then
return nil, 'Dieses Asservat wurde bereits vernichtet.'
end
MySQL.update.await("UPDATE police_asservat SET status = 'vernichtet' WHERE id = ?", { asservatId })
Historie(asservatId, 'Vernichtet (Zeuge: ' .. zeuge .. ')', beamter, grund)
Protokoll.Schreibe(beamter, 'asservat', 'Asservat vernichtet',
{ typ = 'asservat', id = asservatId },
{ kennung = asservat[1].kennung, bezeichnung = asservat[1].bezeichnung, zeuge = zeuge },
grund)
Hinweise.Erstelle(nil, {
kategorie = 'beweis', prioritaet = 'normal',
titel = 'Asservat vernichtet',
text = ('%s (%s) durch %s, Zeuge: %s')
:format(asservat[1].kennung, asservat[1].bezeichnung, beamter.dienstnummer, zeuge),
zielTyp = 'asservat', zielId = asservatId,
})
return { ok = true }
end)
-- =====================================================================
-- LAGERORT ÄNDERN
-- =====================================================================
Kern.RegistriereAktion('asservat', 'umlagern', 'asservat.einlagern', function(quelle, beamter, daten)
local asservatId = tonumber(daten.id)
local lagerort = type(daten.lagerort) == 'string' and daten.lagerort:sub(1, 64) or ''
if not asservatId then return nil, 'Ungültige Kennung.' end
if #lagerort < 2 then return nil, 'Bitte einen Lagerort angeben.' end
local asservat = MySQL.query.await(
'SELECT kennung, lagerort, status FROM police_asservat WHERE id = ?', { asservatId })
if not asservat or not asservat[1] then return nil, 'Asservat nicht gefunden.' end
if asservat[1].status ~= 'eingelagert' then
return nil, 'Nur eingelagerte Asservate können umgelagert werden.'
end
MySQL.update.await('UPDATE police_asservat SET lagerort = ? WHERE id = ?',
{ lagerort, asservatId })
Historie(asservatId, ('Umgelagert: %s → %s'):format(asservat[1].lagerort, lagerort), beamter)
Protokoll.Schreibe(beamter, 'asservat', 'Asservat umgelagert',
{ typ = 'asservat', id = asservatId },
{ von = asservat[1].lagerort, nach = lagerort })
return { ok = true }
end)