import { ABSENCE_TYPES } from "./absence"; import { AUSTRITTSART_LABELS, AUSTRITTSARTEN, BEENDIGUNGSART_WERTE } from "./beendigung"; import { fmtDate } from "./format"; import { HAY_GRADE_WERTE } from "./hay-grade"; import { KUENDIGUNGSSCHUTZ_GRUENDE } from "./kuendigungsschutz"; import { MITARBEITERARTEN } from "./mitarbeiterart"; import { parseIsoDateParam } from "./reports"; import type { Weekday } from "./types"; // Ein Verzeichnis aller Auswahlkriterien — für die Oberfläche, die Abfrage // und den Export dasselbe. // // Ein Export, der weniger filtern kann als der Bericht daneben, ist der // Grund, warum Leute Daten nach Excel kippen und dort weiterarbeiten: was // die Anwendung nicht hergibt, wird eben von Hand nachgebaut, und ab da // stimmt es mit nichts mehr überein. Deshalb steht hier jedes Kriterium // einmal, mit seinen erlaubten Werten, und alle drei Seiten lesen aus // derselben Liste: // // * die Oberfläche baut ihre Felder daraus, // * `parseCriteria` prüft die Adresszeile dagegen, // * `applyCriteria` (serverseitig) macht Bedingungen daraus. // // Ein neues Kriterium ist damit ein Eintrag in dieser Datei und sonst // nichts — und es kann nicht passieren, dass es im Bericht wirkt, im Export // aber stillschweigend ignoriert wird. // // Die Prüfung ist nicht bloss Ordnungsliebe: die Werte landen in // SQL-Vergleichen und im Dateinamen des Downloads, also in einem // Content-Disposition-Header. Was nicht in der Liste steht, kommt nicht // durch. export type JaNein = "ja" | "nein"; /** Ein Kriterium mit fester Werteliste. */ export type AuswahlKriterium = { key: string; label: string; /** Beschriftung für „keine Einschränkung". */ alle: string; optionen: readonly { wert: string; label: string }[]; }; function werte(...w: string[]): { wert: string; label: string }[] { return w.map((x) => ({ wert: x, label: x })); } export const AUSWAHL_KRITERIEN: readonly AuswahlKriterium[] = [ { key: "employment", label: "Beschäftigungsart", alle: "Alle Beschäftigungsarten", optionen: werte("Vollzeit", "Teilzeit") }, { key: "contract", label: "Vertragsart", alle: "Alle Vertragsarten", optionen: werte("unbefristet", "befristet") }, { key: "worker", label: "Beschäftigtengruppe", alle: "Alle", optionen: werte("Angestellte:r", "Arbeiter:in", "Lehrling") }, { key: "mitarbeiterart", label: "Mitarbeiterart", alle: "Alle Mitarbeiterarten", optionen: werte(...MITARBEITERARTEN) }, { key: "kv", label: "Kollektivvertrag", alle: "Alle Kollektivverträge", optionen: werte("Handel", "Süßwaren") }, { key: "paygrade", label: "Hay-Grade", alle: "Alle Hay-Grades", optionen: werte(...HAY_GRADE_WERTE) }, { key: "source", label: "Intern/Extern", alle: "Alle", optionen: werte("Intern", "Extern") }, { key: "gender", label: "Geschlecht", alle: "Alle", optionen: [ { wert: "m", label: "männlich" }, { wert: "w", label: "weiblich" }, ], }, { key: "dienstwagenArt", label: "Antriebsart Dienstwagen", alle: "Alle Antriebsarten", optionen: werte("Verbrenner", "Elektro") }, { key: "absence", label: "Art der Langzeitabwesenheit", alle: "Alle Arten", optionen: werte(...ABSENCE_TYPES) }, { key: "schutzGrund", label: "Personenkreis Kündigungsschutz", alle: "Alle Personenkreise", optionen: werte(...KUENDIGUNGSSCHUTZ_GRUENDE), }, // Die Beendigungsart als Auswertungskriterium — für die Fluktuation nach // Grund. Die Austrittsart (freiwillig/unfreiwillig) steht daneben und wird // aus derselben Angabe abgeleitet, nicht zweitgespeichert. { key: "beendigung", label: "Beendigungsart", alle: "Alle Beendigungsarten", optionen: werte(...BEENDIGUNGSART_WERTE) }, // Eigene Spalte, nicht aus der Beendigungsart abgeleitet — siehe // lib/beendigung.ts. Der Wert steht in der Datenbank genauso da; nur die // Beschriftung kommt von hier. { key: "austrittsart", label: "Freiwillig oder unfreiwillig", alle: "Alle Austrittsarten", optionen: AUSTRITTSARTEN.map((a) => ({ wert: a, label: AUSTRITTSART_LABELS[a] })), }, { key: "teilzeitArt", label: "Teilzeitvariante", alle: "Alle Teilzeitvarianten", optionen: werte("Bildungsteilzeit", "Elternteilzeit", "Pflegeteilzeit", "Wiedereingliederungsteilzeit"), }, { key: "weekday", label: "Arbeitet am", alle: "Beliebiger Wochentag", optionen: werte("Mo", "Di", "Mi", "Do", "Fr", "Sa", "So") }, ] as const; /** Kriterien über ein Ja/Nein-Feld. */ export type JaNeinKriterium = { key: string; label: string }; export const JANEIN_KRITERIEN: readonly JaNeinKriterium[] = [ { key: "dienstwagen", label: "Dienstwagen" }, { key: "betriebsrat", label: "Betriebsrat" }, { key: "lateral", label: "Laterale Führung" }, { key: "clevel", label: "C-Level" }, { key: "kuendigungsschutz", label: "Besonderer Kündigungsschutz" }, { key: "behindert", label: "Begünstigt behindert" }, { key: "teilzeit", label: "In einer Teilzeitvariante" }, { key: "aufenthaltstitel", label: "Aufenthaltstitel" }, { key: "dependents", label: "Angehörige erfasst" }, ] as const; /** Zeiträume: je ein Parameter `From` und `To`. */ export type ZeitraumKriterium = { key: string; label: string }; export const ZEITRAUM_KRITERIEN: readonly ZeitraumKriterium[] = [ { key: "entry", label: "Eintritt" }, { key: "exit", label: "Austritt" }, { key: "birth", label: "Geburtsdatum" }, { key: "schutzAb", label: "Kündigungsschutz ab" }, { key: "schutz", label: "Kündigungsschutz bis" }, { key: "behinderungAb", label: "Bescheid Behinderung ab" }, { key: "behinderungBis", label: "Bescheid Behinderung bis" }, { key: "teilzeitEnde", label: "Teilzeit endet" }, { key: "titelEnde", label: "Aufenthaltstitel läuft ab" }, ] as const; export type Criteria = { /** Werteliste je Kriteriumsschlüssel aus AUSWAHL_KRITERIEN. */ auswahl: Record; /** Ja/Nein je Schlüssel aus JANEIN_KRITERIEN. */ jaNein: Record; /** ISO-Daten je Schlüssel aus ZEITRAUM_KRITERIEN, jeweils von/bis. */ zeitraum: Record; /** Wochenstunden von/bis. */ stundenVon?: number; stundenBis?: number; }; export const LEERE_CRITERIA: Criteria = { auswahl: {}, jaNein: {}, zeitraum: {} }; function parseZahl(value: string | null | undefined): number | undefined { if (!value) return undefined; const n = Number(value.replace(",", ".")); // Negative Stunden oder eine Woche mit 200 Stunden sind keine Eingabe, // sondern ein Tippfehler oder ein Versuch. return Number.isFinite(n) && n >= 0 && n <= 168 ? n : undefined; } /** * Liest die Kriterien aus der Adresszeile und verwirft alles, was nicht in * den Listen oben steht. */ export function parseCriteria(get: (key: string) => string | null | undefined): Criteria { const auswahl: Record = {}; for (const k of AUSWAHL_KRITERIEN) { const roh = get(k.key); if (roh && k.optionen.some((o) => o.wert === roh)) auswahl[k.key] = roh; } const jaNein: Record = {}; for (const k of JANEIN_KRITERIEN) { const roh = get(k.key); if (roh === "ja" || roh === "nein") jaNein[k.key] = roh; } const zeitraum: Record = {}; for (const k of ZEITRAUM_KRITERIEN) { const von = parseIsoDateParam(get(`${k.key}From`)); const bis = parseIsoDateParam(get(`${k.key}To`)); if (von || bis) zeitraum[k.key] = { von, bis }; } return { auswahl, jaNein, zeitraum, stundenVon: parseZahl(get("hoursFrom")), stundenBis: parseZahl(get("hoursTo")) }; } /** Wie viele Kriterien gesetzt sind — für die Anzeige „(3)" am Aufklapper. */ export function anzahlKriterien(c: Criteria): number { let n = Object.keys(c.auswahl).length + Object.keys(c.jaNein).length; for (const z of Object.values(c.zeitraum)) n += (z.von ? 1 : 0) + (z.bis ? 1 : 0); if (c.stundenVon !== undefined) n += 1; if (c.stundenBis !== undefined) n += 1; return n; } /** * Die gesetzten Kriterien in Worten. * * Die Exportkarten nannten nur den Status. Die übrigen Kriterien stehen * eingeklappt weiter oben und wirken auf jeden Export dieser Seite genauso — * wer eines gesetzt und vergessen hatte, lud eine Datei herunter, die nach dem * ganzen Haus aussah und einen Ausschnitt enthielt. Bei einer Datei für ein * Zielsystem merkt das niemand mehr, sobald sie dort eingespielt ist: dort * fehlen dann Personen, und es sieht aus wie ein Datenstand, nicht wie ein * Filter. * * `anzahlKriterien` daneben zählt dasselbe; hier steht, *was* es ist. */ export function beschreibeKriterien(c: Criteria): string[] { const teile: string[] = []; for (const k of AUSWAHL_KRITERIEN) { const wert = c.auswahl[k.key]; if (!wert) continue; teile.push(`${k.label}: ${k.optionen.find((o) => o.wert === wert)?.label ?? wert}`); } for (const k of JANEIN_KRITERIEN) { const wert = c.jaNein[k.key]; if (wert) teile.push(`${k.label}: ${wert}`); } for (const k of ZEITRAUM_KRITERIEN) { const z = c.zeitraum[k.key]; if (!z) continue; if (z.von && z.bis) teile.push(`${k.label} ${fmtDate(z.von)} bis ${fmtDate(z.bis)}`); else if (z.von) teile.push(`${k.label} ab ${fmtDate(z.von)}`); else if (z.bis) teile.push(`${k.label} bis ${fmtDate(z.bis)}`); } const { stundenVon: von, stundenBis: bis } = c; if (von !== undefined && bis !== undefined) teile.push(`Wochenstunden ${von} bis ${bis}`); else if (von !== undefined) teile.push(`Wochenstunden ab ${von}`); else if (bis !== undefined) teile.push(`Wochenstunden bis ${bis}`); return teile; } /** Alle Parameternamen, die zu den Kriterien gehören — zum Zurücksetzen. */ export function kriterienParameter(): string[] { return [ ...AUSWAHL_KRITERIEN.map((k) => k.key), ...JANEIN_KRITERIEN.map((k) => k.key), ...ZEITRAUM_KRITERIEN.flatMap((k) => [`${k.key}From`, `${k.key}To`]), "hoursFrom", "hoursTo", ]; } /** * Die zwei Kriterien, die keine Spalte sind: der Wochentag steckt in einem * Array, die Angehörigen in einer anderen Tabelle. Beide werden an den * geladenen Zeilen geprüft, statt den Enum-Vergleich bzw. einen Join in die * Abfrage zu zwingen. */ export function passtImSpeicher( zeile: { work_days: Weekday[] | string[]; dependentsCount: number }, c: Criteria ): boolean { const tag = c.auswahl.weekday; if (tag && !zeile.work_days.includes(tag as Weekday)) return false; const angehoerige = c.jaNein.dependents; if (angehoerige === "ja" && zeile.dependentsCount === 0) return false; if (angehoerige === "nein" && zeile.dependentsCount > 0) return false; return true; }