"use client"; import { useState, useCallback } from "react"; import Link from "next/link"; import type { Route } from "next"; import { trpc } from "~/lib/trpc/client.js"; // ─── Helpers ──────────────────────────────────────────────────────────────── const ACTION_BADGES: Record = { CREATE: { label: "Create", className: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400" }, UPDATE: { label: "Update", className: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400" }, DELETE: { label: "Delete", className: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400" }, SHIFT: { label: "Shift", className: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400" }, IMPORT: { label: "Import", className: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-400" }, }; function relativeTime(date: Date | string): string { const now = new Date(); const diffMs = now.getTime() - new Date(date).getTime(); const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHr = Math.floor(diffMin / 60); const diffDays = Math.floor(diffHr / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return `${diffMin}m ago`; if (diffHr < 24) return `${diffHr}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return new Date(date).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }); } function formatValue(val: unknown): string { if (val === null || val === undefined) return "(empty)"; if (typeof val === "boolean") return val ? "Yes" : "No"; if (typeof val === "object") return JSON.stringify(val); return String(val); } type DiffEntry = { old: unknown; new: unknown }; type Changes = { before?: Record; after?: Record; diff?: Record; }; function parseChanges(changes: unknown): Changes { if (!changes || typeof changes !== "object") return {}; return changes as Changes; } // ─── Component ────────────────────────────────────────────────────────────── interface EntityHistoryProps { entityType: string; entityId: string; limit?: number; } type AuditEntry = { id: string; entityType: string; entityId: string; action: string; changes: unknown; createdAt: Date | string; source: string | null; entityName: string | null; summary: string | null; user: { id: string; name: string | null; email: string } | null; }; export function EntityHistory({ entityType, entityId, limit = 10 }: EntityHistoryProps) { const [expandedId, setExpandedId] = useState(null); const { data: entries = [], isLoading } = trpc.auditLog.getByEntity.useQuery( { entityType, entityId, limit }, { staleTime: 30_000 }, ) as { data: AuditEntry[]; isLoading: boolean }; const toggleExpand = useCallback((id: string) => { setExpandedId((prev) => (prev === id ? null : id)); }, []); if (isLoading) { return (
); } if (entries.length === 0) { return (
No history recorded yet.
); } return (

Change History

{/* Timeline */}
{/* Vertical line */}
{entries.map((entry) => { const changes = parseChanges(entry.changes); const isExpanded = expandedId === entry.id; const badge = ACTION_BADGES[entry.action] ?? { label: entry.action, className: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" }; return (
{/* Dot */}
{/* Expanded diff */} {isExpanded && changes.diff && Object.keys(changes.diff).length > 0 && (
{Object.entries(changes.diff).map(([field, { old: oldVal, new: newVal }]) => (
{field} {formatValue(oldVal)} {formatValue(newVal)}
))}
)}
); })}
{/* Link to full log */}
View all in Activity Log
); }