/* ===================================================================== aether-police / Ansicht: Personalverwaltung Dienstgrade und Rechte werden hier vergeben — jede Änderung ist eine bewusste Entscheidung eines Vorgesetzten. Das System befördert, degradiert oder entlässt niemanden von selbst; es zeigt nur an, was gilt, und woher ein Recht kommt. ===================================================================== */ "use strict"; (() => { const { sicher, zeit, dauer, aufruf, meldung, fenster, bestaetigen } = App; const z = { daten: null, offen: null, // Charakter-ID der geöffneten Personalakte suche: "", filterRolle: "", filterAbteilung: "", ehemalige: false, }; // Woher stammt ein Recht — Beschriftung und Farbe des Markers const herkunftText = { rolle: { text: "aus Rolle", art: "gut" }, zusaetzlich: { text: "zusätzlich", art: "warn" }, entzogen: { text: "entzogen", art: "kritisch" }, keins: { text: "kein Recht", art: "neutral" }, }; /* ---- Bausteine --------------------------------------------------- */ /** Name einer Abteilung anhand ihrer Kennung */ function abteilungName(id) { const treffer = ((z.daten && z.daten.abteilungen) || []).find((a) => a.id === id); return treffer ? treffer.name : id; } /** * Auswahlfeld der Dienstgrade. * @param {string} gewaehlt vorausgewählte Rolle * @param {number|undefined} maxStufe nur Rollen unterhalb dieser Stufe anbieten */ function rollenFeld(gewaehlt, maxStufe) { const optionen = ((z.daten && z.daten.rollen) || []) // Schranke: nur Rollen unterhalb des eigenen Rangs sind wählbar .filter((r) => maxStufe === undefined || r.stufe < maxStufe) .map((r) => ``).join(""); return ``; } /** Auswahlfeld der Abteilungen */ function abteilungsFeld(gewaehlt) { const optionen = ((z.daten && z.daten.abteilungen) || []) .map((a) => ``).join(""); return ``; } /* ---- Übersicht ---------------------------------------------------- */ function uebersicht(behaelter) { const d = z.daten; const liste = d.personal || []; // Kennzahlen ausschließlich aus den gelieferten Daten const gesamt = (d.verteilung || []).reduce((summe, v) => summe + v.anzahl, 0); const imDienst = liste.filter((p) => p.imDienst).length; const abweichend = liste.filter((p) => p.abweichungen > 0).length; // Verteilung nach Rollen, in der Rangfolge der Rollenliste const verteilungZeilen = (d.rollen || []).map((r) => { const eintrag = (d.verteilung || []).find((v) => v.rolle === r.id); const anzahl = eintrag ? eintrag.anzahl : 0; const anteil = gesamt > 0 ? Math.round((anzahl / gesamt) * 100) : 0; return `
${sicher(r.name)}
${anzahl}
`; }).join(""); const rollenOptionen = (d.rollen || []).map((r) => ``).join(""); const abteilungsOptionen = (d.abteilungen || []).map((a) => ``).join(""); behaelter.innerHTML = `

Personal

Dienstgrade und Rechte vergibt ein Vorgesetzter — das System schlägt nichts vor und befördert niemanden automatisch.

${d.rechte.einstellen ? `` : ""}
${gesamt} im Bestand
${imDienst} gerade im Dienst
${liste.length} in dieser Ansicht
${abweichend} mit Einzelrechten
Verteilung nach Dienstgrad
${verteilungZeilen || `
Kein Personal erfasst.
`}
${liste.length ? liste.map(zeile).join("") : `
Kein Personal gefunden.
`}
`; verdrahteUebersicht(behaelter); } /** Eine Zeile der Personalliste */ function zeile(p) { const punkt = p.imDienst ? `` : ``; return `
${punkt} ${sicher(p.dienstnummer)}
${sicher(p.firstname)} ${sicher(p.lastname)}
${sicher(p.rolleName)} · ${sicher(abteilungName(p.abteilung))} · ${dauer(p.dienstzeit || 0)} Dienstzeit
${p.abweichungen > 0 ? ` ${p.abweichungen} Einzelrecht${p.abweichungen === 1 ? "" : "e"}` : ""} ${p.aktiv ? "" : `entlassen`}
`; } function verdrahteUebersicht(behaelter) { const suchfeld = behaelter.querySelector("[data-suche]"); if (suchfeld) { let warten = null; suchfeld.addEventListener("input", () => { clearTimeout(warten); warten = setTimeout(() => { z.suche = suchfeld.value.trim(); rendern(behaelter); }, 260); }); } behaelter.querySelectorAll("[data-f]").forEach((feld) => { feld.addEventListener("change", () => { if (feld.dataset.f === "rolle") z.filterRolle = feld.value; else z.filterAbteilung = feld.value; rendern(behaelter); }); }); const umschalter = behaelter.querySelector("[data-ehemalige]"); if (umschalter) umschalter.addEventListener("click", () => { z.ehemalige = !z.ehemalige; rendern(behaelter); }); behaelter.querySelectorAll("[data-oeffne]").forEach((el) => { el.addEventListener("click", () => oeffne(behaelter, +el.dataset.oeffne)); }); const neu = behaelter.querySelector("[data-neu]"); if (neu) neu.addEventListener("click", () => einstellungsFenster(behaelter)); } /* ---- Personalakte -------------------------------------------------- */ function akte(behaelter, d) { const p = d.personal; // Rechte-Matrix nach Gruppen const rechteBloecke = (d.rechte || []).map((gruppe) => { const zeilen = gruppe.eintraege.map((r) => { const h = herkunftText[r.herkunft] || herkunftText.keins; const aktiv = r.herkunft === "rolle" || r.herkunft === "zusaetzlich"; // Umschalten nur, wenn der Betrachter den Datensatz verwalten darf const bedienbar = d.darfVerwalten && d.meineRechte.rechte; const gesperrt = !bedienbar || (!r.vergebbar && !aktiv); return `
${sicher(r.name)} ${sicher(r.id)}
${h.text}
${["rolle", "zusaetzlich", "entzogen"].map((modus) => { const gewaehlt = (modus === "rolle" && (r.herkunft === "rolle" || r.herkunft === "keins")) || modus === r.herkunft; const beschriftung = modus === "rolle" ? "Rolle" : modus === "zusaetzlich" ? "+" : "-"; const titel = modus === "rolle" ? "Es gilt das Rollenpaket" : modus === "zusaetzlich" ? "Zusätzlich gewähren" : "Einzeln entziehen"; return ``; }).join("")}
`; }).join(""); return `
${sicher(gruppe.gruppe)}
${zeilen}
`; }).join(""); // Letzte Dienste const dienste = (d.dienste || []).map((s) => `
${sicher(abteilungName(s.abteilung))}
${zeit(s.beginn)}
${s.ende ? "bis " + zeit(s.ende) : "läuft noch"}
${dauer(s.sekunden || 0)}
`).join(""); behaelter.innerHTML = `

${sicher(p.name)}

${sicher(p.dienstnummer)} · ${sicher(p.rolleName)} · ${sicher(abteilungName(p.abteilung))} ${p.aktiv ? "" : " · entlassen"}

${!d.darfVerwalten ? `
🔒
${sicher(d.sperrgrund || "Dieser Datensatz ist für dich schreibgeschützt.")} Du siehst ihn nur.
` : ""}
${dauer(d.gesamtzeit || 0)} Dienstzeit gesamt
${zeit(p.eingestellt)} eingestellt am
${sicher(p.geburt)} geboren
${d.darfVerwalten ? `
Maßnahmen
${d.meineRechte.rolle ? ` ` : ""} ${d.meineRechte.einstellen && p.aktiv ? `` : ""}
${p.notiz ? `

${sicher(p.notiz)}

` : ""}
` : ""}
Rechte „Rolle" folgt dem Dienstgrad — + und - weichen bewusst davon ab.
${rechteBloecke}
Letzte Dienste
${dienste || `
Noch kein Dienst erfasst.
`}
`; verdrahteAkte(behaelter, d); } function verdrahteAkte(behaelter, d) { const p = d.personal; behaelter.querySelector("[data-zurueck]").addEventListener("click", () => { z.offen = null; rendern(behaelter); }); behaelter.querySelectorAll("[data-recht]").forEach((knopf) => { knopf.addEventListener("click", () => rechtUmschalten(behaelter, p, knopf.dataset.recht, knopf.dataset.modus)); }); const rolle = behaelter.querySelector("[data-rolle]"); if (rolle) rolle.addEventListener("click", () => rollenFenster(behaelter, d)); const abteilung = behaelter.querySelector("[data-abteilung]"); if (abteilung) abteilung.addEventListener("click", () => abteilungsFenster(behaelter, d)); const notiz = behaelter.querySelector("[data-notiz]"); if (notiz) notiz.addEventListener("click", () => notizFenster(behaelter, d)); const entlassen = behaelter.querySelector("[data-entlassen]"); if (entlassen) entlassen.addEventListener("click", () => entlassungsFenster(behaelter, d)); } /* ---- Rechte umschalten -------------------------------------------- */ async function rechtUmschalten(behaelter, person, recht, modus) { // Ein einzelnes Recht ist eine kleine, aber bewusste Entscheidung const beschreibung = modus === "rolle" ? "Für dieses Recht gilt künftig wieder das Paket des Dienstgrads." : modus === "zusaetzlich" ? "Dieses Recht wird zusätzlich zum Dienstgrad gewährt." : "Dieses Recht wird trotz Dienstgrad einzeln entzogen."; bestaetigen({ titel: "Recht ändern", inhalt: `

${sicher(person.name)} · ${sicher(recht)}

${beschreibung}

`, bestaetigenText: "Recht setzen", gefahr: modus === "entzogen", bestaetigt: async (el, zu) => { try { await aufruf("personal", "rechtSetzen", { charakterId: person.charakterId, recht, modus, vermerk: el.querySelector("[name=vermerk]").value.trim(), }); zu(); meldung("Recht geändert.", "gut"); oeffne(behaelter, person.charakterId); } catch (f) { meldung(f.message, "fehler"); } }, }); } /* ---- Dienstgrad, Abteilung, Notiz ---------------------------------- */ function rollenFenster(behaelter, d) { const p = d.personal; bestaetigen({ titel: "Dienstgrad ändern", inhalt: `

Aktuell: ${sicher(p.rolleName)}

${rollenFeld(p.rolle, d.meineStufe)}
Rollen auf oder über deinem eigenen Rang stehen nicht zur Wahl.
`, bestaetigenText: "Dienstgrad setzen", bestaetigt: async (el, zu) => { const vermerk = el.querySelector("[name=vermerk]").value.trim(); if (vermerk.length < 3) return meldung("Bitte einen Vermerk angeben.", "fehler"); try { await aufruf("personal", "rolleAendern", { charakterId: p.charakterId, rolle: el.querySelector("[name=rolle]").value, vermerk, }); zu(); meldung("Dienstgrad geändert.", "gut"); oeffne(behaelter, p.charakterId); } catch (f) { meldung(f.message, "fehler"); } }, }); } function abteilungsFenster(behaelter, d) { const p = d.personal; bestaetigen({ titel: "Abteilung ändern", inhalt: `
${abteilungsFeld(p.abteilung)}
`, bestaetigenText: "Versetzen", bestaetigt: async (el, zu) => { try { await aufruf("personal", "abteilungAendern", { charakterId: p.charakterId, abteilung: el.querySelector("[name=abteilung]").value, }); zu(); meldung("Abteilung geändert.", "gut"); oeffne(behaelter, p.charakterId); } catch (f) { meldung(f.message, "fehler"); } }, }); } function notizFenster(behaelter, d) { const p = d.personal; fenster({ titel: "Personalnotiz", inhalt: `
`, knoepfe: [ { text: "Abbrechen", art: "leise", klick: (_, zu) => zu() }, { text: "Speichern", klick: async (el, zu) => { try { await aufruf("personal", "notiz", { charakterId: p.charakterId, notiz: el.querySelector("[name=notiz]").value.trim(), }); zu(); meldung("Notiz gespeichert.", "gut"); oeffne(behaelter, p.charakterId); } catch (f) { meldung(f.message, "fehler"); } } }, ], }); } function entlassungsFenster(behaelter, d) { const p = d.personal; bestaetigen({ titel: "Beamten entlassen", gefahr: true, inhalt: `

${sicher(p.name)} (${sicher(p.dienstnummer)}) verliert sofort allen Zugriff und wird aus einem laufenden Dienst abgemeldet.

Der Personaldatensatz bleibt erhalten und kann später wieder aktiviert werden — gelöscht wird nichts.

`, bestaetigenText: "Entlassen", bestaetigt: async (el, zu) => { const grund = el.querySelector("[name=grund]").value.trim(); if (grund.length < 5) return meldung("Bitte eine Begründung angeben.", "fehler"); try { await aufruf("personal", "entlassen", { charakterId: p.charakterId, grund }); zu(); meldung("Beamter entlassen.", "gut"); z.offen = null; rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, }); } /* ---- Einstellung ---------------------------------------------------- */ async function einstellungsFenster(behaelter) { let gewaehlt = null; // ausgewählte Person aus der Suche let vorschlag = ""; try { const antwort = await aufruf("personal", "nummerVorschlag", {}); vorschlag = antwort.dienstnummer; } catch (f) { /* Vorschlag ist optional */ } const dialog = bestaetigen({ titel: "Beamten einstellen", inhalt: `
Mindestens zwei Zeichen eingeben.
`, bestaetigenText: "Einstellen", bestaetigt: async (el, zu) => { if (!gewaehlt) return meldung("Bitte zuerst eine Person auswählen.", "fehler"); try { const antwort = await aufruf("personal", "einstellen", { charakterId: gewaehlt.id, dienstnummer: el.querySelector("[name=dienstnummer]").value.trim(), rolle: el.querySelector("[name=rolle]").value, abteilung: el.querySelector("[name=abteilung]").value, notiz: el.querySelector("[name=notiz]").value.trim(), }); zu(); meldung("Eingestellt als " + antwort.dienstnummer + ".", "gut"); rendern(behaelter); } catch (f) { meldung(f.message, "fehler"); } }, nachOeffnen: (hg) => { // Auswahlfelder erst hier einsetzen, sie brauchen z.daten hg.querySelector("[data-rollenfeld]").innerHTML = rollenFeld("anwaerter", z.daten.meineStufe); hg.querySelector("[data-abteilungsfeld]").innerHTML = abteilungsFeld("streife"); const suchfeld = hg.querySelector("[name=suche]"); const trefferliste = hg.querySelector("[data-treffer]"); const formular = hg.querySelector("[data-formular]"); let warten = null; suchfeld.addEventListener("input", () => { clearTimeout(warten); warten = setTimeout(async () => { const begriff = suchfeld.value.trim(); if (begriff.length < 2) { trefferliste.innerHTML = `
Mindestens zwei Zeichen eingeben.
`; return; } try { const antwort = await aufruf("personal", "personSuche", { begriff }); const treffer = antwort.treffer || []; trefferliste.innerHTML = treffer.length ? treffer.map((t) => `
${sicher(t.firstname)} ${sicher(t.lastname)}
geb. ${sicher(String(t.dob).slice(0, 10))}
${t.dienstnummer && t.aktiv ? `bereits im Dienst` : t.dienstnummer ? `ehemals ${sicher(t.dienstnummer)}` : ""}
`).join("") : `
Keine Person gefunden.
`; trefferliste.querySelectorAll("[data-person]").forEach((el) => { el.addEventListener("click", () => { const person = treffer.find((t) => t.id === +el.dataset.person); if (person && person.dienstnummer && person.aktiv) { return meldung("Diese Person ist bereits im Personalbestand.", "fehler"); } gewaehlt = person; trefferliste.querySelectorAll("[data-person]") .forEach((zeile) => zeile.classList.remove("gewaehlt")); el.classList.add("gewaehlt"); formular.style.display = ""; }); }); } catch (f) { meldung(f.message, "fehler"); } }, 260); }); }, }); return dialog; } /* ---- Laden ---------------------------------------------------------- */ async function oeffne(behaelter, charakterId) { try { const d = await aufruf("personal", "oeffnen", { charakterId }); z.offen = charakterId; akte(behaelter, d); } catch (f) { meldung(f.message, "fehler"); } } async function rendern(behaelter) { try { z.daten = await aufruf("personal", "liste", { begriff: z.suche, rolle: z.filterRolle, abteilung: z.filterAbteilung, ehemalige: z.ehemalige, }); // Sprungziel aus einem anderen Modul, z. B. „Personalakte öffnen" const ziel = App.zustand.modulZiel; if (ziel && ziel.person) { App.zustand.modulZiel = null; return oeffne(behaelter, ziel.person); } if (z.offen) await oeffne(behaelter, z.offen); else uebersicht(behaelter); } catch (f) { behaelter.innerHTML = `

Personal

Konnte nicht geladen werden: ${sicher(f.message)}

`; } } App.registriereAnsicht("personal", { rendern }); App.registriereSuche(async (begriff) => { try { const d = await aufruf("personal", "liste", { begriff }); return (d.personal || []).slice(0, 5).map((p) => ({ bereich: "Personal", titel: p.firstname + " " + p.lastname, unter: p.dienstnummer + " · " + p.rolleName, })); } catch (e) { return []; } }); })();