-- ===================================================================== -- aether-police / Modul: Berichtswesen -- -- Ein Bericht durchläuft drei Stufen: Entwurf → Eingereicht → Freigegeben. -- Keine davon wird automatisch erreicht. Auch die Freigabe ist eine -- bewusste Handlung eines berechtigten Beamten und wird mit Namen -- festgehalten. Unterschriften sind verbindlich und nicht widerrufbar. -- ===================================================================== Kern.RegistriereModul({ id = 'bericht', name = 'Berichte', icon = '📄', reihenfolge = 8, recht = 'bericht.erstellen', beschreibung = 'Einsatzberichte verfassen und freigeben', }) -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Entschlüsselt ein JSON-Feld sicher zu einer Liste --- @param roh string|nil --- @return table local function ZuListe(roh) if not roh then return {} end local erfolg, liste = pcall(json.decode, roh) return (erfolg and type(liste) == 'table') and liste or {} end --- Lädt einen Bericht samt aufbereiteter Felder --- @param berichtId number --- @return table|nil local function LadeBericht(berichtId) local zeilen = MySQL.query.await([[ SELECT b.*, c.firstname, c.lastname, p.dienstnummer, e.nummer AS einsatz_nummer, e.titel AS einsatz_titel, a.aktenzeichen, a.titel AS akte_titel FROM police_bericht b LEFT JOIN aether_characters c ON c.id = b.erstellt_von LEFT JOIN police_personal p ON p.`character` = b.erstellt_von LEFT JOIN police_einsatz e ON e.id = b.einsatz LEFT JOIN police_akte a ON a.id = b.akte WHERE b.id = ? ]], { berichtId }) if not zeilen or not zeilen[1] then return nil end local bericht = zeilen[1] bericht.beteiligte = ZuListe(bericht.beteiligte) bericht.anlagen = ZuListe(bericht.anlagen) bericht.unterschriften = ZuListe(bericht.unterschriften) return bericht end --- Darf der Beamte diesen Bericht inhaltlich ändern? --- @param bericht table --- @param beamter table --- @return boolean local function DarfBearbeiten(bericht, beamter) -- Freigegebene Berichte sind gesperrt — auch für den Verfasser if bericht.status == 'freigegeben' then return false end if bericht.erstellt_von == beamter.charakterId then return true end -- Fremde Berichte darf nur bearbeiten, wer auch freigeben darf return Rechte.Hat(beamter, 'bericht.freigeben') end -- ===================================================================== -- ÜBERSICHT -- ===================================================================== Kern.RegistriereAktion('bericht', 'liste', nil, function(quelle, beamter, daten) local bedingungen, werte = { '1=1' }, {} if daten.status and daten.status ~= '' then bedingungen[#bedingungen + 1] = 'b.status = ?' werte[#werte + 1] = daten.status end if daten.nurEigene then bedingungen[#bedingungen + 1] = 'b.erstellt_von = ?' werte[#werte + 1] = beamter.charakterId end if type(daten.begriff) == 'string' and #daten.begriff >= 2 then bedingungen[#bedingungen + 1] = '(b.titel LIKE ? OR b.nummer LIKE ?)' local muster = '%' .. daten.begriff .. '%' werte[#werte + 1] = muster werte[#werte + 1] = muster end local berichte = MySQL.query.await(([[ SELECT b.id, b.nummer, b.titel, b.status, b.erstellt_am, b.erstellt_von, b.unterschriften, b.einsatz, b.akte, c.firstname, c.lastname, p.dienstnummer, e.nummer AS einsatz_nummer FROM police_bericht b LEFT JOIN aether_characters c ON c.id = b.erstellt_von LEFT JOIN police_personal p ON p.`character` = b.erstellt_von LEFT JOIN police_einsatz e ON e.id = b.einsatz WHERE %s ORDER BY CASE b.status WHEN 'entwurf' THEN 0 WHEN 'eingereicht' THEN 1 ELSE 2 END, b.id DESC LIMIT 80 ]]):format(table.concat(bedingungen, ' AND ')), werte) or {} for _, bericht in ipairs(berichte) do bericht.unterschriften = ZuListe(bericht.unterschriften) bericht.eigener = bericht.erstellt_von == beamter.charakterId end -- Zahlen für die Übersicht local offen = MySQL.scalar.await( "SELECT COUNT(*) FROM police_bericht WHERE status = 'entwurf' AND erstellt_von = ?", { beamter.charakterId }) or 0 local zurPruefung = MySQL.scalar.await( "SELECT COUNT(*) FROM police_bericht WHERE status = 'eingereicht'") or 0 return { berichte = berichte, eigeneOffen = offen, zurPruefung = zurPruefung, rechte = { erstellen = Rechte.Hat(beamter, 'bericht.erstellen'), freigeben = Rechte.Hat(beamter, 'bericht.freigeben'), }, } end) Kern.RegistriereAktion('bericht', 'oeffnen', nil, function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end -- Habe ich bereits unterschrieben? local habeUnterschrieben = false for _, unterschrift in ipairs(bericht.unterschriften) do if unterschrift.charakterId == beamter.charakterId then habeUnterschrieben = true break end end return { bericht = bericht, habeUnterschrieben = habeUnterschrieben, rechte = { bearbeiten = DarfBearbeiten(bericht, beamter), freigeben = Rechte.Hat(beamter, 'bericht.freigeben'), }, } end) -- ===================================================================== -- ANLEGEN -- ===================================================================== Kern.RegistriereAktion('bericht', 'erstellen', 'bericht.erstellen', function(quelle, beamter, daten) local titel = type(daten.titel) == 'string' and daten.titel:gsub('^%s+', ''):gsub('%s+$', '') or '' if #titel < 3 or #titel > 160 then return nil, 'Der Titel muss 3 bis 160 Zeichen lang sein.' end -- Bezüge prüfen, falls angegeben local einsatzId = tonumber(daten.einsatz) if einsatzId then local vorhanden = MySQL.scalar.await('SELECT id FROM police_einsatz WHERE id = ?', { einsatzId }) if not vorhanden then return nil, 'Der angegebene Einsatz existiert nicht.' end end 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 nummer = Kern.ErzeugeKennung('BER', 'police_bericht', 'nummer') -- Der Verfasser ist von Beginn an als beteiligter Beamter geführt local beteiligte = { { typ = 'beamter', charakterId = beamter.charakterId, name = beamter.name, dienstnummer = beamter.dienstnummer, rolle = 'Verfasser', } } local berichtId = MySQL.insert.await([[ INSERT INTO police_bericht (nummer, einsatz, akte, titel, beschreibung, massnahmen, beteiligte, anlagen, unterschriften, status, erstellt_von) VALUES (?, ?, ?, ?, ?, ?, ?, '[]', '[]', 'entwurf', ?) ]], { nummer, einsatzId, akteId, titel, type(daten.beschreibung) == 'string' and daten.beschreibung or nil, type(daten.massnahmen) == 'string' and daten.massnahmen or nil, json.encode(beteiligte), beamter.charakterId, }) Protokoll.Schreibe(beamter, 'bericht', 'Bericht angelegt', { typ = 'bericht', id = berichtId }, { nummer = nummer }) Kern.Loese('bericht.erstellt', berichtId, beamter) return { id = berichtId, nummer = nummer } end) -- ===================================================================== -- BEARBEITEN -- -- Freigegebene Berichte sind gesperrt — auch für den Verfasser. -- ===================================================================== Kern.RegistriereAktion('bericht', 'speichern', 'bericht.erstellen', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if not DarfBearbeiten(bericht, beamter) then return nil, bericht.status == 'freigegeben' and 'Freigegebene Berichte können nicht mehr geändert werden.' or 'Du kannst nur eigene Berichte bearbeiten.' end -- Liegen bereits Unterschriften vor, ist eine Änderung heikel if #bericht.unterschriften > 0 then local grund = type(daten.grund) == 'string' and daten.grund or '' if #grund < 5 then return nil, 'Der Bericht wurde bereits unterschrieben. Eine Änderung erfordert eine Begründung.' end end MySQL.update.await([[ UPDATE police_bericht SET titel = ?, beschreibung = ?, massnahmen = ? WHERE id = ? ]], { type(daten.titel) == 'string' and daten.titel:sub(1, 160) or bericht.titel, type(daten.beschreibung) == 'string' and daten.beschreibung or nil, type(daten.massnahmen) == 'string' and daten.massnahmen or nil, berichtId, }) Protokoll.Schreibe(beamter, 'bericht', 'Bericht bearbeitet', { typ = 'bericht', id = berichtId }, { unterschriften = #bericht.unterschriften }, type(daten.grund) == 'string' and daten.grund or nil) return { ok = true } end) -- --------------------------------------------------------------------- -- Beteiligte -- --------------------------------------------------------------------- Kern.RegistriereAktion('bericht', 'beteiligtHinzu', 'bericht.erstellen', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if not DarfBearbeiten(bericht, beamter) then return nil, 'Keine Berechtigung.' end local typ = (daten.typ == 'beamter') and 'beamter' or 'person' local name = type(daten.name) == 'string' and daten.name:gsub('^%s+', ''):gsub('%s+$', '') or '' if #name < 2 then return nil, 'Bitte einen Namen angeben.' end local charakterId = tonumber(daten.charakterId) -- Doppelte Einträge vermeiden for _, eintrag in ipairs(bericht.beteiligte) do if charakterId and eintrag.charakterId == charakterId then return nil, 'Diese Person ist bereits aufgeführt.' end end table.insert(bericht.beteiligte, { typ = typ, charakterId = charakterId, name = name:sub(1, 64), rolle = type(daten.rolle) == 'string' and daten.rolle:sub(1, 48) or nil, dienstnummer = type(daten.dienstnummer) == 'string' and daten.dienstnummer:sub(1, 16) or nil, }) MySQL.update.await('UPDATE police_bericht SET beteiligte = ? WHERE id = ?', { json.encode(bericht.beteiligte), berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Beteiligten ergänzt', { typ = 'bericht', id = berichtId }, { name = name, rolle = daten.rolle }) return { ok = true } end) Kern.RegistriereAktion('bericht', 'beteiligtEntfernen', 'bericht.erstellen', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) local index = tonumber(daten.index) if not berichtId or not index then return nil, 'Unvollständige Angaben.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if not DarfBearbeiten(bericht, beamter) then return nil, 'Keine Berechtigung.' end local eintrag = bericht.beteiligte[index] if not eintrag then return nil, 'Eintrag nicht gefunden.' end -- Der Verfasser bleibt immer aufgeführt if eintrag.charakterId == bericht.erstellt_von then return nil, 'Der Verfasser kann nicht entfernt werden.' end table.remove(bericht.beteiligte, index) MySQL.update.await('UPDATE police_bericht SET beteiligte = ? WHERE id = ?', { json.encode(bericht.beteiligte), berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Beteiligten entfernt', { typ = 'bericht', id = berichtId }, { name = eintrag.name }) return { ok = true } end) -- --------------------------------------------------------------------- -- Anlagen (Dateien und Beweisbezüge) -- --------------------------------------------------------------------- Kern.RegistriereAktion('bericht', 'anlageHinzu', 'bericht.erstellen', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if not DarfBearbeiten(bericht, beamter) then return nil, 'Keine Berechtigung.' end local eintrag if daten.typ == 'beweis' then local beweisId = tonumber(daten.beweisId) if not beweisId then return nil, 'Kein Beweismittel gewählt.' 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, 'Beweismittel nicht gefunden.' end for _, vorhanden in ipairs(bericht.anlagen) do if vorhanden.typ == 'beweis' and vorhanden.beweisId == beweisId then return nil, 'Dieses Beweismittel ist bereits aufgeführt.' end end eintrag = { typ = 'beweis', beweisId = beweisId, kennung = beweis[1].kennung, titel = beweis[1].bezeichnung, art = beweis[1].art, } else local titel = type(daten.titel) == 'string' and daten.titel:gsub('^%s+', ''):gsub('%s+$', '') or '' if #titel < 2 then return nil, 'Bitte eine Bezeichnung angeben.' end eintrag = { typ = 'datei', titel = titel:sub(1, 120), adresse = type(daten.adresse) == 'string' and daten.adresse:sub(1, 2000) or nil, } end table.insert(bericht.anlagen, eintrag) MySQL.update.await('UPDATE police_bericht SET anlagen = ? WHERE id = ?', { json.encode(bericht.anlagen), berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Anlage ergänzt', { typ = 'bericht', id = berichtId }, eintrag) return { ok = true } end) Kern.RegistriereAktion('bericht', 'anlageEntfernen', 'bericht.erstellen', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) local index = tonumber(daten.index) if not berichtId or not index then return nil, 'Unvollständige Angaben.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if not DarfBearbeiten(bericht, beamter) then return nil, 'Keine Berechtigung.' end local eintrag = bericht.anlagen[index] if not eintrag then return nil, 'Anlage nicht gefunden.' end table.remove(bericht.anlagen, index) MySQL.update.await('UPDATE police_bericht SET anlagen = ? WHERE id = ?', { json.encode(bericht.anlagen), berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Anlage entfernt', { typ = 'bericht', id = berichtId }, { titel = eintrag.titel }) return { ok = true } end) -- ===================================================================== -- UNTERSCHREIBEN -- -- Eine Unterschrift ist verbindlich und wird nicht zurückgenommen. -- ===================================================================== Kern.RegistriereAktion('bericht', 'unterschreiben', nil, function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if bericht.status == 'freigegeben' then return nil, 'Der Bericht ist bereits freigegeben.' end for _, unterschrift in ipairs(bericht.unterschriften) do if unterschrift.charakterId == beamter.charakterId then return nil, 'Du hast bereits unterschrieben.' end end table.insert(bericht.unterschriften, { charakterId = beamter.charakterId, name = beamter.name, dienstnummer = beamter.dienstnummer, rolle = beamter.rolleName, am = os.date('%Y-%m-%d %H:%M:%S'), }) MySQL.update.await('UPDATE police_bericht SET unterschriften = ? WHERE id = ?', { json.encode(bericht.unterschriften), berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Bericht unterschrieben', { typ = 'bericht', id = berichtId }, { nummer = bericht.nummer }) return { ok = true } end) -- ===================================================================== -- EINREICHEN UND FREIGEBEN -- ===================================================================== Kern.RegistriereAktion('bericht', 'einreichen', 'bericht.erstellen', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if bericht.status ~= 'entwurf' then return nil, 'Nur Entwürfe können eingereicht werden.' end if bericht.erstellt_von ~= beamter.charakterId and not Rechte.Hat(beamter, 'bericht.freigeben') then return nil, 'Nur der Verfasser kann seinen Bericht einreichen.' end if not bericht.beschreibung or #bericht.beschreibung < 20 then return nil, 'Der Bericht enthält noch keine ausreichende Beschreibung.' end if #bericht.unterschriften == 0 then return nil, 'Bitte unterschreibe den Bericht, bevor du ihn einreichst.' end MySQL.update.await("UPDATE police_bericht SET status = 'eingereicht' WHERE id = ?", { berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Bericht eingereicht', { typ = 'bericht', id = berichtId }, { nummer = bericht.nummer }) -- Die Führung darüber informieren, dass eine Prüfung ansteht Hinweise.Erstelle(nil, { kategorie = 'bericht', prioritaet = 'normal', titel = 'Bericht zur Prüfung eingereicht', text = ('%s „%s" von %s wartet auf Freigabe.') :format(bericht.nummer, bericht.titel, beamter.dienstnummer), zielTyp = 'bericht', zielId = berichtId, }) return { ok = true } end) Kern.RegistriereAktion('bericht', 'freigeben', 'bericht.freigeben', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId then return nil, 'Ungültige Kennung.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if bericht.status == 'freigegeben' then return nil, 'Der Bericht ist bereits freigegeben.' end if bericht.status ~= 'eingereicht' then return nil, 'Nur eingereichte Berichte können freigegeben werden.' end -- Mit der Freigabe unterschreibt der Freigebende zugleich local schonDrin = false for _, unterschrift in ipairs(bericht.unterschriften) do if unterschrift.charakterId == beamter.charakterId then schonDrin = true break end end if not schonDrin then table.insert(bericht.unterschriften, { charakterId = beamter.charakterId, name = beamter.name, dienstnummer = beamter.dienstnummer, rolle = beamter.rolleName, am = os.date('%Y-%m-%d %H:%M:%S'), freigabe = true, }) end MySQL.update.await( "UPDATE police_bericht SET status = 'freigegeben', unterschriften = ? WHERE id = ?", { json.encode(bericht.unterschriften), berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Bericht freigegeben', { typ = 'bericht', id = berichtId }, { nummer = bericht.nummer }, daten.vermerk) Hinweise.Erstelle({ charakterId = bericht.erstellt_von }, { kategorie = 'bericht', prioritaet = 'normal', titel = 'Bericht freigegeben', text = ('%s wurde von %s freigegeben.'):format(bericht.nummer, beamter.dienstnummer), zielTyp = 'bericht', zielId = berichtId, }) Kern.Loese('bericht.freigegeben', berichtId, beamter) return { ok = true } end) --- Zurück in den Entwurf (Nachbesserung anfordern) Kern.RegistriereAktion('bericht', 'zurueckweisen', 'bericht.freigeben', function(quelle, beamter, daten) local berichtId = tonumber(daten.id) if not berichtId 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, 'Bitte gib an, was nachgebessert werden soll.' end local bericht = LadeBericht(berichtId) if not bericht then return nil, 'Bericht nicht gefunden.' end if bericht.status ~= 'eingereicht' then return nil, 'Nur eingereichte Berichte können zurückgewiesen werden.' end MySQL.update.await("UPDATE police_bericht SET status = 'entwurf' WHERE id = ?", { berichtId }) Protokoll.Schreibe(beamter, 'bericht', 'Bericht zurückgewiesen', { typ = 'bericht', id = berichtId }, { nummer = bericht.nummer }, grund) Hinweise.Erstelle({ charakterId = bericht.erstellt_von }, { kategorie = 'bericht', prioritaet = 'hoch', titel = 'Bericht zur Nachbesserung', text = ('%s wurde zurückgewiesen: %s'):format(bericht.nummer, grund), zielTyp = 'bericht', zielId = berichtId, }) return { ok = true } end) -- ===================================================================== -- HILFSABFRAGEN FÜR DIE OBERFLÄCHE -- ===================================================================== --- Offene Einsätze und Akten zur Auswahl beim Anlegen Kern.RegistriereAktion('bericht', 'bezuege', nil, function(quelle, beamter, daten) local einsaetze = MySQL.query.await([[ SELECT id, nummer, titel FROM police_einsatz ORDER BY id DESC LIMIT 30 ]]) or {} return { einsaetze = einsaetze } end) --- Beweismittel zur Auswahl als Anlage Kern.RegistriereAktion('bericht', 'beweisSuche', 'bericht.erstellen', function(quelle, beamter, daten) local begriff = type(daten.begriff) == 'string' and daten.begriff or '' if #begriff < 2 then return { beweise = {} } end local muster = '%' .. begriff .. '%' local beweise = MySQL.query.await([[ SELECT id, kennung, art, bezeichnung, fundort FROM police_beweis WHERE kennung LIKE ? OR bezeichnung LIKE ? ORDER BY id DESC LIMIT 20 ]], { muster, muster }) or {} return { beweise = beweise } end)