- 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>
306 lines
12 KiB
Lua
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)
|