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>
146 lines
5.4 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|