aether-framework/resources/[aether]/aether-police/html/js/module/personal.js
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

634 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
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) => `<option value="${sicher(r.id)}" ${r.id === gewaehlt ? "selected" : ""}>
${sicher(r.name)}
</option>`).join("");
return `<select name="rolle">${optionen}</select>`;
}
/** Auswahlfeld der Abteilungen */
function abteilungsFeld(gewaehlt) {
const optionen = ((z.daten && z.daten.abteilungen) || [])
.map((a) => `<option value="${sicher(a.id)}" ${a.id === gewaehlt ? "selected" : ""}>
${sicher(a.name)}
</option>`).join("");
return `<select name="abteilung">${optionen}</select>`;
}
/* ---- Ü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 `<div class="pv-verteilung-zeile">
<span class="pv-vt-name">${sicher(r.name)}</span>
<div class="pv-vt-balken"><i style="width:${anteil}%"></i></div>
<span class="pv-vt-zahl">${anzahl}</span>
</div>`;
}).join("");
const rollenOptionen = (d.rollen || []).map((r) =>
`<option value="${sicher(r.id)}" ${r.id === z.filterRolle ? "selected" : ""}>
${sicher(r.name)}</option>`).join("");
const abteilungsOptionen = (d.abteilungen || []).map((a) =>
`<option value="${sicher(a.id)}" ${a.id === z.filterAbteilung ? "selected" : ""}>
${sicher(a.name)}</option>`).join("");
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Personal</h2>
<p>Dienstgrade und Rechte vergibt ein Vorgesetzter — das System schlägt nichts vor
und befördert niemanden automatisch.</p>
</div>
${d.rechte.einstellen
? `<button class="knopf" data-neu="1">+ Beamten einstellen</button>` : ""}
</div>
<div class="raster raster-4">
<div class="kennzahl"><span class="kz-zahl">${gesamt}</span>
<span class="kz-text">im Bestand</span></div>
<div class="kennzahl"><span class="kz-zahl">${imDienst}</span>
<span class="kz-text">gerade im Dienst</span></div>
<div class="kennzahl"><span class="kz-zahl">${liste.length}</span>
<span class="kz-text">in dieser Ansicht</span></div>
<div class="kennzahl"><span class="kz-zahl">${abweichend}</span>
<span class="kz-text">mit Einzelrechten</span></div>
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Verteilung nach Dienstgrad</span></div>
<div class="pv-verteilung">${verteilungZeilen || `<div class="liste-leer">Kein Personal erfasst.</div>`}</div>
</div>
<div class="mdt-suchleiste">
<input class="feld" data-suche="1" placeholder="Name oder Dienstnummer…"
value="${sicher(z.suche)}">
<select class="feld" data-f="rolle">
<option value="">Alle Dienstgrade</option>${rollenOptionen}
</select>
<select class="feld" data-f="abteilung">
<option value="">Alle Abteilungen</option>${abteilungsOptionen}
</select>
<button class="knopf ${z.ehemalige ? "" : "leise"}" data-ehemalige="1">
${z.ehemalige ? "Ehemalige" : "Aktive"}
</button>
</div>
<div class="liste">
${liste.length ? liste.map(zeile).join("")
: `<div class="liste-leer">Kein Personal gefunden.</div>`}
</div>
`;
verdrahteUebersicht(behaelter);
}
/** Eine Zeile der Personalliste */
function zeile(p) {
const punkt = p.imDienst
? `<span class="pv-punkt an" title="Im Dienst"></span>`
: `<span class="pv-punkt" title="Außer Dienst"></span>`;
return `<div class="liste-zeile" data-oeffne="${p.character}">
${punkt}
<span class="pv-nummer">${sicher(p.dienstnummer)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(p.firstname)} ${sicher(p.lastname)}</div>
<div class="lz-unter">${sicher(p.rolleName)} · ${sicher(abteilungName(p.abteilung))}
· ${dauer(p.dienstzeit || 0)} Dienstzeit</div>
</div>
${p.abweichungen > 0
? `<span class="marker warn" title="Vom Rollenpaket abweichende Einzelrechte">
${p.abweichungen} Einzelrecht${p.abweichungen === 1 ? "" : "e"}</span>` : ""}
${p.aktiv ? "" : `<span class="marker neutral">entlassen</span>`}
</div>`;
}
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 `<div class="pv-recht ${aktiv ? "aktiv" : ""}">
<div class="pv-recht-text">
<span class="pv-recht-name">${sicher(r.name)}</span>
<span class="pv-recht-id">${sicher(r.id)}</span>
</div>
<span class="marker ${h.art}">${h.text}</span>
<div class="pv-recht-schalter">
${["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 `<button class="pv-schalter ${gewaehlt ? "gewaehlt" : ""}"
title="${titel}" ${gesperrt ? "disabled" : ""}
data-recht="${sicher(r.id)}" data-modus="${modus}">
${beschriftung}</button>`;
}).join("")}
</div>
</div>`;
}).join("");
return `<div class="pv-rechte-gruppe">
<div class="pv-rechte-titel">${sicher(gruppe.gruppe)}</div>
${zeilen}
</div>`;
}).join("");
// Letzte Dienste
const dienste = (d.dienste || []).map((s) =>
`<div class="liste-zeile">
<span class="marker neutral">${sicher(abteilungName(s.abteilung))}</span>
<div class="lz-haupt">
<div class="lz-titel">${zeit(s.beginn)}</div>
<div class="lz-unter">${s.ende ? "bis " + zeit(s.ende) : "läuft noch"}</div>
</div>
<span class="pv-dienstdauer">${dauer(s.sekunden || 0)}</span>
</div>`).join("");
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>${sicher(p.name)}</h2>
<p>${sicher(p.dienstnummer)} · ${sicher(p.rolleName)}
· ${sicher(abteilungName(p.abteilung))}
${p.aktiv ? "" : " · <b>entlassen</b>"}</p>
</div>
<button class="knopf leise" data-zurueck="1">← Übersicht</button>
</div>
${!d.darfVerwalten ? `
<div class="warnung">
<span>🔒</span>
<div>${sicher(d.sperrgrund || "Dieser Datensatz ist für dich schreibgeschützt.")}
Du siehst ihn nur.</div>
</div>` : ""}
<div class="raster raster-3">
<div class="kennzahl"><span class="kz-zahl">${dauer(d.gesamtzeit || 0)}</span>
<span class="kz-text">Dienstzeit gesamt</span></div>
<div class="kennzahl"><span class="kz-zahl">${zeit(p.eingestellt)}</span>
<span class="kz-text">eingestellt am</span></div>
<div class="kennzahl"><span class="kz-zahl">${sicher(p.geburt)}</span>
<span class="kz-text">geboren</span></div>
</div>
${d.darfVerwalten ? `
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Maßnahmen</span></div>
<div class="akte-kopf-aktionen">
${d.meineRechte.rolle ? `
<button class="knopf" data-rolle="1">Dienstgrad ändern</button>
<button class="knopf leise" data-abteilung="1">Abteilung ändern</button>
<button class="knopf leise" data-notiz="1">Notiz bearbeiten</button>` : ""}
${d.meineRechte.einstellen && p.aktiv
? `<button class="knopf gefahr" data-entlassen="1">Entlassen</button>` : ""}
</div>
${p.notiz ? `<p class="pv-notiz">${sicher(p.notiz)}</p>` : ""}
</div>` : ""}
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Rechte</span>
<span class="feld-hinweis">„Rolle" folgt dem Dienstgrad — + und -
weichen bewusst davon ab.</span>
</div>
<div class="pv-rechte">${rechteBloecke}</div>
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Letzte Dienste</span></div>
<div class="liste">${dienste || `<div class="liste-leer">Noch kein Dienst erfasst.</div>`}</div>
</div>
`;
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: `<p><b>${sicher(person.name)}</b> · <code>${sicher(recht)}</code></p>
<p>${beschreibung}</p>
<div class="feld"><label>Vermerk (freiwillig)</label>
<input name="vermerk" placeholder="Grund der Änderung…"></div>`,
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: `
<p>Aktuell: <b>${sicher(p.rolleName)}</b></p>
<div class="feld"><label>Neuer Dienstgrad</label>
${rollenFeld(p.rolle, d.meineStufe)}
<div class="feld-hinweis">Rollen auf oder über deinem eigenen Rang
stehen nicht zur Wahl.</div></div>
<div class="feld"><label>Vermerk (Pflicht)</label>
<input name="vermerk" placeholder="Warum wird der Dienstgrad geändert?"></div>`,
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: `<div class="feld"><label>Abteilung</label>
${abteilungsFeld(p.abteilung)}</div>`,
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: `<div class="feld"><label>Notiz (max. 255 Zeichen)</label>
<textarea name="notiz" rows="4"
placeholder="Interner Vermerk zur Person…">${sicher(p.notiz || "")}</textarea></div>`,
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: `
<p><b>${sicher(p.name)}</b> (${sicher(p.dienstnummer)}) verliert sofort allen Zugriff
und wird aus einem laufenden Dienst abgemeldet.</p>
<p class="feld-hinweis">Der Personaldatensatz bleibt erhalten und kann später
wieder aktiviert werden — gelöscht wird nichts.</p>
<div class="feld"><label>Begründung (Pflicht)</label>
<textarea name="grund" rows="3"
placeholder="Warum wird der Beamte entlassen?"></textarea></div>`,
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: `
<div class="feld"><label>Person suchen</label>
<input name="suche" placeholder="Vor- oder Nachname…"></div>
<div class="liste pv-trefferliste" data-treffer="1">
<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>
</div>
<div class="pv-formular" data-formular="1" style="display:none">
<div class="feld"><label>Dienstnummer</label>
<input name="dienstnummer" value="${sicher(vorschlag)}"></div>
<div class="feld"><label>Dienstgrad</label>
<span data-rollenfeld="1"></span></div>
<div class="feld"><label>Abteilung</label>
<span data-abteilungsfeld="1"></span></div>
<div class="feld"><label>Notiz (freiwillig)</label>
<input name="notiz" placeholder="Vermerk zur Einstellung…"></div>
</div>`,
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 =
`<div class="liste-leer">Mindestens zwei Zeichen eingeben.</div>`;
return;
}
try {
const antwort = await aufruf("personal", "personSuche", { begriff });
const treffer = antwort.treffer || [];
trefferliste.innerHTML = treffer.length
? treffer.map((t) => `
<div class="liste-zeile" data-person="${t.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(t.firstname)} ${sicher(t.lastname)}</div>
<div class="lz-unter">geb. ${sicher(String(t.dob).slice(0, 10))}</div>
</div>
${t.dienstnummer && t.aktiv
? `<span class="marker warn">bereits im Dienst</span>`
: t.dienstnummer
? `<span class="marker neutral">ehemals ${sicher(t.dienstnummer)}</span>`
: ""}
</div>`).join("")
: `<div class="liste-leer">Keine Person gefunden.</div>`;
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 = `<div class="seiten-kopf"><div><h2>Personal</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
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 []; }
});
})();