Files
alpenwerk-hr/components/employees/HistorieLoeschen.tsx
Maximilian Stubhan 08d2740690 Let planned changes be taken back and corrected too
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>
2026-08-13 21:35:51 +02:00

146 lines
5.4 KiB
TypeScript

"use client";
import { Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { deleteHistoryEntry } from "@/actions/employees";
import { Button } from "@/components/ui/Button";
import { Modal } from "@/components/ui/Modal";
import { useToast } from "@/components/ui/Toast";
import { fmtDate } from "@/lib/format";
import type { Vorschau } from "@/lib/history";
// Löschen mit Ansage.
//
// Bestätigen heisst hier nicht „Wirklich?" — das beantwortet jede Person nach
// dem dritten Mal blind mit Ja. Der Dialog sagt stattdessen, **was danach
// anders ist**: welches Feld auf welchen Wert zurückgeht, und welches nicht,
// weil eine spätere Änderung es erneut angefasst hat. Wer das liest, merkt
// selbst, ob er den richtigen Eintrag erwischt hat.
export function HistorieLoeschen({
historyId,
employeeId,
bezeichnung,
datum,
vorschau,
istZukunft,
}: {
historyId: string;
employeeId: string;
bezeichnung: string;
datum: string;
vorschau: Vorschau[];
/** Noch nicht wirksam — dann wird der geplante Vorgang entschärft. */
istZukunft: boolean;
}) {
const [offen, setOffen] = useState(false);
const [laeuft, setLaeuft] = useState(false);
const { showToast } = useToast();
const router = useRouter();
const zurueck = vorschau.filter((v) => !v.bleibt);
const bleibt = vorschau.filter((v) => v.bleibt);
async function loeschen() {
setLaeuft(true);
const ergebnis = await deleteHistoryEntry({ history_id: historyId, employee_id: employeeId });
setLaeuft(false);
if (ergebnis.success) {
showToast("Eintrag gelöscht.");
setOffen(false);
router.refresh();
} else {
showToast(ergebnis.error ?? "Löschen fehlgeschlagen.", "error");
}
}
return (
<>
<button
type="button"
onClick={() => setOffen(true)}
aria-label={`${bezeichnung} vom ${fmtDate(datum)} löschen`}
className="rounded p-1 text-ink-muted hover:bg-danger-bg hover:text-danger-text
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<Modal
open={offen}
onClose={() => setOffen(false)}
title="Eintrag löschen"
footer={
<>
<Button variant="ghost" onClick={() => setOffen(false)}>
Abbrechen
</Button>
<Button onClick={loeschen} pending={laeuft} className="!bg-danger-solid text-white hover:brightness-110">
Löschen und zurücksetzen
</Button>
</>
}
>
<p className="text-sm text-ink-body">
<strong className="text-ink">{bezeichnung}</strong> {istZukunft ? "zum" : "vom"} {fmtDate(datum)} wird aus der
Historie entfernt.
</p>
{istZukunft && (
<div className="mt-4">
<h3 className="text-xs font-bold uppercase tracking-wide text-ink-muted">Wird nicht mehr passieren</h3>
<ul className="mt-1.5 flex flex-col gap-1">
{vorschau.map((v) => (
<li key={v.feld} className="text-sm text-ink-body">
<span className="font-semibold text-ink">{v.feld}</span>{" "}
<span className="text-ink-muted">sollte auf</span> <span className="text-ink">{v.von || "leer"}</span>{" "}
<span className="text-ink-muted">gesetzt werden</span>
</li>
))}
</ul>
<p className="mt-2 text-xs text-ink-muted">
An den Stammdaten ändert sich nichts — die Änderung war noch nicht wirksam. Betrifft der geplante Vorgang
noch weitere Felder, läuft er mit diesen weiter.
</p>
</div>
)}
{!istZukunft && zurueck.length > 0 && (
<div className="mt-4">
<h3 className="text-xs font-bold uppercase tracking-wide text-ink-muted">Wird zurückgesetzt</h3>
<ul className="mt-1.5 flex flex-col gap-1">
{zurueck.map((v) => (
<li key={v.feld} className="text-sm text-ink-body">
<span className="font-semibold text-ink">{v.feld}</span>{" "}
<span className="text-ink-muted line-through decoration-ink-muted/40">{v.von || "leer"}</span>{" "}
<span aria-hidden="true">→</span> <span className="text-ink">{v.auf || "leer"}</span>
</li>
))}
</ul>
</div>
)}
{!istZukunft && bleibt.length > 0 && (
<div className="mt-4">
<h3 className="text-xs font-bold uppercase tracking-wide text-ink-muted">Bleibt unverändert</h3>
<ul className="mt-1.5 flex flex-col gap-1">
{bleibt.map((v) => (
<li key={v.feld} className="text-sm text-ink-muted">
<span className="font-semibold">{v.feld}</span> — eine spätere Änderung hat dieses Feld erneut
gesetzt, und die gilt weiter.
</li>
))}
</ul>
</div>
)}
<p className="mt-4 rounded bg-surface px-3 py-2 text-xs text-ink-muted">
Der Vorgang wird im Protokoll festgehalten — mit Zeitpunkt, Person und den Werten der gelöschten Zeile. Die
Zeile selbst lässt sich nicht wiederherstellen.
</p>
</Modal>
</>
);
}