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"; import { actionBadgeStyle } from "@/lib/colors"; import { currentUserId } from "@/lib/auth/session"; import { withUser } from "@/lib/db"; const PAGE_SIZE = 25; type SearchParams = { q?: string; action?: string; page?: string }; function pageHref(params: SearchParams, page: number): string { const sp = new URLSearchParams(); if (params.q) sp.set("q", params.q); if (params.action) sp.set("action", params.action); sp.set("page", String(page)); return `/audit?${sp.toString()}`; } // Pinned to Vienna and built once: audit_log.occurred_at is a timestamptz, and // an unpinned formatter renders it in the *server's* zone — UTC in Docker and // on Vercel — so every entry would read an hour or two early for the people // the log is for. const dateTimeFormatter = new Intl.DateTimeFormat("de-AT", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", timeZone: "Europe/Vienna", }); export default async function AuditPage({ searchParams }: { searchParams: Promise }) { const params = await searchParams; const page = Math.max(1, Number(params.page ?? "1") || 1); const { entries, count } = await withUser(await currentUserId(), async (tx) => { const base = () => { let q = tx.selectFrom("audit_log"); if (params.action) q = q.where("action", "=", params.action); if (params.q) { // Als Parameter gebunden statt in die Abfrage geschrieben: die // Zeichen, die in der alten Filtersyntax ausbrechen konnten, haben // hier keine Bedeutung mehr. const like = `%${params.q.trim()}%`; q = q.where((eb) => eb.or([eb("target_label", "ilike", like), eb("details", "ilike", like), eb("actor_name", "ilike", like)]) ); } return q; }; const [entries, total] = await Promise.all([ base() .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. .orderBy("occurred_at", "desc") .orderBy("id", "desc") .limit(PAGE_SIZE) .offset((page - 1) * PAGE_SIZE) .execute(), base() .select(({ fn }) => fn.countAll().as("anzahl")) .executeTakeFirst(), ]); return { entries, count: Number(total?.anzahl ?? 0) }; }); const totalPages = Math.max(1, Math.ceil(count / PAGE_SIZE)); return (

{count} Einträge

{entries.map((entry) => { return ( ); })} {entries.length === 0 && ( )}
Zeitpunkt Benutzer:in Aktion Objekt Details
{dateTimeFormatter.format(new Date(entry.occurred_at))} {entry.actor_name} {entry.action} {entry.target_employee_id ? ( {entry.target_label} ) : ( entry.target_label )}
Keine Einträge gefunden.
pageHref(params, p)} label="Audit-Log" />

Alle Änderungen an Personal-Stammdaten werden automatisch protokolliert und sind unveränderbar.

); }