- 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>
634 lines
25 KiB
JavaScript
634 lines
25 KiB
JavaScript
/* =====================================================================
|
||
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 []; }
|
||
});
|
||
})();
|