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

306 lines
12 KiB
Lua

-- =====================================================================
-- aether-police / Modul: Beweisverwaltung
--
-- Es gibt KEINE automatische Spurenerkennung. Jeder Beweis wird von
-- einem Beamten erfasst, beschrieben und zugeordnet. Das System
-- vergibt lediglich eine Kennung und hält die Verknüpfungen zusammen.
-- =====================================================================
Kern.RegistriereModul({
id = 'beweis', name = 'Beweise', icon = '🔬',
reihenfolge = 6, recht = 'beweis.erfassen',
beschreibung = 'Beweismittel erfassen und zuordnen',
})
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
--- Prüft, ob eine Beweisart bekannt ist
local function ArtGueltig(art)
for _, eintrag in ipairs(Polizei.Config.Beweisarten) do
if eintrag.id == art then return true end
end
return false
end
--- Lädt die Verknüpfungen eines Beweises (welche Akten nutzen ihn?)
--- @param beweisId number
--- @return table
local function LadeVerknuepfungen(beweisId)
return MySQL.query.await([[
SELECT v.id, a.id AS akte, a.aktenzeichen, a.titel, a.typ, a.status
FROM police_verknuepfung v
JOIN police_akte a ON a.id = v.akte
WHERE v.typ = 'beweis' AND v.ziel_id = ?
ORDER BY a.id DESC
]], { tostring(beweisId) }) or {}
end
-- =====================================================================
-- ÜBERSICHT
-- =====================================================================
Kern.RegistriereAktion('beweis', 'liste', nil, function(quelle, beamter, daten)
local bedingungen, werte = { '1=1' }, {}
if daten.art and daten.art ~= '' then
bedingungen[#bedingungen + 1] = 'b.art = ?'
werte[#werte + 1] = daten.art
end
if type(daten.begriff) == 'string' and #daten.begriff >= 2 then
bedingungen[#bedingungen + 1] =
'(b.bezeichnung LIKE ? OR b.kennung LIKE ? OR b.fundort LIKE ?)'
local muster = '%' .. daten.begriff .. '%'
werte[#werte + 1] = muster
werte[#werte + 1] = muster
werte[#werte + 1] = muster
end
if daten.akte then
bedingungen[#bedingungen + 1] = 'b.akte = ?'
werte[#werte + 1] = tonumber(daten.akte)
end
local beweise = MySQL.query.await(([[
SELECT b.id, b.kennung, b.art, b.bezeichnung, b.beschreibung, b.fundort,
b.gefunden_am, b.datei, b.akte, b.erfasst_am,
c.firstname, c.lastname, p.dienstnummer,
a.aktenzeichen
FROM police_beweis b
LEFT JOIN aether_characters c ON c.id = b.erfasst_von
LEFT JOIN police_personal p ON p.`character` = b.erfasst_von
LEFT JOIN police_akte a ON a.id = b.akte
WHERE %s
ORDER BY b.id DESC LIMIT 80
]]):format(table.concat(bedingungen, ' AND ')), werte) or {}
return {
beweise = beweise,
arten = Polizei.Config.Beweisarten,
rechte = {
erfassen = Rechte.Hat(beamter, 'beweis.erfassen'),
loeschen = Rechte.Hat(beamter, 'beweis.loeschen'),
akte = Rechte.Hat(beamter, 'akte.bearbeiten'),
},
}
end)
Kern.RegistriereAktion('beweis', 'oeffnen', nil, function(quelle, beamter, daten)
local beweisId = tonumber(daten.id)
if not beweisId then return nil, 'Ungültige Kennung.' end
local beweis = MySQL.query.await([[
SELECT b.*, c.firstname, c.lastname, p.dienstnummer, a.aktenzeichen, a.titel AS akte_titel
FROM police_beweis b
LEFT JOIN aether_characters c ON c.id = b.erfasst_von
LEFT JOIN police_personal p ON p.`character` = b.erfasst_von
LEFT JOIN police_akte a ON a.id = b.akte
WHERE b.id = ?
]], { beweisId })
if not beweis or not beweis[1] then return nil, 'Beweis nicht gefunden.' end
-- Asservate, die zu diesem Beweis gehören
local asservate = MySQL.query.await([[
SELECT id, kennung, kategorie, bezeichnung, status, lagerort
FROM police_asservat WHERE beweis = ?
]], { beweisId }) or {}
return {
beweis = beweis[1],
akten = LadeVerknuepfungen(beweisId),
asservate = asservate,
historie = Protokoll.Lies({ zielTyp = 'beweis', zielId = beweisId, limit = 30 }),
rechte = {
loeschen = Rechte.Hat(beamter, 'beweis.loeschen'),
akte = Rechte.Hat(beamter, 'akte.bearbeiten'),
},
}
end)
-- =====================================================================
-- ERFASSEN
-- =====================================================================
Kern.RegistriereAktion('beweis', 'erfassen', 'beweis.erfassen', function(quelle, beamter, daten)
if not ArtGueltig(daten.art) then return nil, 'Unbekannte Beweisart.' 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 fundort = type(daten.fundort) == 'string'
and daten.fundort:gsub('^%s+', ''):gsub('%s+$', '') or ''
if #fundort < 2 then
return nil, 'Bitte einen Fundort angeben — er gehört zur Beweiskette.'
end
-- Akte prüfen, falls angegeben
local akteId = tonumber(daten.akte)
if akteId then
local vorhanden = MySQL.scalar.await('SELECT id FROM police_akte WHERE id = ?', { akteId })
if not vorhanden then return nil, 'Die angegebene Akte existiert nicht.' end
end
local kennung = Kern.ErzeugeKennung('BM', 'police_beweis', 'kennung')
local beweisId = MySQL.insert.await([[
INSERT INTO police_beweis
(kennung, akte, art, bezeichnung, beschreibung, fundort, datei, erfasst_von)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
]], {
kennung, akteId, daten.art, bezeichnung,
type(daten.beschreibung) == 'string' and daten.beschreibung or nil,
fundort,
type(daten.datei) == 'string' and daten.datei:sub(1, 2000) or nil,
beamter.charakterId,
})
-- Ist eine Akte angegeben, gleich verknüpfen
if akteId then
MySQL.insert('INSERT INTO police_verknuepfung (akte, typ, ziel_id) VALUES (?, ?, ?)',
{ akteId, 'beweis', tostring(beweisId) })
end
Protokoll.Schreibe(beamter, 'beweis', 'Beweis erfasst',
{ typ = 'beweis', id = beweisId },
{ kennung = kennung, art = daten.art, fundort = fundort })
Kern.Loese('beweis.erfasst', beweisId, beamter)
return { id = beweisId, kennung = kennung }
end)
-- =====================================================================
-- BEARBEITEN
-- =====================================================================
Kern.RegistriereAktion('beweis', 'bearbeiten', 'beweis.erfassen', function(quelle, beamter, daten)
local beweisId = tonumber(daten.id)
if not beweisId then return nil, 'Ungültige Kennung.' end
local beweis = MySQL.query.await(
'SELECT erfasst_von, kennung FROM police_beweis WHERE id = ?', { beweisId })
if not beweis or not beweis[1] then return nil, 'Beweis nicht gefunden.' end
-- Fremde Beweise darf nur ändern, wer auch löschen darf
if beweis[1].erfasst_von ~= beamter.charakterId
and not Rechte.Hat(beamter, 'beweis.loeschen') then
return nil, 'Du kannst nur eigene Beweise bearbeiten.'
end
local grund = type(daten.grund) == 'string' and daten.grund:sub(1, 255) or ''
if #grund < 3 then
return nil, 'Änderungen an Beweismitteln erfordern einen Grund.'
end
MySQL.update.await([[
UPDATE police_beweis
SET bezeichnung = ?, beschreibung = ?, fundort = ?, datei = ?
WHERE id = ?
]], {
type(daten.bezeichnung) == 'string' and daten.bezeichnung:sub(1, 160) or nil,
type(daten.beschreibung) == 'string' and daten.beschreibung or nil,
type(daten.fundort) == 'string' and daten.fundort:sub(1, 160) or nil,
type(daten.datei) == 'string' and daten.datei:sub(1, 2000) or nil,
beweisId,
})
Protokoll.Schreibe(beamter, 'beweis', 'Beweis geändert',
{ typ = 'beweis', id = beweisId }, { kennung = beweis[1].kennung }, grund)
return { ok = true }
end)
-- =====================================================================
-- ENTFERNEN
--
-- Beweise verschwinden nicht still — das Entfernen verlangt ein
-- eigenes Recht und eine Begründung und bleibt im Protokoll sichtbar.
-- =====================================================================
Kern.RegistriereAktion('beweis', 'entfernen', 'beweis.loeschen', function(quelle, beamter, daten)
local beweisId = tonumber(daten.id)
if not beweisId 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, 'Das Entfernen eines Beweismittels erfordert eine ausführliche Begründung.'
end
local beweis = MySQL.query.await(
'SELECT kennung, bezeichnung, art FROM police_beweis WHERE id = ?', { beweisId })
if not beweis or not beweis[1] then return nil, 'Beweis nicht gefunden.' end
-- Verknüpfte Asservate müssen zuerst geklärt werden
local asservate = MySQL.scalar.await(
"SELECT COUNT(*) FROM police_asservat WHERE beweis = ? AND status = 'eingelagert'",
{ beweisId }) or 0
if asservate > 0 then
return nil, ('Zu diesem Beweis sind noch %d Asservate eingelagert. Bitte zuerst klären.')
:format(asservate)
end
MySQL.update.await('DELETE FROM police_verknuepfung WHERE typ = ? AND ziel_id = ?',
{ 'beweis', tostring(beweisId) })
MySQL.update.await('DELETE FROM police_beweis WHERE id = ?', { beweisId })
Protokoll.Schreibe(beamter, 'beweis', 'Beweis entfernt',
{ typ = 'beweis', id = beweisId },
{ kennung = beweis[1].kennung, bezeichnung = beweis[1].bezeichnung }, grund)
Hinweise.Erstelle(nil, {
kategorie = 'beweis', prioritaet = 'hoch',
titel = 'Beweismittel entfernt',
text = ('%s (%s) wurde von %s entfernt: %s')
:format(beweis[1].kennung, beweis[1].bezeichnung, beamter.dienstnummer, grund),
})
return { ok = true }
end)
-- =====================================================================
-- MIT AKTE VERKNÜPFEN
-- =====================================================================
Kern.RegistriereAktion('beweis', 'akteVerknuepfen', 'akte.bearbeiten', function(quelle, beamter, daten)
local beweisId = tonumber(daten.beweisId)
local akteId = tonumber(daten.akteId)
if not beweisId or not akteId then return nil, 'Unvollständige Angaben.' end
local akte = MySQL.query.await(
'SELECT aktenzeichen FROM police_akte WHERE id = ?', { akteId })
if not akte or not akte[1] then return nil, 'Akte nicht gefunden.' end
local vorhanden = MySQL.scalar.await(
'SELECT id FROM police_verknuepfung WHERE akte = ? AND typ = ? AND ziel_id = ?',
{ akteId, 'beweis', tostring(beweisId) })
if vorhanden then return nil, 'Diese Verknüpfung besteht bereits.' end
MySQL.insert.await('INSERT INTO police_verknuepfung (akte, typ, ziel_id) VALUES (?, ?, ?)',
{ akteId, 'beweis', tostring(beweisId) })
Protokoll.Schreibe(beamter, 'beweis', 'Mit Akte verknüpft',
{ typ = 'beweis', id = beweisId }, { akte = akte[1].aktenzeichen })
return { ok = true }
end)
--- Aktensuche für die Verknüpfungsauswahl
Kern.RegistriereAktion('beweis', 'aktenSuche', 'akte.lesen', function(quelle, beamter, daten)
local begriff = type(daten.begriff) == 'string' and daten.begriff or ''
if #begriff < 2 then return { akten = {} } end
local muster = '%' .. begriff .. '%'
local akten = MySQL.query.await([[
SELECT id, aktenzeichen, typ, titel, status
FROM police_akte
WHERE aktenzeichen LIKE ? OR titel LIKE ?
ORDER BY id DESC LIMIT 20
]], { muster, muster }) or {}
return { akten = akten }
end)