Let the truncated Anstehend list open
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m35s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m14s

"… und 5 weitere Ereignisse in diesem Zeitraum" was a sentence to read.
It is now a button: click it and the rest unfolds in place, click again
and the card goes back to eight rows.

Until now the only way to see the remaining entries was to narrow the
period or the kinds — which changes the question rather than the answer.

This happens in the browser, not through the URL, unlike the period and
kind filters. Those go through the address because a longer period brings
rows into play that were never loaded; here every entry in the period is
already on the page and the eight was purely presentational. A round trip
would mean waiting for data that is already there, plus a history entry
for something nobody wants to go back to.

The list moved into its own component so the state has somewhere to live.
KIND_LABEL and the item type moved with it, since they only describe this
list.

The button only appears when there is something to unfold, and once open
it offers the way back — otherwise the card stays long for the rest of the
session because somebody looked once.

Verified: 10 tests, five mutation-checked (ignoring the state, dropping
the way back, showing the button with nothing to unfold, losing the
singular, dropping aria-expanded each turn them red). 487 tests,
typecheck and build clean. Not seen in a browser — login goes through the
company account and the database is unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 20:20:19 +02:00
parent 99b1df9735
commit eb25369d1d
3 changed files with 219 additions and 68 deletions

View File

@@ -0,0 +1,106 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { fmtDate } from "@/lib/format";
export const KIND_LABEL = {
hire: "Eintritt",
exit: "Austritt",
return: "Rückkehr aus Abwesenheit",
note: "Wiedervorlage",
} as const;
export type AnstehendEintrag = {
id: string;
/** Ziel des Klicks — bei einer Wiedervorlage die Akte, nicht die Notiz. */
employeeId: string;
label: string;
/** Zweite Zeile: bei einer Wiedervorlage der Notiztext statt der Art. */
hinweis?: string;
date: string;
kind: keyof typeof KIND_LABEL;
};
/** Wie viele Zeilen die Karte zeigt, bevor aufgeklappt wird. */
export const VORSCHAU = 8;
/**
* Die Karte bleibt eine Übersicht — acht Zeilen, und darunter steht, wie
* viele es insgesamt sind. Neu ist, dass diese Zeile ein Knopf ist: wer alle
* sehen will, klappt sie auf, statt den Zeitraum enger zu stellen und die
* Antwort damit zu verändern.
*
* Das geschieht im Browser und nicht über die Adresse, anders als bei
* Zeitraum und Art: die Einträge sind bereits alle geladen, es gibt nichts
* nachzuholen. Ein Umweg über den Server brächte eine Wartezeit für Daten,
* die schon da sind — und einen Eintrag im Verlauf für etwas, das niemand
* zurückgehen will.
*/
export function AnstehendListe({
eintraege,
today,
leerText,
}: {
eintraege: AnstehendEintrag[];
today: string;
leerText: string;
}) {
const [alle, setAlle] = useState(false);
const sichtbar = alle ? eintraege : eintraege.slice(0, VORSCHAU);
const weitere = eintraege.length - sichtbar.length;
return (
<>
<ul className="flex flex-col divide-y divide-border-subtle">
{sichtbar.map((item) => {
// Überfällig gibt es nur bei Wiedervorlagen: die anderen Arten
// haben eine untere Grenze, eine offene Aufgabe nicht.
const ueberfaellig = item.date < today;
return (
<li key={`${item.kind}-${item.id}`}>
<Link
href={`/employees/${item.employeeId}`}
className="-mx-2 flex items-center justify-between gap-2 rounded px-2 py-2.5 text-sm hover:bg-surface"
>
<span className="min-w-0">
<span className="block truncate font-semibold text-ink">{item.label}</span>
<span className="block truncate text-xs text-ink-muted">
{KIND_LABEL[item.kind]}
{item.hinweis ? ` — ${item.hinweis}` : ""}
</span>
</span>
<span
className={`shrink-0 text-xs font-semibold tabular-nums ${
ueberfaellig ? "text-danger-text" : "text-ink-muted"
}`}
>
{ueberfaellig ? "überfällig " : ""}
{fmtDate(item.date)}
</span>
</Link>
</li>
);
})}
{eintraege.length === 0 && <p className="py-2 text-sm text-ink-muted">{leerText}</p>}
</ul>
{/* Der Knopf steht nur da, wenn es etwas zu holen gibt. Ist die Liste
aufgeklappt, führt er wieder zurück — sonst bliebe die Karte für den
Rest der Sitzung lang, auch wenn jemand nur einmal nachsehen
wollte. */}
{(weitere > 0 || alle) && (
<button
type="button"
onClick={() => setAlle((v) => !v)}
aria-expanded={alle}
className="mt-2 rounded text-xs font-semibold text-brand-700 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
>
{alle
? "Weniger anzeigen"
: `… und ${weitere} ${weitere === 1 ? "weiteres Ereignis" : "weitere Ereignisse"} anzeigen`}
</button>
)}
</>
);
}