Files
alpenwerk-hr/lib/dashboard-filter.ts
Maximilian Stubhan 6957b95a97 Let the overview say what "upcoming" means
Sixty days and all three kinds was a guess, and it was the only one on
offer. Payroll cares about next month; the person filling a vacancy
cares about entries and nothing else. The card now takes a period and a
set of kinds.

The choice lives in the address rather than in the browser, because it
has to: the page is built on the server, and ninety days pulls in rows
that were never loaded at sixty. Filtering client-side would silently
cap the answer at whatever the first query happened to fetch. It also
means a filtered overview can be sent to someone and opened again the
same way.

Deselecting every kind returns to all of them. An empty card is not an
answer to a question nobody asked, and the way back would otherwise be
one click further than the way in. The default period and the full set
are absent from the URL instead of written into it, so a shared link
carries only what was actually chosen.

Anything the address cannot be trusted to hold is rejected: an unknown
period falls back to sixty rather than reaching the query, which would
otherwise be an invitation to ask for ten years of rows through a link.

Eight rows still, with a count of what did not fit underneath — this is
an overview, and the employee list is where lists belong.

Not verified in a browser: the built-in preview has no company sign-in,
so the page redirects to the login before it renders. Types, lint and
386 tests pass, and the filter's behaviour is covered directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 18:56:05 +02:00

47 lines
1.9 KiB
TypeScript

// Was auf der Übersicht als „anstehend" gilt — Zeitraum und Art.
//
// Die Auswahl steht in der Adresse, nicht im Browser: die Übersicht wird auf
// dem Server gebaut, und ein Zeitraum von 90 statt 60 Tagen bringt Zeilen ins
// Spiel, die im Browser gar nicht liegen. Nebenbei lässt sich eine so
// eingestellte Seite verschicken und wiederfinden.
export const ZEITRAEUME = [30, 60, 90, 180] as const;
export type Zeitraum = (typeof ZEITRAEUME)[number];
export const STANDARD_ZEITRAUM: Zeitraum = 60;
export const ANSTEHEND_ARTEN = [
{ value: "hire", label: "Eintritt" },
{ value: "exit", label: "Austritt" },
{ value: "return", label: "Rückkehr" },
] as const;
export type AnstehendArt = (typeof ANSTEHEND_ARTEN)[number]["value"];
const ALLE_ARTEN: AnstehendArt[] = ANSTEHEND_ARTEN.map((a) => a.value);
/** Alles, was nicht in der Liste steht, führt auf den Standard zurück. */
export function parseZeitraum(wert: string | undefined): Zeitraum {
const zahl = Number(wert);
return (ZEITRAEUME as readonly number[]).includes(zahl) ? (zahl as Zeitraum) : STANDARD_ZEITRAUM;
}
/**
* Keine Angabe heisst „alle" — nicht „keine". Wer die Übersicht aufruft, ohne
* etwas auszuwählen, will alles sehen; eine leere Karte wäre die falsche
* Antwort auf eine nicht gestellte Frage. Aus demselben Grund führt auch eine
* Auswahl, in der nur Unbekanntes steht, zurück auf alle.
*/
export function parseArten(wert: string | undefined): AnstehendArt[] {
const gewaehlt = (wert ?? "")
.split(",")
.map((s) => s.trim())
.filter((s): s is AnstehendArt => (ALLE_ARTEN as string[]).includes(s));
return gewaehlt.length > 0 ? gewaehlt : ALLE_ARTEN;
}
/** Steht in der Adresse etwas anderes als der Standard? */
export function istEingeschraenkt(zeitraum: Zeitraum, arten: AnstehendArt[]): boolean {
return zeitraum !== STANDARD_ZEITRAUM || arten.length !== ALLE_ARTEN.length;
}