Deleting and correcting a history entry stopped at the present: anything not yet effective stayed put. That was not a principle, it was a missing link. A planned change lives as a payload in pending_org_changes, and nothing tied it to the history row — only a person and a date, and the data already holds an Eintritt and a Vertragsänderung sharing one. So employee_history now carries pending_id, set by change_employee_data when it schedules something. One planned change can carry two history rows: Stammdaten and Vertrag are kept apart but scheduled together. Taking one back therefore strips only that group's fields from the payload, and cancels the operation only when nothing is left. Correcting one rewrites its group and the effective date, and touches no employee data — the change has not happened yet. An entry stays on its side of the present. Pulling a planned change into today, or pushing an effective one into the future, would mean adjusting the employee record and the pending payload in opposite directions; that is what the real operations are for. Existing rows were linked where exactly one running operation matched the person and date and no other row had claimed it. All five of them matched. Anything ambiguous would have kept the old refusal, which now says the actual reason. The edit dialog surfaced a bug in useDialogFocus that predates it: the effect depended on the identity of onClose, which almost every caller rebuilds on render, so it re-ran after each keystroke and its cleanup pulled focus back to whatever opened the dialog. Any dialog with a text field would have accepted one character. It never showed because until now no dialog kept its own state next to its own onClose. Rehearsed against real data: a two-row planned change corrected, one row taken back with the operation continuing on the rest, the second taken back with the operation cancelled, and both refusals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
4.4 KiB
TypeScript
99 lines
4.4 KiB
TypeScript
import { HistorieBearbeiten } from "@/components/employees/HistorieBearbeiten";
|
||
import { HistorieLoeschen } from "@/components/employees/HistorieLoeschen";
|
||
import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle";
|
||
import { actionBadgeStyle } from "@/lib/colors";
|
||
import { fmtDate, todayIso } from "@/lib/format";
|
||
import { darfKorrigiertWerden, loeschVorschau } from "@/lib/history";
|
||
import type { Database } from "@/lib/supabase/types";
|
||
|
||
type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"];
|
||
|
||
// Die Geschichte einer Person — aufklappbar bis auf die Werte, und dort, wo
|
||
// ein Eintrag irrtümlich entstanden ist, auch zurücknehmbar.
|
||
//
|
||
// Vorher stand hier nur „Geänderte Felder: Adresse, Ort". Damit liess sich
|
||
// zwar sehen, *dass* jemand die Anschrift angefasst hat, aber nicht, was
|
||
// vorher dort stand. Die alte Adresse lag allein im Protokoll, und das ist
|
||
// eine andere Seite, nach Zeitpunkt sortiert statt nach Person — man hätte
|
||
// also erst wissen müssen, wonach man sucht.
|
||
//
|
||
// Aufgeklappt wird mit <details>, nicht mit einem Zustand im Browser: die
|
||
// Werte stehen dann schon in der Seite, sind durchsuchbar (Strg+F) und im
|
||
// Ausdruck sichtbar, und es braucht kein Skript dafür.
|
||
//
|
||
// Der Löschknopf erscheint nur an Einträgen, die sich zurücknehmen lassen.
|
||
// An allen anderen steht stattdessen der Grund — leise, aber lesbar. Ein
|
||
// Knopf, der erst nach dem Klick sagt „geht nicht", wäre eine Falle; ein
|
||
// fehlender Knopf ohne Erklärung wäre ein Rätsel.
|
||
|
||
export function HistorieTab({ history, employeeId }: { history: HistoryRow[]; employeeId: string }) {
|
||
const today = todayIso();
|
||
|
||
if (history.length === 0) {
|
||
return <p className="text-sm text-ink-muted">Keine Historieneinträge vorhanden.</p>;
|
||
}
|
||
|
||
return (
|
||
<ul className="flex flex-col divide-y divide-border">
|
||
{history.map((h) => {
|
||
const isFuture = h.event_date > today;
|
||
const changes = h.changes ?? [];
|
||
const urteil = darfKorrigiertWerden(h, today);
|
||
return (
|
||
<li key={h.id} className="py-3">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<span className={`rounded-full px-2 py-0.5 text-xs font-semibold ${actionBadgeStyle(h.event_type)}`}>{h.event_type}</span>
|
||
<span className="text-sm text-ink-muted">{fmtDate(h.event_date)}</span>
|
||
{isFuture && (
|
||
<span className="rounded-full bg-warning-bg px-2 py-0.5 text-xs font-semibold text-warning-text">
|
||
⏱ zukünftig – wirksam ab {fmtDate(h.event_date)}
|
||
</span>
|
||
)}
|
||
{urteil.erlaubt && (
|
||
<span className="ml-auto flex items-center gap-0.5">
|
||
<HistorieBearbeiten
|
||
historyId={h.id}
|
||
employeeId={employeeId}
|
||
bezeichnung={h.event_type}
|
||
datum={h.event_date}
|
||
changes={changes}
|
||
istZukunft={isFuture}
|
||
heute={today}
|
||
/>
|
||
<HistorieLoeschen
|
||
historyId={h.id}
|
||
employeeId={employeeId}
|
||
bezeichnung={h.event_type}
|
||
datum={h.event_date}
|
||
vorschau={loeschVorschau(h, history)}
|
||
istZukunft={isFuture}
|
||
/>
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="mt-1 text-sm text-ink">{h.description}</p>
|
||
|
||
{changes.length > 0 && (
|
||
<details className="group mt-1.5">
|
||
<summary
|
||
className="inline-flex cursor-pointer list-none items-center gap-1 rounded text-xs font-semibold text-brand-700
|
||
hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
|
||
>
|
||
<span className="transition-transform group-open:rotate-90" aria-hidden="true">
|
||
›
|
||
</span>
|
||
{changes.length} {changes.length === 1 ? "Feld" : "Felder"} im Detail
|
||
</summary>
|
||
<div className="mt-2 rounded border border-border bg-surface px-3 py-2">
|
||
<AenderungsTabelle changes={changes} />
|
||
</div>
|
||
{!urteil.erlaubt && <p className="mt-1.5 text-xs text-ink-muted">{urteil.grund}</p>}
|
||
</details>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
);
|
||
}
|