/* =====================================================================
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 []; }
});
})();