Workshop-Anforderungen, erster Teil: was ohne Migration geht

Anforderung 1 — Freiwilliger vs unfreiwilliger Austritt. Die Liste der
Beendigungsarten zieht aus TerminatePanel.tsx nach lib/beendigung.ts um: der
Berichtemanager braucht sie ebenso, und zwei Listen liefen auseinander. Zwei
neue Arten (Beendigung in der Probezeit, je Seite). Auf wessen Betreiben
beendet wurde, wird aus der Art **abgeleitet** und nicht daneben gespeichert
— als zweites freies Feld liesse sich "Entlassung, freiwillig" erfassen. Das
Dropdown im Formular schraenkt die Auswahl darunter ein.

Drei Gruppen statt zwei: Befristungsablauf geschieht auf niemandes
Betreiben, ein Nichtantritt ist kein Austritt. Beide einer Seite
zuzuschlagen wuerde jede Fluktuationsquote verfaelschen.

Anforderung 2 — Namensfilter in "Anstehend", ab neun Eintraegen.

Anforderung 3 — die zwei Unterschriftenfelder im gedruckten Blatt sind weg;
"Firmenfahrzeug" steht in beiden Checklisten. has_dienstwagen sagt, ob eines
zusteht, nicht ob es uebergeben wurde.

Anforderung 4 — "+794 weitere" ist ein Knopf geworden; die Namen waren
vorher nur ueber den Export erreichbar. Stammdatenaenderung und
Gehaltsanpassung stehen nicht mehr zur Auswahl: die eine entsteht bei jeder
geaenderten Telefonnummer, die andere ist ein totes Ereignis, seit das
Gehalt in Loga liegt. Neu ist der Untertyp — Beendigungsart beim Austritt,
Art der Abwesenheit bei der Langzeitabwesenheit, im Bericht und im Export.

Anforderung 10 — zwei Kacheln. "Aktives Dienstverhaeltnis" ist nicht
dasselbe wie "Aktive Mitarbeiter:innen": dort steht, wer heute arbeitet,
hier, mit wem ein Vertrag laeuft. Sichtbar waren 806 und 10, addieren musste
man selbst.
This commit is contained in:
2026-09-15 22:35:47 +02:00
parent 1cbed1a8f5
commit 8d0c9b4b65
15 changed files with 426 additions and 48 deletions

View File

@@ -1,3 +1,5 @@
import { ABSENCE_TYPES } from "./absence";
import { BEENDIGUNGSART_WERTE } from "./beendigung";
import { fmtName, todayIso, yearsBetweenIso } from "./format";
import type { EmploymentStatus, HistoryEventType, Weekday } from "./types";
@@ -392,6 +394,28 @@ export const EVENT_TYPE_LABELS: Record<HistoryEventType, string> = {
Rückkehr: "Rückkehr aus Langzeitabwesenheit",
};
// ── Was im Berichtemanager zur Auswahl steht ────────────────────────
//
// Nicht alle elf. EVENT_TYPE_LABELS bleibt vollständig — die Historie einer
// Person zeigt jedes Ereignis, und jedes braucht seine Beschriftung und seine
// Farbe. Die *Auswertung* fragt aber nach Bewegungen im Bestand, und zwei
// Typen sind dort nur Rauschen:
//
// * „Stammdatenänderung" entsteht bei jeder geänderten Telefonnummer.
// * „Gehaltsanpassung" ist ein totes Ereignis: das Gehalt liegt in Loga,
// keine SQL-Funktion schreibt diesen Typ mehr.
//
// Beide standen in der Liste und lieferten Auswertungen, die niemand wollte.
// Aus dem Workshop gestrichen (Anforderung 4).
//
// „Wiedereintritt" stand dagegen zu Recht schon hier und bekommt in
// lib/colors.ts dieselbe grüne Kategorie wie der Eintritt — es ist einer.
const NICHT_AUSWERTBAR: readonly HistoryEventType[] = ["Stammdatenänderung", "Gehaltsanpassung"];
export const EREIGNIS_AUSWAHL: readonly HistoryEventType[] = (
Object.keys(EVENT_TYPE_LABELS) as HistoryEventType[]
).filter((t) => !NICHT_AUSWERTBAR.includes(t));
export type ReportEvent = {
employee_id: string;
first_name: string;
@@ -403,6 +427,25 @@ export type ReportEvent = {
event_date: string;
event_type: HistoryEventType;
description: string;
/**
* Der Untertyp des Ereignisses — die Beendigungsart bei einem Austritt, die
* Art der Abwesenheit bei einer Langzeitabwesenheit. Beides steht auf der
* Person und nicht am Ereignis: `employee_history` hält den Verlauf, die
* Einzelheiten stehen in `employees`.
*
* Daraus folgt die Grenze, und sie steht auch in der Oberfläche: nach einer
* Wiedereinstellung setzt rehire_employee `exit_reason` auf null zurück, und
* der Austritt von damals hat dann keinen Grund mehr. Für die Frage, die
* gestellt wird — „wie viele sind dieses Jahr gegangen, und warum" —, ist
* das ohne Belang; für eine Auswertung über fünf Jahre wäre es eine Lücke.
*/
subtype: string | null;
};
/** Zu welchen Ereignistypen es überhaupt einen Untertyp gibt. */
export const EREIGNIS_UNTERTYP: Partial<Record<HistoryEventType, { label: string; alle: string }>> = {
Austritt: { label: "Beendigungsart", alle: "Alle Beendigungsarten" },
Karenz: { label: "Art der Langzeitabwesenheit", alle: "Alle Arten" },
};
function eventGroupKeyFor(e: ReportEvent, dim: EventGroupDimension, lookups: OrgLookups): string {
@@ -518,6 +561,26 @@ export function parseEventType(value: string | null | undefined): HistoryEventTy
return value && Object.hasOwn(EVENT_TYPE_LABELS, value) ? (value as HistoryEventType) : null;
}
/**
* Der Untertyp aus der Adresszeile — geprüft gegen die Liste, die zum
* gewählten Ereignistyp gehört.
*
* Ohne den Ereignistyp gibt es keinen gültigen Untertyp: „Wochenhilfe" zu
* einem Austritt ist keine Einschränkung, sondern ein leeres Ergebnis mit
* unklarer Ursache.
*/
export function parseEventSubtype(value: string | null | undefined, typ: HistoryEventType | null): string | null {
if (!value || !typ || !EREIGNIS_UNTERTYP[typ]) return null;
return untertypOptionen(typ).includes(value) ? value : null;
}
/** Die erlaubten Untertypen eines Ereignistyps. */
export function untertypOptionen(typ: HistoryEventType | null): readonly string[] {
if (typ === "Austritt") return BEENDIGUNGSART_WERTE;
if (typ === "Karenz") return ABSENCE_TYPES;
return [];
}
// Rejects anything that is not a real calendar date, so a Stichtag from the
// URL can never reach a date comparison (or a column header) as free text.
export function parseIsoDateParam(value: string | null | undefined): string | undefined {