-- ===================================================================== -- aether-police / Modul: Personalverwaltung -- -- Rollen und Einzelrechte werden hier vergeben. Es gelten drei feste -- Schranken, die sich nicht umgehen lassen: -- 1. Niemand verwaltet Gleichrangige oder Höherstehende. -- 2. Niemand vergibt eine Rolle auf oder über dem eigenen Rang. -- 3. Niemand vergibt ein Recht, das er selbst nicht besitzt. -- Zusätzlich kann sich niemand selbst bearbeiten. -- ===================================================================== Kern.RegistriereModul({ id = 'personal', name = 'Personal', icon = '👮', reihenfolge = 11, recht = 'personal.sehen', beschreibung = 'Dienstgrade, Rechte und Personalakten', }) -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- --- Schlägt die nächste freie Dienstnummer vor --- @return string local function NaechsteDienstnummer() local praefix = Polizei.Config.Kuerzel .. '-' -- Alle vergebenen Nummern einmal holen, dann die erste Lücke suchen local vergeben = {} local zeilen = MySQL.query.await( 'SELECT dienstnummer FROM police_personal WHERE dienstnummer LIKE ?', { praefix .. '%' }) or {} for _, zeile in ipairs(zeilen) do vergeben[zeile.dienstnummer] = true end for nummer = 1, 9999 do local kandidat = ('%s%04d'):format(praefix, nummer) if not vergeben[kandidat] then return kandidat end end return praefix .. tostring(os.time()):sub(-4) end --- Wandelt eine JSON-Liste in eine Menge um local function ZuMenge(roh) local menge = {} if not roh then return menge end local erfolg, liste = pcall(json.decode, roh) if erfolg and type(liste) == 'table' then for _, wert in ipairs(liste) do menge[wert] = true end end return menge end --- Wandelt eine Menge zurück in eine sortierte Liste local function ZuListe(menge) local liste = {} for wert in pairs(menge) do liste[#liste + 1] = wert end table.sort(liste) return liste end --- Prüft, ob ein Beamter einen anderen Personaldatensatz verwalten darf --- @param beamter table Handelnder --- @param ziel table Zeile aus police_personal --- @return boolean, string|nil local function DarfVerwalten(beamter, ziel) if ziel.character == beamter.charakterId then return false, 'Du kannst deinen eigenen Datensatz nicht bearbeiten.' end if not Rechte.StehtUeber(beamter, ziel.rolle) then return false, 'Du kannst nur rangniedrigere Beamte verwalten.' end return true end -- ===================================================================== -- ÜBERSICHT -- ===================================================================== Kern.RegistriereAktion('personal', 'liste', nil, function(quelle, beamter, daten) local bedingungen, werte = { '1=1' }, {} if daten.ehemalige then bedingungen[#bedingungen + 1] = 'p.aktiv = 0' else bedingungen[#bedingungen + 1] = 'p.aktiv = 1' end if daten.abteilung and daten.abteilung ~= '' then bedingungen[#bedingungen + 1] = 'p.abteilung = ?' werte[#werte + 1] = daten.abteilung end if daten.rolle and daten.rolle ~= '' then bedingungen[#bedingungen + 1] = 'p.rolle = ?' werte[#werte + 1] = daten.rolle end if type(daten.begriff) == 'string' and #daten.begriff >= 2 then bedingungen[#bedingungen + 1] = '(p.dienstnummer LIKE ? OR c.firstname LIKE ? OR c.lastname LIKE ?)' local muster = '%' .. daten.begriff .. '%' werte[#werte + 1] = muster werte[#werte + 1] = muster werte[#werte + 1] = muster end local personal = MySQL.query.await(([[ SELECT p.`character`, p.dienstnummer, p.rolle, p.abteilung, p.eingestellt, p.aktiv, p.notiz, p.rechte_extra, p.rechte_entzug, c.firstname, c.lastname, (SELECT COALESCE(SUM(d.sekunden), 0) FROM police_dienst d WHERE d.`character` = p.`character`) AS dienstzeit FROM police_personal p JOIN aether_characters c ON c.id = p.`character` WHERE %s ORDER BY p.dienstnummer LIMIT 200 ]]):format(table.concat(bedingungen, ' AND ')), werte) or {} -- Wer ist gerade im Dienst? local imDienst = {} for _, eintrag in pairs(Dienst.AlleImDienst()) do imDienst[eintrag.charakterId] = true end for _, eintrag in ipairs(personal) do local rolle = Polizei.HoleRolle(eintrag.rolle) eintrag.rolleName = rolle and rolle.name or eintrag.rolle eintrag.stufe = Polizei.Stufe(eintrag.rolle) eintrag.imDienst = imDienst[eintrag.character] == true -- Anzahl abweichender Einzelrechte für die Übersicht local extra = ZuMenge(eintrag.rechte_extra) local entzug = ZuMenge(eintrag.rechte_entzug) local anzahl = 0 for _ in pairs(extra) do anzahl = anzahl + 1 end for _ in pairs(entzug) do anzahl = anzahl + 1 end eintrag.abweichungen = anzahl eintrag.rechte_extra, eintrag.rechte_entzug = nil, nil end -- Verteilung nach Rollen local verteilung = MySQL.query.await( 'SELECT rolle, COUNT(*) AS anzahl FROM police_personal WHERE aktiv = 1 GROUP BY rolle') or {} return { personal = personal, verteilung = verteilung, rollen = Polizei.Rollen, abteilungen= Polizei.Config.Abteilungen, meineStufe = beamter.stufe, rechte = { einstellen = Rechte.Hat(beamter, 'personal.einstellen'), rolle = Rechte.Hat(beamter, 'personal.rolle'), rechte = Rechte.Hat(beamter, 'personal.rechte'), }, } end) Kern.RegistriereAktion('personal', 'oeffnen', nil, function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Ungültige Kennung.' end local zeilen = MySQL.query.await([[ SELECT p.*, c.firstname, c.lastname, c.dob FROM police_personal p JOIN aether_characters c ON c.id = p.`character` WHERE p.`character` = ? ]], { charakterId }) if not zeilen or not zeilen[1] then return nil, 'Personaldatensatz nicht gefunden.' end local eintrag = zeilen[1] local rolle = Polizei.HoleRolle(eintrag.rolle) -- Rechte aufschlüsseln: woher kommt welches Recht? local ausRolle = Polizei.RechteDerRolle(eintrag.rolle) local extra = ZuMenge(eintrag.rechte_extra) local entzug = ZuMenge(eintrag.rechte_entzug) local rechteUebersicht = {} for _, gruppe in ipairs(Polizei.Rechte) do local eintraege = {} for _, recht in ipairs(gruppe.eintraege) do local herkunft = 'keins' if entzug[recht.id] then herkunft = 'entzogen' elseif extra[recht.id] then herkunft = 'zusaetzlich' elseif ausRolle[recht.id] then herkunft = 'rolle' end eintraege[#eintraege + 1] = { id = recht.id, name = recht.name, herkunft = herkunft, -- Darf der Handelnde dieses Recht überhaupt vergeben? vergebbar = Rechte.Hat(beamter, recht.id), } end rechteUebersicht[#rechteUebersicht + 1] = { gruppe = gruppe.gruppe, eintraege = eintraege } end -- Dienstzeiten local dienste = MySQL.query.await([[ SELECT beginn, ende, sekunden, abteilung FROM police_dienst WHERE `character` = ? ORDER BY id DESC LIMIT 20 ]], { charakterId }) or {} local gesamt = MySQL.scalar.await( 'SELECT COALESCE(SUM(sekunden), 0) FROM police_dienst WHERE `character` = ?', { charakterId }) or 0 local darf, grund = DarfVerwalten(beamter, eintrag) return { personal = { charakterId = eintrag.character, name = eintrag.firstname .. ' ' .. eintrag.lastname, geburt = tostring(eintrag.dob):sub(1, 10), dienstnummer= eintrag.dienstnummer, rolle = eintrag.rolle, rolleName = rolle and rolle.name or eintrag.rolle, abteilung = eintrag.abteilung, eingestellt = eintrag.eingestellt, aktiv = eintrag.aktiv == 1, notiz = eintrag.notiz, }, rechte = rechteUebersicht, dienste = dienste, gesamtzeit = gesamt, rollen = Polizei.Rollen, abteilungen = Polizei.Config.Abteilungen, meineStufe = beamter.stufe, darfVerwalten= darf, sperrgrund = grund, meineRechte = { rolle = Rechte.Hat(beamter, 'personal.rolle'), rechte = Rechte.Hat(beamter, 'personal.rechte'), einstellen = Rechte.Hat(beamter, 'personal.einstellen'), }, } end) -- ===================================================================== -- EINSTELLEN -- ===================================================================== Kern.RegistriereAktion('personal', 'einstellen', 'personal.einstellen', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Keine Person ausgewählt.' end local person = MySQL.query.await( 'SELECT firstname, lastname FROM aether_characters WHERE id = ? AND deleted = 0', { charakterId }) if not person or not person[1] then return nil, 'Diese Person existiert nicht.' end -- Bereits im Bestand? local vorhanden = MySQL.query.await( 'SELECT dienstnummer, aktiv FROM police_personal WHERE `character` = ?', { charakterId }) if vorhanden and vorhanden[1] and vorhanden[1].aktiv == 1 then return nil, 'Diese Person ist bereits im Personalbestand.' end -- Rolle prüfen: niemals auf oder über dem eigenen Rang local rollenId = tostring(daten.rolle or 'anwaerter') local rolle = Polizei.HoleRolle(rollenId) if not rolle then return nil, 'Unbekannte Rolle.' end if rolle.stufe >= beamter.stufe then return nil, 'Du kannst niemanden auf deinen Rang oder darüber einstellen.' end -- Abteilung prüfen local abteilung = 'streife' for _, eintrag in ipairs(Polizei.Config.Abteilungen) do if eintrag.id == daten.abteilung then abteilung = daten.abteilung break end end -- Dienstnummer: eigene Angabe oder Vorschlag local dienstnummer = type(daten.dienstnummer) == 'string' and daten.dienstnummer:gsub('%s', ''):upper() or '' if dienstnummer == '' then dienstnummer = NaechsteDienstnummer() end if #dienstnummer > 16 then return nil, 'Die Dienstnummer ist zu lang.' end local belegt = MySQL.scalar.await( 'SELECT `character` FROM police_personal WHERE dienstnummer = ? AND `character` <> ?', { dienstnummer, charakterId }) if belegt then return nil, 'Diese Dienstnummer ist bereits vergeben.' end -- Wiedereinstellung oder Neuanlage if vorhanden and vorhanden[1] then MySQL.update.await([[ UPDATE police_personal SET dienstnummer = ?, rolle = ?, abteilung = ?, aktiv = 1, rechte_extra = NULL, rechte_entzug = NULL, eingestellt = NOW() WHERE `character` = ? ]], { dienstnummer, rollenId, abteilung, charakterId }) else MySQL.insert.await([[ INSERT INTO police_personal (`character`, dienstnummer, rolle, abteilung, notiz) VALUES (?, ?, ?, ?, ?) ]], { charakterId, dienstnummer, rollenId, abteilung, type(daten.notiz) == 'string' and daten.notiz:sub(1, 255) or nil }) end Protokoll.Schreibe(beamter, 'personal', 'Beamten eingestellt', { typ = 'person', id = charakterId }, { dienstnummer = dienstnummer, rolle = rollenId, abteilung = abteilung }) Hinweise.Erstelle(nil, { kategorie = 'system', prioritaet = 'niedrig', titel = 'Neuer Kollege', text = ('%s %s (%s) wurde als %s eingestellt.') :format(person[1].firstname, person[1].lastname, dienstnummer, rolle.name), }) Kern.Loese('personal.eingestellt', charakterId, beamter) return { dienstnummer = dienstnummer } end) --- Vorschlag für die nächste Dienstnummer Kern.RegistriereAktion('personal', 'nummerVorschlag', 'personal.einstellen', function() return { dienstnummer = NaechsteDienstnummer() } end) --- Personensuche für die Einstellung Kern.RegistriereAktion('personal', 'personSuche', 'personal.einstellen', function(quelle, beamter, daten) local begriff = type(daten.begriff) == 'string' and daten.begriff or '' if #begriff < 2 then return { treffer = {} } end local muster = '%' .. begriff .. '%' local treffer = MySQL.query.await([[ SELECT c.id, c.firstname, c.lastname, c.dob, p.dienstnummer, p.aktiv FROM aether_characters c LEFT JOIN police_personal p ON p.`character` = c.id WHERE c.deleted = 0 AND (c.firstname LIKE ? OR c.lastname LIKE ? OR CONCAT(c.firstname, ' ', c.lastname) LIKE ?) LIMIT 20 ]], { muster, muster, muster }) or {} return { treffer = treffer } end) -- ===================================================================== -- ROLLE UND ABTEILUNG -- ===================================================================== Kern.RegistriereAktion('personal', 'rolleAendern', 'personal.rolle', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Ungültige Kennung.' end local ziel = MySQL.query.await( 'SELECT `character`, rolle, dienstnummer FROM police_personal WHERE `character` = ? AND aktiv = 1', { charakterId }) if not ziel or not ziel[1] then return nil, 'Personaldatensatz nicht gefunden.' end local darf, grund = DarfVerwalten(beamter, ziel[1]) if not darf then return nil, grund end local neueRolle = Polizei.HoleRolle(tostring(daten.rolle or '')) if not neueRolle then return nil, 'Unbekannte Rolle.' end -- Schranke 2: nie auf oder über den eigenen Rang befördern if neueRolle.stufe >= beamter.stufe then return nil, 'Du kannst niemanden auf deinen Rang oder darüber setzen.' end local vermerk = type(daten.vermerk) == 'string' and daten.vermerk:sub(1, 255) or '' if #vermerk < 3 then return nil, 'Bitte einen Vermerk zur Änderung angeben.' end local alteStufe = Polizei.Stufe(ziel[1].rolle) local richtung = neueRolle.stufe > alteStufe and 'Beförderung' or neueRolle.stufe < alteStufe and 'Degradierung' or 'Rollenwechsel' MySQL.update.await('UPDATE police_personal SET rolle = ? WHERE `character` = ?', { neueRolle.id, charakterId }) -- Läuft die Person gerade im Dienst, Rechte sofort neu laden local zielQuelle = Dienst.QuelleVonCharakter(charakterId) if zielQuelle then local neu = Dienst.LadeBeamten(zielQuelle, charakterId) if neu then local aktuell = Dienst.Hole(zielQuelle) aktuell.rolle = neu.rolle aktuell.rolleName = neu.rolleName aktuell.stufe = neu.stufe aktuell.rechte = neu.rechte end Hinweise.Erstelle({ charakterId = charakterId }, { kategorie = 'system', prioritaet = 'normal', titel = richtung, text = ('Dein Dienstgrad wurde auf %s gesetzt: %s'):format(neueRolle.name, vermerk), }) end Protokoll.Schreibe(beamter, 'personal', richtung, { typ = 'person', id = charakterId }, { von = ziel[1].rolle, nach = neueRolle.id, dienstnummer = ziel[1].dienstnummer }, vermerk) return { ok = true } end) Kern.RegistriereAktion('personal', 'abteilungAendern', 'personal.rolle', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Ungültige Kennung.' end local ziel = MySQL.query.await( 'SELECT `character`, rolle FROM police_personal WHERE `character` = ? AND aktiv = 1', { charakterId }) if not ziel or not ziel[1] then return nil, 'Personaldatensatz nicht gefunden.' end local darf, grund = DarfVerwalten(beamter, ziel[1]) if not darf then return nil, grund end local abteilung = nil for _, eintrag in ipairs(Polizei.Config.Abteilungen) do if eintrag.id == daten.abteilung then abteilung = eintrag break end end if not abteilung then return nil, 'Unbekannte Abteilung.' end MySQL.update.await('UPDATE police_personal SET abteilung = ? WHERE `character` = ?', { abteilung.id, charakterId }) Protokoll.Schreibe(beamter, 'personal', 'Abteilung geändert', { typ = 'person', id = charakterId }, { abteilung = abteilung.id }) return { ok = true } end) -- ===================================================================== -- EINZELRECHTE -- -- Schranke 3: Niemand kann ein Recht vergeben, das er selbst nicht hat. -- ===================================================================== Kern.RegistriereAktion('personal', 'rechtSetzen', 'personal.rechte', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) local rechtId = tostring(daten.recht or '') local modus = tostring(daten.modus or '') -- zusaetzlich | entzogen | rolle if not charakterId or rechtId == '' then return nil, 'Unvollständige Angaben.' end if modus ~= 'zusaetzlich' and modus ~= 'entzogen' and modus ~= 'rolle' then return nil, 'Unbekannter Modus.' end -- Existiert das Recht überhaupt? local bekannt = false for _, id in ipairs(Polizei.AlleRechte()) do if id == rechtId then bekannt = true break end end if not bekannt then return nil, 'Unbekanntes Recht.' end local ziel = MySQL.query.await( 'SELECT `character`, rolle, rechte_extra, rechte_entzug, dienstnummer FROM police_personal WHERE `character` = ? AND aktiv = 1', { charakterId }) if not ziel or not ziel[1] then return nil, 'Personaldatensatz nicht gefunden.' end local darf, grund = DarfVerwalten(beamter, ziel[1]) if not darf then return nil, grund end -- Schranke 3: nur eigene Rechte weitergeben if modus == 'zusaetzlich' and not Rechte.Hat(beamter, rechtId) then return nil, 'Du kannst nur Rechte vergeben, die du selbst besitzt.' end local extra = ZuMenge(ziel[1].rechte_extra) local entzug = ZuMenge(ziel[1].rechte_entzug) -- Erst beide Listen bereinigen, dann neu setzen extra[rechtId] = nil entzug[rechtId] = nil if modus == 'zusaetzlich' then extra[rechtId] = true elseif modus == 'entzogen' then entzug[rechtId] = true end -- modus == 'rolle' lässt beide leer: es gilt wieder die Rolle MySQL.update.await( 'UPDATE police_personal SET rechte_extra = ?, rechte_entzug = ? WHERE `character` = ?', { json.encode(ZuListe(extra)), json.encode(ZuListe(entzug)), charakterId }) -- Im Dienst? Dann sofort wirksam machen local zielQuelle = Dienst.QuelleVonCharakter(charakterId) if zielQuelle then local neu = Dienst.LadeBeamten(zielQuelle, charakterId) if neu then Dienst.Hole(zielQuelle).rechte = neu.rechte end end Protokoll.Schreibe(beamter, 'personal', 'Einzelrecht geändert', { typ = 'person', id = charakterId }, { recht = rechtId, modus = modus, dienstnummer = ziel[1].dienstnummer }, daten.vermerk) return { ok = true } end) -- ===================================================================== -- NOTIZ UND ENTLASSUNG -- ===================================================================== Kern.RegistriereAktion('personal', 'notiz', 'personal.rolle', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId then return nil, 'Ungültige Kennung.' end local ziel = MySQL.query.await( 'SELECT `character`, rolle FROM police_personal WHERE `character` = ?', { charakterId }) if not ziel or not ziel[1] then return nil, 'Personaldatensatz nicht gefunden.' end local darf, grund = DarfVerwalten(beamter, ziel[1]) if not darf then return nil, grund end MySQL.update.await('UPDATE police_personal SET notiz = ? WHERE `character` = ?', { type(daten.notiz) == 'string' and daten.notiz:sub(1, 255) or nil, charakterId }) Protokoll.Schreibe(beamter, 'personal', 'Personalnotiz geändert', { typ = 'person', id = charakterId }) return { ok = true } end) Kern.RegistriereAktion('personal', 'entlassen', 'personal.einstellen', function(quelle, beamter, daten) local charakterId = tonumber(daten.charakterId) if not charakterId 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, 'Eine Entlassung erfordert eine Begründung.' end local ziel = MySQL.query.await([[ SELECT p.`character`, p.rolle, p.dienstnummer, c.firstname, c.lastname FROM police_personal p JOIN aether_characters c ON c.id = p.`character` WHERE p.`character` = ? AND p.aktiv = 1 ]], { charakterId }) if not ziel or not ziel[1] then return nil, 'Personaldatensatz nicht gefunden.' end local darf, sperre = DarfVerwalten(beamter, ziel[1]) if not darf then return nil, sperre end -- Datensatz bleibt bestehen, nur inaktiv gesetzt MySQL.update.await('UPDATE police_personal SET aktiv = 0 WHERE `character` = ?', { charakterId }) -- Läuft die Person im Dienst, wird sie abgemeldet local zielQuelle = Dienst.QuelleVonCharakter(charakterId) if zielQuelle then Dienst.Beenden(zielQuelle) TriggerClientEvent('aether:police:schliessen', zielQuelle) end -- Zugewiesene Dienstfahrzeuge freigeben MySQL.update('UPDATE police_fahrzeug SET zugewiesen_an = NULL WHERE zugewiesen_an = ?', { charakterId }) Protokoll.Schreibe(beamter, 'personal', 'Beamten entlassen', { typ = 'person', id = charakterId }, { dienstnummer = ziel[1].dienstnummer, rolle = ziel[1].rolle }, grund) Hinweise.Erstelle(nil, { kategorie = 'system', prioritaet = 'normal', titel = 'Personalveränderung', text = ('%s %s (%s) wurde aus dem Dienst entlassen.') :format(ziel[1].firstname, ziel[1].lastname, ziel[1].dienstnummer), }) Kern.Loese('personal.entlassen', charakterId, beamter) return { ok = true } end)