Record what a change was, not only which field it touched

The audit log said "Adresse, wirksam ab 30.07.2026". That names the field
and hides the answer: what did it say before? For a personnel record that is
the question the log exists to answer.

Both values are in hand at the moment of the change — v_old holds the row as
it was, the payload holds what is being written. change_employee_data
already compared them to decide whether to mention the field at all, then
dropped them. It now keeps them in audit_log.changes as
[{feld, vorher, nachher}], and derives the old one-line text from the same
array so existing views are unaffected.

Clicking a row opens the detail. Fields with no previous value read "leer"
rather than showing an empty cell, because "was not set" is itself a
statement.

Two honest limits, both stated in the panel rather than left to look like a
bug:

  - Existing entries cannot be enriched. The values were never captured;
    there is nothing to recover.
  - Hire, exit and import record no individual fields, so they show none.

The rewritten function also drops auth.uid() for app_current_user_id(),
which works on either system — one of the last few call sites before #23.

Caught while writing this: my scripted edit of types.ts silently did nothing
and my own check reported success, because the pattern matched
pending_org_changes. Redone with the editor. That is the second time a
regex-driven edit has lied about its result in this project.

Not verified end to end: the migration needs privileges I no longer hold
after the database password was rotated. Until it is applied the audit page
will not load, since it selects a column that does not exist yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 14:54:03 +02:00
parent 3926f1bb80
commit 1271cef879
4 changed files with 356 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import { Suspense } from "react";
import { AuditDetail } from "@/components/audit/AuditDetail";
import { AuditFilters } from "@/components/audit/AuditFilters";
import { CARD_CLASS } from "@/components/ui/Card";
import { Pagination } from "@/components/ui/Pagination";
@@ -54,7 +55,7 @@ export default async function AuditPage({ searchParams }: { searchParams: Promis
const [entries, total] = await Promise.all([
base()
.select(["id", "occurred_at", "actor_name", "action", "target_label", "target_employee_id", "details"])
.select(["id", "occurred_at", "actor_name", "action", "target_label", "target_employee_id", "details", "changes"])
// Nach id als zweitem Kriterium: bei gleichem Zeitstempel wäre die
// Reihenfolge sonst unbestimmt und ein Eintrag könnte auf zwei Seiten
// erscheinen oder auf keiner.
@@ -115,7 +116,9 @@ export default async function AuditPage({ searchParams }: { searchParams: Promis
entry.target_label
)}
</td>
<td className="px-4 py-2.5 text-ink-muted">{entry.details ?? ""}</td>
<td className="px-2 py-1.5">
<AuditDetail eintrag={entry} />
</td>
</tr>
);
})}