-- ===================================================================== -- aether-police / Modul: Administration -- -- Drei getrennte Bereiche mit je eigenem Recht: -- * Module (admin.module) — Bausteine an- und abschalten -- * Katalog (katalog.bearbeiten) — Delikte und ihre Vorschlagswerte -- * Protokoll (protokoll.lesen) — wer hat wann was getan -- -- Auch hier gilt: der Katalog liefert VORSCHLÄGE. Was hier eingetragen -- wird, ist nie eine Strafe, sondern ein Richtwert, von dem ein Beamter -- jederzeit begründet abweichen darf. -- -- Nichts wird gelöscht. Delikte werden stillgelegt, damit alte Anzeigen -- nachvollziehbar bleiben. -- ===================================================================== Kern.RegistriereModul({ id = 'admin', name = 'Administration', icon = '⚙', reihenfolge = 14, -- Ein Recht aus dieser Liste genügt zum Öffnen; die einzelnen -- Bereiche prüfen danach noch einmal getrennt. recht = { 'admin.module', 'katalog.bearbeiten', 'protokoll.lesen' }, beschreibung = 'Module, Strafenkatalog und Protokoll', }) -- Module, die sich nicht abschalten lassen — sonst sperrt man sich aus local UNVERZICHTBAR = { admin = true, dashboard = true } -- Schlüssel, unter dem die abgeschalteten Module gespeichert werden local SCHLUESSEL_AUS = 'module_abgeschaltet' -- --------------------------------------------------------------------- -- Einstellungen lesen und schreiben -- --------------------------------------------------------------------- --- Liest einen Wert aus police_einstellung --- @param schluessel string Kennung --- @return any|nil local function LeseEinstellung(schluessel) local roh = MySQL.scalar.await( 'SELECT wert FROM police_einstellung WHERE schluessel = ?', { schluessel }) if not roh then return nil end local erfolg, wert = pcall(json.decode, roh) return erfolg and wert or nil end --- Schreibt einen Wert nach police_einstellung --- @param schluessel string Kennung --- @param wert any Beliebiger Wert local function SchreibeEinstellung(schluessel, wert) MySQL.query.await([[ INSERT INTO police_einstellung (schluessel, wert) VALUES (?, ?) ON DUPLICATE KEY UPDATE wert = VALUES(wert) ]], { schluessel, json.encode(wert) }) end -- --------------------------------------------------------------------- -- Beim Start: gespeicherte Modulzustände wiederherstellen -- Läuft erst, wenn alle Moduldateien geladen und registriert sind. -- --------------------------------------------------------------------- CreateThread(function() Wait(0) local abgeschaltet = LeseEinstellung(SCHLUESSEL_AUS) if type(abgeschaltet) ~= 'table' then return end for _, modulId in ipairs(abgeschaltet) do if not UNVERZICHTBAR[modulId] and Kern.KenntModul(modulId) then Kern.SetzeModulAktiv(modulId, false) Aether.Debug(('Polizei-Modul abgeschaltet (gespeichert): %s'):format(modulId)) end end end) -- ===================================================================== -- MODULE -- ===================================================================== --- Welche Bereiche darf dieser Beamte sehen? Steuert die Reiter der Oberfläche. Kern.RegistriereAktion('admin', 'bereiche', nil, function(quelle, beamter) return { module = Rechte.Hat(beamter, 'admin.module'), katalog = Rechte.Hat(beamter, 'katalog.bearbeiten'), protokoll = Rechte.Hat(beamter, 'protokoll.lesen'), } end) Kern.RegistriereAktion('admin', 'module', 'admin.module', function(quelle, beamter) local module = Kern.AlleModule() for _, modul in ipairs(module) do modul.unverzichtbar = UNVERZICHTBAR[modul.id] == true -- Welches Recht öffnet dieses Modul, und wie heißt es im Klartext? modul.rechtName = nil if modul.recht then for _, gruppe in ipairs(Polizei.Rechte) do for _, recht in ipairs(gruppe.eintraege) do if recht.id == modul.recht then modul.rechtName = recht.name end end end end end -- Werte aus config.lua zur Ansicht — sie leben in der Datei, nicht in der DB local konfiguration = { { name = 'Behörde', wert = Polizei.Config.Behoerde }, { name = 'Kürzel', wert = Polizei.Config.Kuerzel }, { name = 'Terminals', wert = tostring(#Polizei.Config.Terminals) }, { name = 'Abteilungen', wert = tostring(#Polizei.Config.Abteilungen) }, { name = 'Rollen', wert = tostring(#Polizei.Rollen) }, { name = 'Rechte', wert = tostring(#Polizei.AlleRechte()) }, { name = 'Fristprüfung alle', wert = (Polizei.Config.Erinnerungen.pruefIntervall / 1000) .. ' Sekunden' }, } return { module = module, konfiguration = konfiguration, beamteImDienst = (function() local anzahl = 0 for _ in pairs(Dienst.AlleImDienst()) do anzahl = anzahl + 1 end return anzahl end)(), } end) Kern.RegistriereAktion('admin', 'modulSchalten', 'admin.module', function(quelle, beamter, daten) local modulId = tostring(daten.id or '') local aktiv = daten.aktiv == true if not Kern.KenntModul(modulId) then return nil, 'Unbekanntes Modul.' end if UNVERZICHTBAR[modulId] then return nil, 'Dieses Modul lässt sich nicht abschalten.' end Kern.SetzeModulAktiv(modulId, aktiv) -- Zustand merken, damit er einen Neustart übersteht local abgeschaltet, name = {}, modulId for _, modul in ipairs(Kern.AlleModule()) do if not modul.aktiv then abgeschaltet[#abgeschaltet + 1] = modul.id end if modul.id == modulId then name = modul.name end end SchreibeEinstellung(SCHLUESSEL_AUS, abgeschaltet) Protokoll.Schreibe(beamter, 'admin', aktiv and 'Modul eingeschaltet' or 'Modul abgeschaltet', { typ = 'modul', id = modulId }, nil, daten.grund) -- Alle im Dienst darüber informieren. Die Seitenleiste aktualisiert sich -- beim nächsten Öffnen des Tablets — laufende Arbeit wird nicht unterbrochen. Hinweise.Erstelle(nil, { kategorie = 'system', prioritaet = 'niedrig', titel = aktiv and 'Modul eingeschaltet' or 'Modul abgeschaltet', text = ('„%s" — beim nächsten Öffnen des Tablets sichtbar.'):format(name), }) Kern.Loese('admin.modulGeschaltet', modulId, aktiv) return { ok = true } end) -- ===================================================================== -- STRAFENKATALOG -- -- Die Werte hier sind Vorschläge. Sie werden nie automatisch verhängt. -- ===================================================================== Kern.RegistriereAktion('admin', 'katalog', 'katalog.bearbeiten', function(quelle, beamter, daten) local bedingungen, werte = { '1=1' }, {} if not daten.stillgelegt then bedingungen[#bedingungen + 1] = 'aktiv = 1' end if daten.kategorie and daten.kategorie ~= '' then bedingungen[#bedingungen + 1] = 'kategorie = ?' werte[#werte + 1] = daten.kategorie end if type(daten.begriff) == 'string' and #daten.begriff >= 2 then bedingungen[#bedingungen + 1] = '(kennung LIKE ? OR bezeichnung LIKE ? OR paragraph LIKE ?)' local muster = '%' .. daten.begriff .. '%' werte[#werte + 1] = muster werte[#werte + 1] = muster werte[#werte + 1] = muster end local delikte = MySQL.query.await(([[ SELECT id, kennung, kategorie, bezeichnung, paragraph, haft, geld, hinweis, aktiv, sortierung FROM police_katalog WHERE %s ORDER BY kategorie, sortierung, kennung LIMIT 500 ]]):format(table.concat(bedingungen, ' AND ')), werte) or {} for _, delikt in ipairs(delikte) do delikt.aktiv = delikt.aktiv == 1 end local kategorien = MySQL.query.await( 'SELECT DISTINCT kategorie FROM police_katalog ORDER BY kategorie') or {} return { delikte = delikte, kategorien = kategorien, stillgelegt = daten.stillgelegt == true, gesamt = MySQL.scalar.await('SELECT COUNT(*) FROM police_katalog WHERE aktiv = 1') or 0, } end) Kern.RegistriereAktion('admin', 'katalogSpeichern', 'katalog.bearbeiten', function(quelle, beamter, daten) local kennung = type(daten.kennung) == 'string' and daten.kennung:gsub('%s', ''):upper() or '' local bezeichnung = type(daten.bezeichnung) == 'string' and daten.bezeichnung:sub(1, 160) or '' local kategorie = type(daten.kategorie) == 'string' and daten.kategorie:sub(1, 48) or '' if #kennung < 2 then return nil, 'Bitte eine Kennung angeben.' end if #bezeichnung < 3 then return nil, 'Bitte eine Bezeichnung angeben.' end if #kategorie < 2 then return nil, 'Bitte eine Kategorie angeben.' end local haft = math.max(0, math.floor(tonumber(daten.haft) or 0)) local geld = math.max(0, math.floor(tonumber(daten.geld) or 0)) if haft > 100000 or geld > 10000000 then return nil, 'Der Vorschlagswert ist unplausibel hoch.' end local id = tonumber(daten.id) local hinweis = type(daten.hinweis) == 'string' and daten.hinweis:sub(1, 255) or nil local sortierung = math.floor(tonumber(daten.sortierung) or 0) -- Kennung darf nur einmal vergeben sein local belegt = MySQL.scalar.await( 'SELECT id FROM police_katalog WHERE kennung = ? AND (? IS NULL OR id <> ?)', { kennung, id, id }) if belegt then return nil, 'Diese Kennung ist bereits vergeben.' end if id then local alt = MySQL.query.await( 'SELECT kennung, bezeichnung, haft, geld FROM police_katalog WHERE id = ?', { id }) if not alt or not alt[1] then return nil, 'Delikt nicht gefunden.' end MySQL.update.await([[ UPDATE police_katalog SET kennung = ?, kategorie = ?, bezeichnung = ?, paragraph = ?, haft = ?, geld = ?, hinweis = ?, sortierung = ? WHERE id = ? ]], { kennung, kategorie, bezeichnung, daten.paragraph, haft, geld, hinweis, sortierung, id }) Protokoll.Schreibe(beamter, 'admin', 'Delikt geändert', { typ = 'katalog', id = id }, { kennung = kennung, haftAlt = alt[1].haft, haftNeu = haft, geldAlt = alt[1].geld, geldNeu = geld }) else id = MySQL.insert.await([[ INSERT INTO police_katalog (kennung, kategorie, bezeichnung, paragraph, haft, geld, hinweis, sortierung) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ]], { kennung, kategorie, bezeichnung, daten.paragraph, haft, geld, hinweis, sortierung }) Protokoll.Schreibe(beamter, 'admin', 'Delikt angelegt', { typ = 'katalog', id = id }, { kennung = kennung, haft = haft, geld = geld }) end Kern.Loese('katalog.geaendert', id, beamter) return { id = id } end) Kern.RegistriereAktion('admin', 'katalogSchalten', 'katalog.bearbeiten', function(quelle, beamter, daten) local id = tonumber(daten.id) if not id then return nil, 'Ungültige Kennung.' end local aktiv = daten.aktiv == true local delikt = MySQL.query.await( 'SELECT kennung, bezeichnung FROM police_katalog WHERE id = ?', { id }) if not delikt or not delikt[1] then return nil, 'Delikt nicht gefunden.' end -- Stilllegen statt löschen: bestehende Anzeigen bleiben nachvollziehbar MySQL.update.await('UPDATE police_katalog SET aktiv = ? WHERE id = ?', { aktiv and 1 or 0, id }) Protokoll.Schreibe(beamter, 'admin', aktiv and 'Delikt wieder freigegeben' or 'Delikt stillgelegt', { typ = 'katalog', id = id }, { kennung = delikt[1].kennung }, daten.grund) return { ok = true } end) -- ===================================================================== -- PROTOKOLL -- -- Reine Auskunft. Einträge lassen sich weder ändern noch entfernen. -- ===================================================================== Kern.RegistriereAktion('admin', 'protokoll', 'protokoll.lesen', function(quelle, beamter, daten) local filter = { limit = math.min(tonumber(daten.limit) or 100, 500) } if daten.modul and daten.modul ~= '' then filter.modul = daten.modul end -- Nach Beamtem filtern: über die Dienstnummer, nicht über eine ID. -- Eine unbekannte Nummer liefert eine leere Liste, keinen Fehler — -- sonst bricht die Ansicht schon beim Tippen ab. local unbekannteNummer = false if type(daten.dienstnummer) == 'string' and #daten.dienstnummer >= 2 then local charakterId = MySQL.scalar.await( 'SELECT `character` FROM police_personal WHERE dienstnummer = ?', { daten.dienstnummer:upper() }) if charakterId then filter.charakterId = charakterId else unbekannteNummer = true end end local eintraege = unbekannteNummer and {} or Protokoll.Lies(filter) for _, eintrag in ipairs(eintraege) do eintrag.name = eintrag.firstname and (eintrag.firstname .. ' ' .. eintrag.lastname) or 'System' eintrag.firstname, eintrag.lastname = nil, nil -- Details lesbar machen if eintrag.details then local erfolg, wert = pcall(json.decode, eintrag.details) eintrag.details = erfolg and wert or nil end end -- Welche Module tauchen im Protokoll überhaupt auf? local module = MySQL.query.await( 'SELECT DISTINCT modul FROM police_protokoll ORDER BY modul') or {} return { eintraege = eintraege, module = module, unbekannteNummer = unbekannteNummer, gesamt = MySQL.scalar.await('SELECT COUNT(*) FROM police_protokoll') or 0, } end) -- --------------------------------------------------------------------- -- Schnittstelle für andere Ressourcen -- --------------------------------------------------------------------- --- Liefert alle registrierten Module mit ihrem Zustand --- @return table exports('HoleModule', function() return Kern.AlleModule() end)