diff --git a/app/(app)/employees/[id]/page.tsx b/app/(app)/employees/[id]/page.tsx index 1f1201a..eeb8488 100644 --- a/app/(app)/employees/[id]/page.tsx +++ b/app/(app)/employees/[id]/page.tsx @@ -51,6 +51,7 @@ export default async function EmployeeDetailPage({ params }: PageProps) { notes={notes ?? []} locations={orgMaps.locationList} openPositions={openPositions} + today={today} /> ); } diff --git a/app/(app)/employees/page.tsx b/app/(app)/employees/page.tsx index 7849d76..2602d6d 100644 --- a/app/(app)/employees/page.tsx +++ b/app/(app)/employees/page.tsx @@ -205,10 +205,15 @@ export default async function EmployeesPage({ searchParams }: EmployeesPageProps "entry_date", "employment_type", "weekly_hours", - "status", + // Nicht `status`: der Chip leitet ab, wie der Filter es tut — + // siehe components/ui/StatusChip.tsx. Drei Spalten mehr auf + // fünfzehn Zeilen, keine zusätzliche Rundreise. + "exit_date", + "karenz_start_date", + "karenz_return_date", "absence_type", ]) - .$call((q) => sortiere(q, sortFeld, sortRichtung)) + .$call((q) => sortiere(q, sortFeld, sortRichtung, today)) .limit(PAGE_SIZE) .offset((page - 1) * PAGE_SIZE) ).as("rows"), @@ -331,7 +336,7 @@ export default async function EmployeesPage({ searchParams }: EmployeesPageProps {e.employment_type} · {e.weekly_hours}h - + ); diff --git a/components/employees/EmployeeDetail.tsx b/components/employees/EmployeeDetail.tsx index 6c4bd83..34a9975 100644 --- a/components/employees/EmployeeDetail.tsx +++ b/components/employees/EmployeeDetail.tsx @@ -11,6 +11,7 @@ import { fmtFullName, tenure } from "@/lib/format"; import { fortschritt as fortschrittOffboarding, gehoertOffboarding } from "@/lib/offboarding"; import { fortschritt as fortschrittOnboarding } from "@/lib/onboarding"; import type { OpenPositionResolved } from "@/lib/positions"; +import { deriveStatusAsOf } from "@/lib/reports"; import type { Database } from "@/lib/types"; import { DatenAendernPanel } from "./panels/DatenAendernPanel"; import { KarenzPanel } from "./panels/KarenzPanel"; @@ -54,6 +55,8 @@ type EmployeeDetailProps = { notes: NoteRow[]; locations: Location[]; openPositions: OpenPositionResolved[]; + /** Der Tag, zu dem die Akte gelesen wurde — derselbe wie in loadEmployeeDetail. */ + today: string; }; type PanelType = "transfer" | "promote" | "karenz" | "daten" | "terminate" | "rehire" | null; @@ -61,7 +64,7 @@ const ALLE_TABS = ["Stammdaten", "Vertrag", "Organisation", "Onboarding", "Offbo type Tab = (typeof ALLE_TABS)[number]; export function EmployeeDetail(props: EmployeeDetailProps) { - const { employee, placement, breadcrumb, kostenstelle, onboarding, offboarding, manager, formalManager, directReports, history, dependents, notes, locations, openPositions } = props; + const { employee, placement, breadcrumb, kostenstelle, onboarding, offboarding, manager, formalManager, directReports, history, dependents, notes, locations, openPositions, today } = props; const [tab, setTab] = useState("Stammdaten"); const [panel, setPanel] = useState(null); @@ -74,8 +77,15 @@ export function EmployeeDetail(props: EmployeeDetailProps) { const zeigtOffboarding = gehoertOffboarding(employee); const tabs = ALLE_TABS.filter((t) => t !== "Offboarding" || zeigtOffboarding); - const isActive = employee.status === "Aktiv" || employee.status === "Karenz"; - const canEditData = employee.status !== "Ausgetreten"; + // Abgeleitet, nicht aus `employee.status` gelesen. Die Spalte hängt nach, + // wenn ein Austritt mit einem damals künftigen Datum erfasst wurde — es gibt + // keinen Lauf, der sie später nachzieht (Migration 20260814100000). Hier + // entschied sie nicht nur über eine Beschriftung, sondern über die Knöpfe: + // an einer Person, die seit zwei Wochen ausgetreten ist, stand „Austritt" + // weiter zur Verfügung, und „Daten ändern" ebenfalls. + const status = deriveStatusAsOf(employee, today); + const isActive = status === "Aktiv" || status === "Karenz"; + const canEditData = status !== "Ausgetreten"; return (
@@ -92,7 +102,7 @@ export function EmployeeDetail(props: EmployeeDetailProps) {

{fmtFullName(employee.first_name, employee.last_name, employee.title_prefix, employee.title_suffix)}

- +

{placement?.jobTitle ?? employee.job_title}

diff --git a/components/ui/StatusChip.tsx b/components/ui/StatusChip.tsx index 6410ef6..8f96fa1 100644 --- a/components/ui/StatusChip.tsx +++ b/components/ui/StatusChip.tsx @@ -1,19 +1,46 @@ import { absenceLabel } from "@/lib/absence"; import { STATUS_STYLES } from "@/lib/colors"; import { fmtDate } from "@/lib/format"; -import type { EmploymentStatus } from "@/lib/types"; +import { deriveStatusAsOf } from "@/lib/reports"; -type StatusChipProps = { - status: EmploymentStatus; - entryDate?: string | null; // shown as "Eintritt {date}" when status is Geplant +// ── Warum der Status hier abgeleitet und nicht übergeben wird ──────── +// +// Vorher nahm dieses Bauteil einen fertigen Status entgegen, und beide +// Aufrufer reichten `employees.status` durch — die gespeicherte Spalte. Die +// Liste daneben filtert aber über die Datumsspalten (lib/employee-status- +// filter.ts), und sobald die Spalte nachhängt, widersprechen sich Filter und +// Beschriftung: der Filter „Ausgetreten" fand 48 Personen, von denen mehrere +// als „Aktiv" beschriftet waren. +// +// Nachhängen kann sie regelmässig. terminate_employee (Migration +// 20260814100000) setzt die Spalte nur, wenn das Austrittsdatum nicht in der +// Zukunft liegt — „es gibt keinen Lauf, der das später nachzieht" steht dort +// als Kommentar. Ein Austritt, der zum Zeitpunkt der Erfassung noch bevorstand, +// lässt die Spalte also für immer auf „Aktiv" stehen. +// +// Deshalb nimmt das Bauteil jetzt die Zeile und den Stichtag und leitet +// selbst ab. Die Spalte lässt sich nicht mehr hineinreichen: die zweite +// Quelle ist nicht bloss ungenutzt, es gibt sie hier nicht mehr. +export type StatusChipEmployee = { + entry_date: string; + exit_date: string | null; + karenz_start_date: string | null; + karenz_return_date: string | null; /** Shown instead of the generic label when the kind of absence is known. */ - absenceType?: string | null; + absence_type?: string | null; }; -export function StatusChip({ status, entryDate, absenceType }: StatusChipProps) { - // The stored status is still 'Karenz'; absenceLabel maps it to - // "Langzeitabwesenheit", or to the specific kind when one is recorded. - const label = status === "Geplant" && entryDate ? `Eintritt ${fmtDate(entryDate)}` : absenceLabel(status, absenceType); +type StatusChipProps = { + employee: StatusChipEmployee; + /** Der Tag, zu dem der Status gilt — derselbe, nach dem die Seite filtert. */ + asOf: string; +}; + +export function StatusChip({ employee, asOf }: StatusChipProps) { + const status = deriveStatusAsOf(employee, asOf); + // Der abgeleitete Status ist 'Karenz'; absenceLabel macht daraus + // "Langzeitabwesenheit", oder die konkrete Art, wenn eine erfasst ist. + const label = status === "Geplant" ? `Eintritt ${fmtDate(employee.entry_date)}` : absenceLabel(status, employee.absence_type ?? null); return ( derivedStatusFilter(e, ["Geplant"], today) ?? e.val(true)) .where("entry_date", ">=", today) .where("entry_date", "<=", bisIso) .where((e) => e.lit(zeigt("hire"))) @@ -91,6 +96,11 @@ export async function loadDashboardData(tx: Tx, p: DashboardParams) { .selectFrom("employees") .select(["id", "first_name", "last_name", "exit_date"]) .where("exit_date", "is not", null) + // Ein Nichtantritt trägt als Austrittsdatum den Eintrittstag. Liegt + // der in der Zukunft, stand er hier als bevorstehender Austritt — + // vorzubereiten gibt es daran nichts, die Person fängt gar nicht an. + // Dieselbe Überlegung wie bei gehoertOffboarding(). + .where((e) => e(e.ref("exit_date"), ">", e.ref("entry_date"))) .where("exit_date", ">=", today) .where("exit_date", "<=", bisIso) .where((e) => e.lit(zeigt("exit"))) @@ -100,7 +110,7 @@ export async function loadDashboardData(tx: Tx, p: DashboardParams) { eb .selectFrom("employees") .select(["id", "first_name", "last_name", "karenz_return_date"]) - .where("status", "=", "Karenz") + .where((e) => derivedStatusFilter(e, ["Karenz"], today) ?? e.val(true)) .where("karenz_return_date", "is not", null) .where("karenz_return_date", ">=", today) .where("karenz_return_date", "<=", bisIso) diff --git a/lib/employee-sort.ts b/lib/employee-sort.ts index 6c55511..1af061f 100644 --- a/lib/employee-sort.ts +++ b/lib/employee-sort.ts @@ -94,6 +94,29 @@ const bereichAusdruck = sql`( const standortAusdruck = sql`(select name from locations where id = employees.location_id)`; +/** + * Der Status zum Stichtag als Rang — nicht die Spalte `employees.status`. + * + * Die Liste beschriftet jede Zeile mit dem **abgeleiteten** Status + * (components/ui/StatusChip.tsx) und filtert danach + * (lib/employee-status-filter.ts). Nach der gespeicherten Spalte zu sortieren + * hiesse, die Zeilen nach einem Wert zu ordnen, der nirgends auf der Seite + * steht: eine als „Ausgetreten" beschriftete Person landete mitten unter den + * aktiven, weil in ihrer Spalte noch „Aktiv" steht. + * + * Die Reihenfolge ist die des Aufzählungstyps — Aktiv, Karenz, Geplant, + * Ausgetreten. Das ist der Verlauf eines Dienstverhältnisses und sagt mehr + * als alphabetisch. Die Klauseln stehen in derselben Reihenfolge wie in + * deriveStatusAsOf; wer dort etwas ändert, ändert es auch hier. + */ +const statusRang = (asOf: string) => sql`case + when exit_date is not null and (exit_date <= ${asOf}::date or exit_date <= entry_date) then 4 + when entry_date > ${asOf}::date then 3 + when karenz_start_date is not null and karenz_start_date <= ${asOf}::date + and (karenz_return_date is null or karenz_return_date > ${asOf}::date) then 2 + else 1 +end`; + /** * Hängt die Reihenfolge an eine Abfrage über `employees`. * @@ -114,7 +137,8 @@ const standortAusdruck = sql`(select name from locations where id = empl export function sortiere( q: SelectQueryBuilder, feld: Sortierfeld, - richtung: Richtung + richtung: Richtung, + asOf: string ): SelectQueryBuilder { // Zwei ausgeschriebene Zweige statt einer eingesetzten Richtung: so gerät // nichts aus der Adresse in die Abfrage, auch nicht als geprüfter Wert. @@ -140,10 +164,7 @@ export function sortiere( case "beschaeftigung": return nachName(q.orderBy(ordne(sql.ref("employment_type"))).orderBy(ordne(sql.ref("weekly_hours")))); case "status": - // Aufzählungstyp: Postgres ordnet nach der Reihenfolge der Werte — - // Aktiv, Karenz, Geplant, Ausgetreten. Das ist der Verlauf eines - // Dienstverhältnisses und sagt mehr als alphabetisch. - return nachName(q.orderBy(ordne(sql.ref("status")))); + return nachName(q.orderBy(ordne(statusRang(asOf)))); case "name": return nachName(q); } diff --git a/lib/employee-status-filter.ts b/lib/employee-status-filter.ts index 0325d40..cf1881d 100644 --- a/lib/employee-status-filter.ts +++ b/lib/employee-status-filter.ts @@ -13,21 +13,40 @@ import type { EmploymentStatus } from "./types"; // // Kept deliberately close to deriveStatusAsOf, clause for clause: // -// exit_date <= asOf -> Ausgetreten -// entry_date > asOf -> Geplant -// karenz window covers asOf -> Karenz -// otherwise -> Aktiv +// exit_date <= asOf ODER exit_date <= entry_date -> Ausgetreten +// entry_date > asOf -> Geplant +// karenz window covers asOf -> Karenz +// otherwise -> Aktiv // // Die Reihenfolge der ersten beiden ist nicht beliebig: ein abgeschlossener // Austritt schlaegt einen Eintritt, der noch bevorsteht. Warum das der Fall -// ist, steht bei deriveStatusAsOf. +// ist, und warum die erste Klausel zwei Haelften hat, steht bei +// deriveStatusAsOf. // // tests/integration/employee-status-filter.test.ts asserts the two agree // against a real database, which is the only place that can prove it. type Eb = ExpressionBuilder; -/** True once the person has started and has not left yet. */ +/** + * Das Verhältnis ist am Stichtag beendet — entweder weil der Austritt + * vorbei ist, oder weil er nicht später liegt als der Eintritt und es + * damit nie einen Tag Beschäftigung gab (Nichtantritt). + */ +function ausgetreten(eb: Eb, asOf: string): Expression { + return eb.and([ + eb("exit_date", "is not", null), + eb.or([eb("exit_date", "<=", asOf), eb("exit_date", "<=", eb.ref("entry_date"))]), + ]); +} + +/** + * True once the person has started and has not left yet. + * + * Der Nichtantritt braucht hier keine eigene Haelfte: waere exit_date nicht + * groesser als entry_date, muesste zugleich entry_date <= asOf < exit_date <= + * entry_date gelten — das kann keine Zeile erfuellen. + */ function employed(eb: Eb, asOf: string): Expression { return eb.and([eb("entry_date", "<=", asOf), eb.or([eb("exit_date", "is", null), eb("exit_date", ">", asOf)])]); } @@ -45,17 +64,20 @@ export function derivedStatusFilter(eb: Eb, statuses: EmploymentStatus[], asOf: // „Geplant" ist ein Eintritt, der noch bevorsteht **und** nicht // zurueckgenommen wurde. Ohne die zweite Haelfte zaehlte der Filter die - // No-Shows mit: eingestellt, nie erschienen, Austritt vor dem Eintrittstag - // verbucht — in der Liste als „Ausgetreten" ausgewiesen und trotzdem unter - // „Geplant" gefunden. + // Nichtantritte mit: eingestellt, nie erschienen — in der Liste als + // „Ausgetreten" ausgewiesen und trotzdem unter „Geplant" gefunden. + // + // Die zweite Haelfte muss ueber ausgetreten() laufen und nicht nur ueber + // `exit_date > asOf`. Ein Nichtantritt traegt als Austrittsdatum den + // Eintrittstag; liegt der in der Zukunft, ist auch der Austritt groesser + // als der Stichtag und rutschte durch eine Pruefung, die nur den Stichtag + // kennt, wieder hindurch. Genau das war nach der ersten Korrektur noch zu + // sehen: der Filter blieb wirkungslos, weil er den falschen Vergleich zog. if (wanted.size === 1 && wanted.has("Geplant")) { - return eb.and([ - eb("entry_date", ">", asOf), - eb.or([eb("exit_date", "is", null), eb("exit_date", ">", asOf)]), - ]); + return eb.and([eb("entry_date", ">", asOf), eb.not(ausgetreten(eb, asOf))]); } if (wanted.size === 1 && wanted.has("Ausgetreten")) { - return eb.and([eb("exit_date", "is not", null), eb("exit_date", "<=", asOf)]); + return ausgetreten(eb, asOf); } const wantsAktiv = wanted.has("Aktiv"); diff --git a/lib/reports.ts b/lib/reports.ts index ded4237..b93f4c5 100644 --- a/lib/reports.ts +++ b/lib/reports.ts @@ -150,13 +150,28 @@ export type OrgLookups = { // „Geplant" heißt danach genau das, was es heißen soll — ein Eintritt, der // noch bevorsteht und nicht zurückgenommen wurde. // +// ── Warum ein Austritt am Eintrittstag *jeden* Stichtag schlägt ────── +// +// Die erste Fassung dieser Regel prüfte nur `exit_date <= asOf`, und das +// reichte für den Nichtantritt nicht: Migration 20260814100000 setzt bei +// „No Show" das Austrittsdatum **auf den Eintrittstag**. Liegt der noch in +// der Zukunft, ist auch der Austritt in der Zukunft — die Regel fiel durch +// auf „Geplant", und die Liste zeigte jemanden als anstehenden Eintritt, von +// dem längst feststand, dass er nicht kommt. +// +// Die Migration begründet ihr Vorgehen damit, „nie aktiv" folge aus dem +// Datum von selbst. In SQL stimmt das; hier stand der Satz nur als Absicht +// und nicht als Klausel. Er steht jetzt da: endet das Verhältnis nicht +// später, als es beginnt, gab es keinen einzigen Tag Beschäftigung — zu +// keinem Stichtag, auch zu keinem vor dem Eintritt. +// // lib/employee-status-filter.ts bildet dieselbe Reihenfolge in SQL ab; die // beiden müssen Klausel für Klausel zusammenpassen. export function deriveStatusAsOf( e: { entry_date: string; exit_date: string | null; karenz_start_date: string | null; karenz_return_date: string | null }, asOf: string ): EmploymentStatus { - if (e.exit_date && e.exit_date <= asOf) return "Ausgetreten"; + if (e.exit_date && (e.exit_date <= asOf || e.exit_date <= e.entry_date)) return "Ausgetreten"; if (e.entry_date > asOf) return "Geplant"; if (e.karenz_start_date && e.karenz_start_date <= asOf && (!e.karenz_return_date || asOf < e.karenz_return_date)) return "Karenz"; return "Aktiv"; diff --git a/tests/unit/employee-sort.test.ts b/tests/unit/employee-sort.test.ts index ff77a2e..0f0666b 100644 --- a/tests/unit/employee-sort.test.ts +++ b/tests/unit/employee-sort.test.ts @@ -119,8 +119,10 @@ const db = new Kysely({ }, }); +const STICHTAG = "2026-09-15"; + const ordnung = (feld: Sortierfeld, richtung: "asc" | "desc") => - sortiere(db.selectFrom("employees").select("id"), feld, richtung) + sortiere(db.selectFrom("employees").select("id"), feld, richtung, STICHTAG) .compile() .sql.replace(/^.*?order by /s, "") .replace(/\s+/g, " "); @@ -222,9 +224,46 @@ describe("das erzeugte SQL", () => { it("setzt keinen Wert aus der Adresse in die Abfrage", () => { // Die Richtung ist ausgeschrieben, nicht eingesetzt. Kämen je Werte aus // der Adresse hierher, stünden sie im SQL statt als Parameter. + // + // Gebunden wird genau ein Wert, und er kommt nicht aus der Adresse: der + // Stichtag der Statusableitung. Deshalb wird er hier namentlich + // zugelassen und alles andere ausgeschlossen — „gar keine Parameter" + // wäre die schärfere Zusicherung, aber die falsche. for (const f of SORTIERFELDER) { - const { parameters } = sortiere(db.selectFrom("employees").select("id"), f.value, "desc").compile(); - expect(parameters, f.value).toEqual([]); + const { parameters } = sortiere(db.selectFrom("employees").select("id"), f.value, "desc", STICHTAG).compile(); + expect(new Set(parameters), f.value).toEqual(new Set(f.value === "status" ? [STICHTAG] : [])); } }); + + // ── Status: abgeleitet, nicht aus der Spalte gelesen ────────────────── + // + // Die Zeile zeigt den zum Stichtag abgeleiteten Status. Nach + // `employees.status` zu sortieren hiesse, nach einem Wert zu ordnen, der + // nirgends auf der Seite steht — eine als „Ausgetreten" beschriftete Person + // stünde mitten unter den aktiven. + describe("Status", () => { + it("ordnet nach der Ableitung und nicht nach der Spalte", () => { + const sql = ordnung("status", "asc"); + expect(sql).toContain("exit_date"); + expect(sql).toContain("entry_date"); + expect(sql).toContain("karenz_start_date"); + expect(sql).not.toContain('"status"'); + }); + + it("hält die Reihenfolge des Dienstverhältnisses ein", () => { + // Aktiv (1), Karenz (2), Geplant (3), Ausgetreten (4) — dieselbe + // Folge wie im Aufzählungstyp, und aussagekräftiger als alphabetisch. + const sql = ordnung("status", "asc"); + expect(sql.indexOf("then 4")).toBeLessThan(sql.indexOf("then 3")); + expect(sql.indexOf("then 3")).toBeLessThan(sql.indexOf("then 2")); + expect(sql).toContain("else 1"); + }); + + it("erkennt den Nichtantritt wie die Ableitung", () => { + // exit_date <= entry_date: kein einziger Tag Beschäftigung. Fehlte der + // Vergleich, sortierte ein künftiger Nichtantritt unter „Geplant", + // während sein Chip „Ausgetreten" zeigt. + expect(ordnung("status", "asc")).toContain("exit_date <= entry_date"); + }); + }); }); diff --git a/tests/unit/employee-status-filter.test.ts b/tests/unit/employee-status-filter.test.ts index 75c8afc..f935106 100644 --- a/tests/unit/employee-status-filter.test.ts +++ b/tests/unit/employee-status-filter.test.ts @@ -46,9 +46,16 @@ describe("derivedStatusFilter — Geplant", () => { // Die eigentliche Zusicherung: ohne diese Hälfte war der Filter falsch. it("schliesst aus, wessen Austritt schon vollzogen ist", () => { - const sql = bedingung(["Geplant"]); - expect(sql).toContain('"exit_date" is null'); - expect(sql).toContain('"exit_date" >'); + expect(bedingung(["Geplant"])).toContain('"exit_date" <='); + }); + + // Und ohne *diese* Hälfte blieb er wirkungslos. Ein Nichtantritt trägt als + // Austrittsdatum den Eintrittstag; liegt der in der Zukunft, ist auch der + // Austritt grösser als der Stichtag. Ein Vergleich, der nur den Stichtag + // kennt, lässt ihn durch — deshalb muss exit_date auch gegen entry_date + // stehen. + it("vergleicht den Austritt auch mit dem Eintritt", () => { + expect(bedingung(["Geplant"])).toContain('"exit_date" <= "entry_date"'); }); }); @@ -58,6 +65,10 @@ describe("derivedStatusFilter — Ausgetreten", () => { expect(sql).toContain('"exit_date" is not null'); expect(sql).toContain('"exit_date" <='); }); + + it("zählt den Nichtantritt mit, dessen Eintritt noch bevorsteht", () => { + expect(bedingung(["Ausgetreten"])).toContain('"exit_date" <= "entry_date"'); + }); }); describe("derivedStatusFilter — nicht abgedeckte Auswahl", () => { @@ -90,6 +101,41 @@ describe("Filter und Anzeige meinen dasselbe", () => { e: { ...basis, entry_date: "2026-10-01", exit_date: "2026-12-31" }, erwartet: "Geplant", }, + // Der Fall, der nach der ersten Korrektur noch durchrutschte. So und nicht + // anders legt ihn terminate_employee an: bei „No Show" wird das übergebene + // Austrittsdatum verworfen und der Eintrittstag eingesetzt. + { + was: "Nichtantritt: Austritt am Eintrittstag, beides in der Zukunft", + e: { ...basis, entry_date: "2026-10-01", exit_date: "2026-10-01" }, + erwartet: "Ausgetreten", + }, + { + was: "Nichtantritt in der Vergangenheit", + e: { ...basis, entry_date: "2026-08-01", exit_date: "2026-08-01" }, + erwartet: "Ausgetreten", + }, + // Die Gegenprobe: ein einziger Tag Beschäftigung genügt, und es ist kein + // Nichtantritt mehr. Läge die Grenze bei „<" statt „<=", fiele genau + // dieser Fall auf die falsche Seite. + { + was: "ein Tag Beschäftigung, Austritt am Folgetag des Eintritts", + e: { ...basis, entry_date: "2026-08-01", exit_date: "2026-08-02" }, + erwartet: "Ausgetreten", + }, + { + was: "laufendes Verhältnis", + e: { ...basis, entry_date: "2020-04-30", exit_date: null }, + erwartet: "Aktiv", + }, + // Aigner, Christian aus dem Testbestand: Austritt zum 01.09.2026 erfasst, + // in der Spalte `status` steht bis heute „Aktiv", weil kein Lauf sie + // nachzieht. Abgeleitet ist er ausgetreten — und danach richtet sich jetzt + // auch der Chip in der Liste und in der Akte. + { + was: "Austritt liegt zurück, Spalte hängt nach", + e: { ...basis, entry_date: "2020-04-30", exit_date: "2026-09-01" }, + erwartet: "Ausgetreten", + }, ]; for (const { was, e, erwartet } of faelle) {