Correct an entry date, and stop returns without an absence

Three things, all from the same screenshot.

The entry date can now be corrected. The Eintritt entry gets an edit
button — date only, no delete, because it is the start of the timeline
and a person without one has no beginning. Unlike every other entry it
needs no recorded before-values: the old date is on the employee row, so
this works on rows written long before any of this existed, which is
exactly the case that matters.

What hangs off that date is checked: no other event may precede it, exit
and absence start may not fall before it, and the first position
assignment moves with it — left behind it would leave days of employment
with no post, or a post with nobody in it. Someone already working
cannot be given a future entry date either; without that check a person
who has been here for years could be turned into a planned entry, and
the status derivation would agree.

That last rule came out of the rehearsal finding a hole: my first probe
picked a person with no other history rows, so the "nothing may precede
it" check had nothing to compare against and a date in 2099 sailed
through.

Second, the screenshot showed two returns from one absence, and the data
confirmed it: one person with two Rückkehr entries and a third still
scheduled, recorded while they were long since active. record_karenz_
return never checked that there was an absence to return from. Now it
does, and it refuses a second scheduled return — which would have
silently overwritten the first on its effective date.

Third, the history is filterable: upcoming versus done, a date range,
and the event types that actually occur in that file. The count of
upcoming items shows without filtering, because "what is coming" is the
usual reason to open the tab at all.

Still not deletable: Versetzung, Beförderung, Austritt, Wiedereintritt,
Reorganisation. Undoing those means restoring position assignments, and
that deserves its own step rather than being tacked onto this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 11:31:41 +02:00
parent d2f4a7aab7
commit 861c47b757
5 changed files with 707 additions and 79 deletions

View File

@@ -234,3 +234,72 @@ describe("HistorieTab", () => {
expect(screen.getByText("Keine Historieneinträge vorhanden.")).toBeInTheDocument();
});
});
// Eine Akte wird meist aus einem von zwei Gründen aufgeschlagen: „was steht
// an" oder „was war damals". Ohne Filter muss man dafür durch alles scrollen,
// und je länger jemand da ist, desto weniger taugt das.
describe("HistorieTab — Filter", () => {
const VERGANGEN = eintrag({ id: "v", event_type: "Versetzung", event_date: "2020-05-01", description: "Versetzung damals" });
const HEUTE_EINTRAG = eintrag({ id: "h", event_type: "Stammdatenänderung", event_date: "2026-03-02", description: "Heute-Eintrag" });
const KUENFTIG = eintrag({ id: "z", event_type: "Versetzung", event_date: "2099-01-01", description: "Versetzung künftig" });
const ALLE = [KUENFTIG, HEUTE_EINTRAG, VERGANGEN];
it("zeigt ohne Filter alles", () => {
render(<HistorieTab history={ALLE} employeeId="e1" />);
expect(screen.getByText("Versetzung damals")).toBeInTheDocument();
expect(screen.getByText("Versetzung künftig")).toBeInTheDocument();
});
it("nennt die Zahl der anstehenden Punkte, auch ohne dass jemand filtert", () => {
render(<HistorieTab history={ALLE} employeeId="e1" />);
expect(screen.getByText("1 anstehend")).toBeInTheDocument();
});
it("blendet mit „Anstehend“ alles Gelaufene aus", async () => {
const user = userEvent.setup();
render(<HistorieTab history={ALLE} employeeId="e1" />);
await user.click(screen.getByRole("button", { name: "Anstehend" }));
expect(screen.getByText("Versetzung künftig")).toBeInTheDocument();
expect(screen.queryByText("Versetzung damals")).not.toBeInTheDocument();
});
it("blendet mit „Gelaufen“ das Anstehende aus", async () => {
const user = userEvent.setup();
render(<HistorieTab history={ALLE} employeeId="e1" />);
await user.click(screen.getByRole("button", { name: "Gelaufen" }));
expect(screen.queryByText("Versetzung künftig")).not.toBeInTheDocument();
expect(screen.getByText("Versetzung damals")).toBeInTheDocument();
});
it("grenzt über den Zeitraum ein", async () => {
const user = userEvent.setup();
render(<HistorieTab history={ALLE} employeeId="e1" />);
await user.type(screen.getByLabelText("Von"), "2026-01-01");
expect(screen.queryByText("Versetzung damals")).not.toBeInTheDocument();
expect(screen.getByText("Heute-Eintrag")).toBeInTheDocument();
});
it("filtert nach Ereignisart", async () => {
const user = userEvent.setup();
render(<HistorieTab history={ALLE} employeeId="e1" />);
await user.click(screen.getByRole("button", { name: "Versetzung", pressed: false }));
expect(screen.getByText("Versetzung damals")).toBeInTheDocument();
expect(screen.queryByText("Heute-Eintrag")).not.toBeInTheDocument();
});
it("sagt bei leerem Ergebnis, wie viele Einträge es überhaupt gibt", async () => {
const user = userEvent.setup();
render(<HistorieTab history={[VERGANGEN]} employeeId="e1" />);
await user.click(screen.getByRole("button", { name: "Anstehend" }));
expect(screen.getByText(/Kein Eintrag passt zu dieser Auswahl/)).toBeInTheDocument();
expect(screen.getByText(/1 Eintrag sind vorhanden|1 Eintrag/)).toBeInTheDocument();
});
it("bietet das Eintrittsdatum zum Bearbeiten an, aber nicht zum Löschen", () => {
// Der Eintritt ist der Anfang der Zeitleiste und bleibt — sein Datum kann
// trotzdem falsch erfasst sein.
render(<HistorieTab history={[eintrag({ event_type: "Eintritt", changes: null })]} employeeId="e1" />);
expect(screen.getByRole("button", { name: /Eintritt vom .* bearbeiten/ })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /löschen/ })).not.toBeInTheDocument();
});
});