Files
alpenwerk-hr/components/audit/AuditDetail.tsx
Maximilian Stubhan 5f50cb97f3 Let the history say what an address was before
HR reported it from testing: change someone's address and their history
shows "Geänderte Felder: Adresse, Ort" — the new address is on the
Stammdaten tab, the old one is nowhere. It was recorded, but only in the
audit log, which is a different page sorted by time and actor rather
than by person. So you had to already know what you were looking for to
find out whether an address had ever changed, let alone what it used to
be.

The field-by-field diff was being built anyway and written to the audit
log. employee_history now carries the same list, and the person's
history renders it as an expandable Feld / Vorher / Nachher table — the
same table the audit log uses, lifted into a shared component so the two
views don't drift into reading differently. It expands with <details>,
so the values are in the page: findable with Ctrl+F, present when
printed, no script involved.

The duplication with audit_log is deliberate. A person's history should
be readable on its own, including after the log is eventually thinned by
a retention rule.

Rows written before today stay without values. They could only be
reconstructed from the audit log, and the link is not reliable — no key,
only a timestamp and a person. Honestly empty beats plausibly wrong.

The migration was generated from the live function definition rather
than retyped, and the diff is four lines: two column lists, two value
lists. It carries a self-check that raises if either insert failed to
pick up the new column, and it was rehearsed inside a rolled-back
transaction against real data first — the probe confirmed the old street
name lands in the history row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:57:10 +02:00

113 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { useState } from "react";
import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle";
import { SlideOver } from "@/components/ui/SlideOver";
import { actionBadgeStyle } from "@/lib/colors";
import type { AuditChange } from "@/lib/supabase/types";
// Eine Protokollzeile zum Aufklappen.
//
// Die Liste zeigt, *dass* etwas geändert wurde; hier steht, *was*. Beides in
// der Tabelle unterzubringen ginge nicht — bei sieben geänderten Feldern
// wäre die Zeile höher als der Bildschirm.
export type AuditEintrag = {
id: string;
occurred_at: string;
actor_name: string;
action: string;
target_label: string;
target_employee_id: string | null;
details: string | null;
changes: AuditChange[] | null;
};
const zeitFormat = new Intl.DateTimeFormat("de-AT", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZone: "Europe/Vienna",
});
export function AuditDetail({ eintrag }: { eintrag: AuditEintrag }) {
const [offen, setOffen] = useState(false);
const anzahl = eintrag.changes?.length ?? 0;
return (
<>
<button
type="button"
onClick={() => setOffen(true)}
aria-haspopup="dialog"
className="w-full rounded px-2 py-1 text-left text-ink-muted hover:bg-brand-50 hover:text-ink
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
>
<span>{eintrag.details ?? "–"}</span>
{anzahl > 0 && (
<span className="ml-2 whitespace-nowrap rounded-full bg-brand-50 px-2 py-0.5 text-[11px] font-semibold text-brand-700">
{anzahl} {anzahl === 1 ? "Feld" : "Felder"}
</span>
)}
</button>
<SlideOver
open={offen}
onClose={() => setOffen(false)}
title={eintrag.target_label}
subtitle={`${eintrag.action} · ${zeitFormat.format(new Date(eintrag.occurred_at))}`}
>
<dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-sm">
<dt className="font-semibold text-ink-muted">Aktion</dt>
<dd>
<span className={`rounded-full px-2 py-0.5 text-[11px] font-semibold ${actionBadgeStyle(eintrag.action)}`}>
{eintrag.action}
</span>
</dd>
<dt className="font-semibold text-ink-muted">Benutzer:in</dt>
<dd className="text-ink">{eintrag.actor_name}</dd>
<dt className="font-semibold text-ink-muted">Zeitpunkt</dt>
<dd className="tabular-nums text-ink">{zeitFormat.format(new Date(eintrag.occurred_at))}</dd>
{eintrag.target_employee_id && (
<>
<dt className="font-semibold text-ink-muted">Objekt</dt>
<dd>
<Link
href={`/employees/${eintrag.target_employee_id}`}
className="rounded font-semibold text-brand-700 hover:underline
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500"
>
{eintrag.target_label}
</Link>
</dd>
</>
)}
</dl>
{eintrag.details && (
<p className="mt-5 rounded-md bg-surface px-3 py-2 text-sm text-ink-body">{eintrag.details}</p>
)}
<h3 className="mt-6 text-sm font-bold text-ink">Geänderte Felder</h3>
{anzahl > 0 ? (
<div className="mt-2">
<AenderungsTabelle changes={eintrag.changes!} />
</div>
) : (
// Kein Aufzählungszeichen für „nichts da“: der Grund ist wichtig,
// damit niemand einen Fehler vermutet.
<p className="mt-2 max-w-prose text-sm text-ink-muted">
Für diesen Eintrag liegen keine Feldwerte vor. Vorgänge wie Eintritt, Austritt oder Import erfassen keine
Einzelfelder — und Einträge von vor der Erweiterung des Protokolls haben nur die Feldnamen behalten, nicht
die Werte. Nachliefern lässt sich das nicht.
</p>
)}
</SlideOver>
</>
);
}