A long-term absence recorded by mistake could only be undone by booking a second event on top of it — leaving two entries in the file, the first of which never happened. Karenz and Rückkehr can now be deleted and corrected like the other entries. For that to restore anything, the two operations first had to start recording what they overwrote. start_karenz and record_karenz_return now keep before/after the way change_employee_data does: status, kind of absence, start, planned return — and for a return also employment type, hours and the part-time variant. Without that there is nothing to revert to, only a sentence. The ordering rule HR asked for is enforced in the database, not just in the UI: an absence cannot be deleted while a later return exists. A return standing on its own would be a return from nothing, and the person's status would derive from an entry whose starting point had been deleted. Delete the return first and the absence frees up. Rehearsed end to end on real data: absence recorded, return recorded on reduced hours; deleting the absence refused; deleting the return put the person back on Karenz with the original hours and the part-time variant cleared; deleting the absence then put them back to Aktiv with no trace. Rows written before today carry no before/after and stay untouchable, with the reason they already gave. Planned absences are refused too — they have their own operation, and their fields have no place in a pending payload, which is why app_feld_karte carries a null group for them rather than a plausible-looking wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4.6 KiB
TypeScript
101 lines
4.6 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 ?? [];
|
||
// Die ganze Historie mitgeben: ob eine Abwesenheit gelöscht werden
|
||
// darf, hängt daran, ob es eine spätere Rückkehr gibt.
|
||
const urteil = darfKorrigiertWerden(h, today, history);
|
||
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>
|
||
);
|
||
}
|