import type { Metadata } from 'next' import Link from 'next/link' import { requirePermission } from '@/lib/auth/current' import { auditActions, auditActors, queryAuditLog, type AuditRow } from '@/lib/audit/query' import { de } from '@/lib/i18n/de' export const metadata: Metadata = { title: de.audit.heading } export const dynamic = 'force-dynamic' type Params = Record /** * Das Zugriffsprotokoll. * * Es beantwortet die eine Frage, die eine Abteilungsleitung an ein Aktensystem * hat: **wer hat was wo getan.** Nach harter Regel 3 aus CLAUDE.md entsteht * die Spur bei jeder schreibenden Aktion und bei jedem Lesezugriff ab Stufe 3, * ohne Ausnahme und ohne Möglichkeit, sie zu unterdrücken. * * Nur für die Leitung. Das folgt aus dem, was drinsteht: eine Zeile nennt * Aktenzeichen und Namen, und schon die Existenz einer Zeile verrät, dass es * einen Vorgang gibt. Ein „gefiltertes Protokoll für alle" wäre ein Weg, die * Sichtbarkeitsregeln von hinten aufzurollen. * * Der Zustand steht in der Adresse, wie bei der Suche: teilbar, und der * Zurück-Knopf tut das Richtige. */ export default async function AuditLogPage({ searchParams, }: { searchParams: Promise }) { await requirePermission('admin') const params = await searchParams const wert = (name: string): string | undefined => { const roh = params[name] const text = (Array.isArray(roh) ? roh[0] : roh)?.trim() return text ? text : undefined } const filter = { actorId: wert('wer'), action: wert('was'), targetType: wert('art'), targetId: wert('ziel'), from: datum(wert('ab')), to: datum(wert('bis'), true), before: wert('vor') ? BigInt(wert('vor')!) : undefined, } const [{ rows, more }, akteure, aktionen] = await Promise.all([ queryAuditLog(filter), auditActors(), auditActions(), ]) const arten = [...new Set(rows.map((zeile) => zeile.targetType))].sort() const gefiltert = Boolean( filter.actorId || filter.action || filter.targetType || filter.targetId || filter.from || filter.to, ) return (

{de.audit.heading}

{de.audit.intro}

{filter.targetType ? ( ) : null} {filter.targetId ? : null} {gefiltert ? ( {de.audit.reset} ) : null}
{arten.length > 1 || filter.targetType ? (
{de.audit.all} {arten.map((art) => ( {de.audit.targets[art] ?? art} ))}
) : null} {rows.length === 0 ? (

{de.audit.empty}

) : (
{rows.map((zeile) => ( ))}
{de.audit.when} {de.audit.who} {de.audit.what} {de.audit.where} {de.audit.clearance}
)} {more && rows.length > 0 ? (
{de.audit.older}
) : null}
) } function Zeile({ zeile, params }: { zeile: AuditRow; params: Params }) { return ( {zeile.at.toLocaleString('de-DE')} {zeile.actorName} {de.audit.actions[zeile.action] ?? zeile.action} {de.audit.targets[zeile.targetType] ?? zeile.targetType} {' '} {zeile.targetHref ? ( {zeile.targetLabel} ) : ( {zeile.targetLabel ?? de.audit.gone} )} {zeile.clearanceAtTime} ) } /** Datum aus dem Formular. „Bis" meint den ganzen Tag, nicht Mitternacht. */ function datum(text: string | undefined, endeDesTages = false): Date | undefined { if (!text) return undefined const wert = new Date(text) if (Number.isNaN(wert.getTime())) return undefined if (endeDesTages) wert.setHours(23, 59, 59, 999) return wert } /** Die eigene Adresse mit geänderten Parametern. */ function adresse(params: Params, aenderungen: Record): string { const such = new URLSearchParams() for (const [name, roh] of Object.entries(params)) { const text = Array.isArray(roh) ? roh[0] : roh if (text) such.set(name, text) } for (const [name, wert] of Object.entries(aenderungen)) { if (wert === undefined) such.delete(name) else such.set(name, wert) } const rest = such.toString() return rest ? `/admin/protokoll?${rest}` : '/admin/protokoll' }