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>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { AnstehendFilter } from "@/components/dashboard/AnstehendFilter";
|
||||
import { DraftsCard } from "@/components/dashboard/DraftsCard";
|
||||
import { Card, CARD_CLASS, CardTitle } from "@/components/ui/Card";
|
||||
import { actionBadgeStyle } from "@/lib/colors";
|
||||
import { istEingeschraenkt, parseArten, parseZeitraum } from "@/lib/dashboard-filter";
|
||||
import { addDaysIso, fmtDate, fmtName, todayIso } from "@/lib/format";
|
||||
import { divisionOf, loadOrgMaps } from "@/lib/org";
|
||||
import { loadPlacements } from "@/lib/placement";
|
||||
@@ -39,7 +42,11 @@ const DOT_STYLES: Record<string, string> = {
|
||||
|
||||
const KIND_LABEL = { hire: "Eintritt", exit: "Austritt", return: "Rückkehr aus Abwesenheit" } as const;
|
||||
|
||||
export default async function DashboardPage() {
|
||||
export default async function DashboardPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ tage?: string; arten?: string }>;
|
||||
}) {
|
||||
// Built as strings, not by round-tripping a local Date through
|
||||
// toISOString(): in any positive-offset zone new Date(year, 0, 1) is still
|
||||
// the previous year in UTC, which shifted the whole YTD window a day early
|
||||
@@ -48,7 +55,15 @@ export default async function DashboardPage() {
|
||||
const year = today.slice(0, 4);
|
||||
const yearStart = `${year}-01-01`;
|
||||
const yearEnd = `${year}-12-31`;
|
||||
const in60Iso = addDaysIso(today, 60);
|
||||
|
||||
// Der Vorschauzeitraum ist einstellbar, und mit ihm, was überhaupt geladen
|
||||
// wird. Deshalb steht die Auswahl in der Adresse und nicht im Browser: 90
|
||||
// statt 60 Tage bringt Zeilen ins Spiel, die sonst nirgends lägen.
|
||||
const params = await searchParams;
|
||||
const zeitraum = parseZeitraum(params.tage);
|
||||
const arten = parseArten(params.arten);
|
||||
const bisIso = addDaysIso(today, zeitraum);
|
||||
const zeigt = (art: "hire" | "exit" | "return") => arten.includes(art);
|
||||
|
||||
const userId = await currentUserId();
|
||||
|
||||
@@ -121,30 +136,39 @@ export default async function DashboardPage() {
|
||||
loadOrgMaps(tx),
|
||||
loadPlacements(tx, { asOf: today }),
|
||||
|
||||
tx
|
||||
// Abgewählte Arten werden gar nicht erst gelesen — die Karte zeigt sie
|
||||
// ohnehin nicht, und eine Abfrage, deren Ergebnis niemand ansieht, ist
|
||||
// eine Abfrage zu viel.
|
||||
zeigt("hire")
|
||||
? tx
|
||||
.selectFrom("employees")
|
||||
.select(["id", "first_name", "last_name", "entry_date"])
|
||||
.where("status", "=", "Geplant")
|
||||
.where("entry_date", ">=", today)
|
||||
.where("entry_date", "<=", in60Iso)
|
||||
.execute(),
|
||||
.where("entry_date", "<=", bisIso)
|
||||
.execute()
|
||||
: Promise.resolve([]),
|
||||
|
||||
tx
|
||||
zeigt("exit")
|
||||
? tx
|
||||
.selectFrom("employees")
|
||||
.select(["id", "first_name", "last_name", "exit_date"])
|
||||
.where("exit_date", "is not", null)
|
||||
.where("exit_date", ">=", today)
|
||||
.where("exit_date", "<=", in60Iso)
|
||||
.execute(),
|
||||
.where("exit_date", "<=", bisIso)
|
||||
.execute()
|
||||
: Promise.resolve([]),
|
||||
|
||||
tx
|
||||
zeigt("return")
|
||||
? tx
|
||||
.selectFrom("employees")
|
||||
.select(["id", "first_name", "last_name", "karenz_return_date"])
|
||||
.where("status", "=", "Karenz")
|
||||
.where("karenz_return_date", "is not", null)
|
||||
.where("karenz_return_date", ">=", today)
|
||||
.where("karenz_return_date", "<=", in60Iso)
|
||||
.execute(),
|
||||
.where("karenz_return_date", "<=", bisIso)
|
||||
.execute()
|
||||
: Promise.resolve([]),
|
||||
|
||||
tx
|
||||
.selectFrom("employee_history as h")
|
||||
@@ -202,7 +226,7 @@ export default async function DashboardPage() {
|
||||
const maxDivisionCount = Math.max(1, ...divisionBars.map((d) => d.count));
|
||||
|
||||
type UpcomingItem = { id: string; label: string; date: string; kind: keyof typeof KIND_LABEL };
|
||||
const upcoming: UpcomingItem[] = [
|
||||
const upcomingAlle: UpcomingItem[] = [
|
||||
...(upcomingHires).map((e) => ({
|
||||
id: e.id,
|
||||
label: fmtName(e.first_name, e.last_name),
|
||||
@@ -221,9 +245,13 @@ export default async function DashboardPage() {
|
||||
date: e.karenz_return_date!,
|
||||
kind: "return" as const,
|
||||
})),
|
||||
]
|
||||
.sort((a, b) => a.date.localeCompare(b.date))
|
||||
.slice(0, 8);
|
||||
].sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// Die Karte bleibt eine Übersicht, keine Liste: acht Zeilen, und darunter
|
||||
// steht, wie viele es insgesamt sind. Wer alle sehen will, filtert enger
|
||||
// oder geht in die Mitarbeiterliste.
|
||||
const upcoming = upcomingAlle.slice(0, 8);
|
||||
const weitere = upcomingAlle.length - upcoming.length;
|
||||
|
||||
// Each tile links to the view that shows what it counts, with the filters
|
||||
// pre-applied.
|
||||
@@ -308,7 +336,12 @@ export default async function DashboardPage() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardTitle className="mb-1">Anstehend (60 Tage)</CardTitle>
|
||||
<CardTitle className="mb-2">Anstehend ({zeitraum} Tage)</CardTitle>
|
||||
{/* useSearchParams braucht eine Suspense-Grenze; ohne sie fällt beim
|
||||
Bauen die ganze Seite auf Rendern zur Laufzeit zurück. */}
|
||||
<Suspense fallback={<div className="mb-2 h-9" />}>
|
||||
<AnstehendFilter zeitraum={zeitraum} arten={arten} />
|
||||
</Suspense>
|
||||
<ul className="flex flex-col divide-y divide-border-subtle">
|
||||
{upcoming.map((item) => (
|
||||
<li key={`${item.kind}-${item.id}`}>
|
||||
@@ -324,8 +357,19 @@ export default async function DashboardPage() {
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
{upcoming.length === 0 && <p className="py-2 text-sm text-ink-muted">Keine anstehenden Ereignisse.</p>}
|
||||
{upcoming.length === 0 && (
|
||||
<p className="py-2 text-sm text-ink-muted">
|
||||
{istEingeschraenkt(zeitraum, arten)
|
||||
? "Zu dieser Auswahl steht nichts an."
|
||||
: "Keine anstehenden Ereignisse."}
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
{weitere > 0 && (
|
||||
<p className="mt-2 text-xs text-ink-muted">
|
||||
… und {weitere} {weitere === 1 ? "weiteres Ereignis" : "weitere Ereignisse"} in diesem Zeitraum.
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
|
||||
70
components/dashboard/AnstehendFilter.tsx
Normal file
70
components/dashboard/AnstehendFilter.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { SegmentedControl } from "@/components/ui/SegmentedControl";
|
||||
import { actionBadgeStyle } from "@/lib/colors";
|
||||
import {
|
||||
ANSTEHEND_ARTEN,
|
||||
STANDARD_ZEITRAUM,
|
||||
ZEITRAEUME,
|
||||
type AnstehendArt,
|
||||
type Zeitraum,
|
||||
} from "@/lib/dashboard-filter";
|
||||
|
||||
// Die Auswahl wandert in die Adresse; die Seite baut sich damit neu. Das ist
|
||||
// hier nötig und nicht bloss ordentlich: ein längerer Zeitraum bringt Zeilen
|
||||
// ins Spiel, die vorher gar nicht geladen waren.
|
||||
//
|
||||
// router.replace statt push, damit der Zurück-Knopf nicht durch jede einzelne
|
||||
// Filterstellung zurückläuft, und ohne Sprung nach oben — die Karte steht in
|
||||
// der unteren Hälfte, und dorthin sieht gerade, wer hier klickt.
|
||||
|
||||
export function AnstehendFilter({ zeitraum, arten }: { zeitraum: Zeitraum; arten: AnstehendArt[] }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
function setzen(key: string, wert: string | null) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (wert) params.set(key, wert);
|
||||
else params.delete(key);
|
||||
const query = params.toString();
|
||||
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
||||
}
|
||||
|
||||
function artUmschalten(art: AnstehendArt) {
|
||||
const alle = ANSTEHEND_ARTEN.map((a) => a.value);
|
||||
const naechste = arten.includes(art) ? arten.filter((a) => a !== art) : [...arten, art];
|
||||
// Nichts ausgewählt heisst wieder alles: eine leere Karte ist keine
|
||||
// Antwort, und der Weg dorthin wäre ein Klick zu weit.
|
||||
setzen("arten", naechste.length === 0 || naechste.length === alle.length ? null : naechste.join(","));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<SegmentedControl<string>
|
||||
value={String(zeitraum)}
|
||||
onChange={(v) => setzen("tage", v === String(STANDARD_ZEITRAUM) ? null : v)}
|
||||
options={ZEITRAEUME.map((t) => ({ value: String(t), label: `${t} Tage` }))}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{ANSTEHEND_ARTEN.map((a) => {
|
||||
const aktiv = arten.includes(a.value);
|
||||
return (
|
||||
<button
|
||||
key={a.value}
|
||||
type="button"
|
||||
onClick={() => artUmschalten(a.value)}
|
||||
aria-pressed={aktiv}
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 ${
|
||||
aktiv ? actionBadgeStyle(a.label) : "bg-surface text-ink-muted hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
{a.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
lib/dashboard-filter.ts
Normal file
46
lib/dashboard-filter.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
}
|
||||
98
tests/components/AnstehendFilter.test.tsx
Normal file
98
tests/components/AnstehendFilter.test.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AnstehendFilter } from "@/components/dashboard/AnstehendFilter";
|
||||
|
||||
// Die Auswahl muss in die Adresse, nicht in den Browser-Zustand: die
|
||||
// Übersicht wird auf dem Server gebaut, und ein längerer Zeitraum bringt
|
||||
// Zeilen ins Spiel, die im Browser gar nicht liegen. Geprüft wird deshalb
|
||||
// genau das — was in der Adresse landet.
|
||||
|
||||
const replace = vi.fn();
|
||||
let suchString = "";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ replace }),
|
||||
usePathname: () => "/",
|
||||
useSearchParams: () => new URLSearchParams(suchString),
|
||||
}));
|
||||
|
||||
const ALLE = ["hire", "exit", "return"] as const;
|
||||
|
||||
beforeEach(() => {
|
||||
replace.mockClear();
|
||||
suchString = "";
|
||||
});
|
||||
|
||||
describe("AnstehendFilter", () => {
|
||||
it("schreibt einen gewählten Zeitraum in die Adresse", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AnstehendFilter zeitraum={60} arten={[...ALLE]} />);
|
||||
await user.click(screen.getByRole("button", { name: "90 Tage" }));
|
||||
expect(replace).toHaveBeenCalledWith("/?tage=90", { scroll: false });
|
||||
});
|
||||
|
||||
it("lässt den Standard aus der Adresse verschwinden statt ihn hineinzuschreiben", async () => {
|
||||
// Sonst trüge jede geteilte Adresse einen Filter, den niemand gesetzt hat.
|
||||
const user = userEvent.setup();
|
||||
suchString = "tage=90";
|
||||
render(<AnstehendFilter zeitraum={90} arten={[...ALLE]} />);
|
||||
await user.click(screen.getByRole("button", { name: "60 Tage" }));
|
||||
expect(replace).toHaveBeenCalledWith("/", { scroll: false });
|
||||
});
|
||||
|
||||
it("nimmt eine abgewählte Art aus der Auswahl", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AnstehendFilter zeitraum={60} arten={[...ALLE]} />);
|
||||
await user.click(screen.getByRole("button", { name: "Austritt" }));
|
||||
expect(replace).toHaveBeenCalledWith("/?arten=hire%2Creturn", { scroll: false });
|
||||
});
|
||||
|
||||
it("nimmt eine wieder hinzugewählte Art auf", async () => {
|
||||
const user = userEvent.setup();
|
||||
suchString = "arten=hire";
|
||||
render(<AnstehendFilter zeitraum={60} arten={["hire"]} />);
|
||||
await user.click(screen.getByRole("button", { name: "Rückkehr" }));
|
||||
expect(replace).toHaveBeenCalledWith("/?arten=hire%2Creturn", { scroll: false });
|
||||
});
|
||||
|
||||
it("führt die letzte abgewählte Art auf alle zurück, nicht auf keine", async () => {
|
||||
// Eine leere Karte ist keine Antwort, und der Weg zurück wäre sonst ein
|
||||
// Klick zu weit.
|
||||
const user = userEvent.setup();
|
||||
suchString = "arten=hire";
|
||||
render(<AnstehendFilter zeitraum={60} arten={["hire"]} />);
|
||||
await user.click(screen.getByRole("button", { name: "Eintritt" }));
|
||||
expect(replace).toHaveBeenCalledWith("/", { scroll: false });
|
||||
});
|
||||
|
||||
it("lässt vollständige Auswahl gar nicht erst in der Adresse stehen", async () => {
|
||||
const user = userEvent.setup();
|
||||
suchString = "arten=hire,exit";
|
||||
render(<AnstehendFilter zeitraum={60} arten={["hire", "exit"]} />);
|
||||
await user.click(screen.getByRole("button", { name: "Rückkehr" }));
|
||||
expect(replace).toHaveBeenCalledWith("/", { scroll: false });
|
||||
});
|
||||
|
||||
it("behält andere Angaben in der Adresse bei", async () => {
|
||||
const user = userEvent.setup();
|
||||
suchString = "arten=hire";
|
||||
render(<AnstehendFilter zeitraum={60} arten={["hire"]} />);
|
||||
await user.click(screen.getByRole("button", { name: "30 Tage" }));
|
||||
expect(replace).toHaveBeenCalledWith("/?arten=hire&tage=30", { scroll: false });
|
||||
});
|
||||
|
||||
it("zeigt am Knopf an, was gerade gewählt ist", () => {
|
||||
render(<AnstehendFilter zeitraum={60} arten={["hire"]} />);
|
||||
expect(screen.getByRole("button", { name: "Eintritt" })).toHaveAttribute("aria-pressed", "true");
|
||||
expect(screen.getByRole("button", { name: "Austritt" })).toHaveAttribute("aria-pressed", "false");
|
||||
});
|
||||
|
||||
it("springt beim Filtern nicht an den Seitenanfang", async () => {
|
||||
// Die Karte steht in der unteren Hälfte; dorthin sieht gerade, wer klickt.
|
||||
const user = userEvent.setup();
|
||||
render(<AnstehendFilter zeitraum={60} arten={[...ALLE]} />);
|
||||
await user.click(screen.getByRole("button", { name: "30 Tage" }));
|
||||
expect(replace.mock.calls[0][1]).toEqual({ scroll: false });
|
||||
});
|
||||
});
|
||||
73
tests/unit/dashboard-filter.test.ts
Normal file
73
tests/unit/dashboard-filter.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ANSTEHEND_ARTEN,
|
||||
STANDARD_ZEITRAUM,
|
||||
istEingeschraenkt,
|
||||
parseArten,
|
||||
parseZeitraum,
|
||||
} from "@/lib/dashboard-filter";
|
||||
|
||||
// Die Auswahl steht in der Adresse, und dort kann alles stehen: eine
|
||||
// veraltete Verknüpfung, ein Tippfehler, ein abgeschnittener Wert. Sie darf
|
||||
// die Übersicht weder leeren noch mit einem Zeitraum laden, den niemand
|
||||
// gewählt hat.
|
||||
|
||||
describe("parseZeitraum", () => {
|
||||
it("nimmt die angebotenen Zeiträume", () => {
|
||||
expect(parseZeitraum("30")).toBe(30);
|
||||
expect(parseZeitraum("180")).toBe(180);
|
||||
});
|
||||
|
||||
it("fällt ohne Angabe auf den Standard zurück", () => {
|
||||
expect(parseZeitraum(undefined)).toBe(STANDARD_ZEITRAUM);
|
||||
expect(parseZeitraum("")).toBe(STANDARD_ZEITRAUM);
|
||||
});
|
||||
|
||||
it("lässt sich keinen beliebigen Zeitraum unterschieben", () => {
|
||||
// Sonst liesse sich über die Adresse ein Lesevorgang über Jahre auslösen.
|
||||
expect(parseZeitraum("3650")).toBe(STANDARD_ZEITRAUM);
|
||||
expect(parseZeitraum("-5")).toBe(STANDARD_ZEITRAUM);
|
||||
expect(parseZeitraum("60; drop")).toBe(STANDARD_ZEITRAUM);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseArten", () => {
|
||||
const alle = ANSTEHEND_ARTEN.map((a) => a.value);
|
||||
|
||||
it("versteht eine Auswahl", () => {
|
||||
expect(parseArten("hire,exit")).toEqual(["hire", "exit"]);
|
||||
});
|
||||
|
||||
it("liest ohne Angabe alle — eine leere Karte wäre die falsche Antwort", () => {
|
||||
expect(parseArten(undefined)).toEqual(alle);
|
||||
expect(parseArten("")).toEqual(alle);
|
||||
});
|
||||
|
||||
it("verwirft Unbekanntes und behält den Rest", () => {
|
||||
expect(parseArten("hire,unfug")).toEqual(["hire"]);
|
||||
});
|
||||
|
||||
it("führt eine Auswahl aus lauter Unbekanntem auf alle zurück", () => {
|
||||
expect(parseArten("unfug,quatsch")).toEqual(alle);
|
||||
});
|
||||
|
||||
it("stört sich nicht an Leerzeichen", () => {
|
||||
expect(parseArten(" hire , return ")).toEqual(["hire", "return"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("istEingeschraenkt", () => {
|
||||
const alle = ANSTEHEND_ARTEN.map((a) => a.value);
|
||||
|
||||
it("erkennt den unveränderten Zustand", () => {
|
||||
expect(istEingeschraenkt(STANDARD_ZEITRAUM, alle)).toBe(false);
|
||||
});
|
||||
|
||||
it("erkennt einen anderen Zeitraum", () => {
|
||||
expect(istEingeschraenkt(90, alle)).toBe(true);
|
||||
});
|
||||
|
||||
it("erkennt eine engere Auswahl", () => {
|
||||
expect(istEingeschraenkt(STANDARD_ZEITRAUM, ["hire"])).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user