- 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>
321 lines
13 KiB
Lua
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)
|