The old refusal read: "Diese Abwesenheit ist noch nicht wirksam. Sie muss über den Vorgang selbst abgebrochen werden." There was no such way. The row sat in the file, the scheduled change kept running toward its date, and nothing could stop either one. That is not hypothetical. One person went absent in July, came back in August, and still has a second return booked for the first of September — recorded while they were already working again. The guard added yesterday stops a third from being written; it does not remove the one that exists. Absences are called off whole, not field by field. For a planned contract change the scheduled payload gets the affected fields lifted out of it and runs on with the rest; an absence has no fields in that map, and half an absence is not a thing anyone means. So the whole scheduled change is cancelled, and what it had already noted on the person goes with it: the date they were to be away from, the date they were to come back on. Left behind, the profile would show an absence with no event behind it. If the absence is still running, the return date planned when it began applies again. The link between the row and the scheduled change had to exist first — start_karenz and record_karenz_return now record it. Existing rows get it backfilled, but only where one running change of that kind falls on that person and that day. Where two would match, the row keeps refusing: guessing which process to cancel is worse than refusing to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
163 lines
6.4 KiB
TypeScript
163 lines
6.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);
|
|
// Eine geplante Abwesenheit oder Rückkehr hat keine einzelnen Felder, die
|
|
// sich herausnehmen liessen — sie fällt als Ganzes. Dann ist „Löschen und
|
|
// zurücksetzen" das falsche Wort für das, was der Knopf tut.
|
|
const ganzerVorgang = istZukunft && vorschau.length === 0;
|
|
|
|
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={ganzerVorgang ? "Geplanten Vorgang abbrechen" : "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">
|
|
{ganzerVorgang ? "Vorgang abbrechen" : "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>
|
|
|
|
{ganzerVorgang && (
|
|
<div className="mt-4">
|
|
<h3 className="text-xs font-bold uppercase tracking-wide text-ink-muted">Wird nicht mehr passieren</h3>
|
|
<p className="mt-1.5 text-sm text-ink-body">
|
|
Der Vorgang entfällt ganz — die {bezeichnung} zum {fmtDate(datum)} findet nicht statt. Am Stammsatz wird
|
|
das vorgemerkte Datum mit entfernt.
|
|
</p>
|
|
<p className="mt-2 text-xs text-ink-muted">
|
|
An den heutigen Stammdaten ändert sich nichts: der Vorgang war noch nicht wirksam.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{istZukunft && vorschau.length > 0 && (
|
|
<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>
|
|
</>
|
|
);
|
|
}
|