/** * EntityHistoryPanel — Vertical timeline of entity history entries. * Shows action badges, timestamps, user info, expandable diffs, * per-entry restore, and undo-last-action. */ import React, { useState, useCallback } from 'react'; import clsx from 'clsx'; import { PlusCircle, Pencil, Trash2, RotateCcw, Undo2, ChevronDown, ChevronRight, Clock, User as UserIcon, History, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useEntityHistory, useRestoreFromHistory, useUndoLastAction, type EntityHistoryEntry, } from '../../api/entityHistory'; import { Card } from '../ui/Card'; import { Button } from '../ui/Button'; import { Badge, type BadgeVariant } from '../ui/Badge'; import { HistoryDiff } from './HistoryDiff'; export interface EntityHistoryPanelProps { entityType: string; entityId: string; className?: string; } /** * Map action type to badge variant + icon + label. */ function actionMeta(action: EntityHistoryEntry['action']): { variant: BadgeVariant; icon: React.ReactNode; } { switch (action) { case 'create': return { variant: 'success', icon: }; case 'update': return { variant: 'info', icon: }; case 'delete': return { variant: 'danger', icon: }; default: return { variant: 'secondary', icon: }; } } /** * Format an ISO date string into a human-readable timestamp. */ function formatTimestamp(iso: string): string { const d = new Date(iso); if (isNaN(d.getTime())) return iso; return d.toLocaleString(); } /** * Single timeline entry row — expandable for update diffs. */ function TimelineEntry({ entry, onRestore, restoringId, }: { entry: EntityHistoryEntry; onRestore: (id: string) => void; restoringId: string | null; }) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); const [confirming, setConfirming] = useState(false); const meta = actionMeta(entry.action); const hasChanges = entry.changes && Object.keys(entry.changes).length > 0; const isRestoring = restoringId === entry.id; const handleRestoreClick = useCallback(() => { if (!confirming) { setConfirming(true); return; } onRestore(entry.id); setConfirming(false); }, [confirming, entry.id, onRestore]); const handleCancelConfirm = useCallback(() => { setConfirming(false); }, []); return (
{/* Timeline line + dot */}
{meta.icon}
{/* Content */}
{t(`entityHistory.actions.${entry.action}`, entry.action)} {entry.user_id && ( )}
{/* Expand toggle for update entries with changes */} {entry.action === 'update' && hasChanges && ( )} {/* Expandable diff */} {expanded && hasChanges && (
)} {/* Snapshot before for delete entries */} {entry.action === 'delete' && entry.snapshot_before && (

{t('entityHistory.snapshotBefore', 'Zustand vor Löschung')}

              {JSON.stringify(entry.snapshot_before, null, 2)}
            
)} {/* Restore action */}
{!confirming ? ( ) : ( <> {t('entityHistory.confirmRestore', 'Wirklich wiederherstellen?')} )}
); } /** * Loading skeleton — 3 placeholder entries. */ function LoadingSkeleton() { const { t } = useTranslation(); return (
{[0, 1, 2].map(i => (
))}
); } /** * Empty state. */ function EmptyState() { const { t } = useTranslation(); return (
); } export function EntityHistoryPanel({ entityType, entityId, className }: EntityHistoryPanelProps) { const { t } = useTranslation(); const { data, isLoading, isError, error, refetch } = useEntityHistory(entityType, entityId); const restoreMutation = useRestoreFromHistory(); const undoMutation = useUndoLastAction(); const [restoringId, setRestoringId] = useState(null); const [undoConfirm, setUndoConfirm] = useState(false); const entries = data?.items ?? []; const handleRestore = useCallback( (historyId: string) => { setRestoringId(historyId); restoreMutation.mutate(historyId, { onSettled: () => setRestoringId(null), }); }, [restoreMutation] ); const handleUndo = useCallback(() => { if (!undoConfirm) { setUndoConfirm(true); return; } undoMutation.mutate( { entityType, entityId }, { onSettled: () => setUndoConfirm(false) } ); }, [undoConfirm, undoMutation, entityType, entityId]); const handleUndoCancel = useCallback(() => setUndoConfirm(false), []); const hasHistory = entries.length > 0; return ( {t('entityHistory.confirmUndo', 'Letzte Aktion rückgängig machen?')}
) : ( ) ) : undefined } > {isLoading ? ( ) : isError ? (

{t('entityHistory.loadError', 'Fehler beim Laden des Verlaufs')}

{error instanceof Error ? error.message : String(error)}

) : !hasHistory ? ( ) : (
{entries.map(entry => ( ))}
)} {/* Restore error display */} {restoreMutation.isError && (

{t('entityHistory.restoreError', 'Fehler beim Wiederherstellen')} {restoreMutation.error instanceof Error ? `: ${restoreMutation.error.message}` : ''}

)} {/* Undo error display */} {undoMutation.isError && (

{t('entityHistory.undoError', 'Fehler beim Rückgängigmachen')} {undoMutation.error instanceof Error ? `: ${undoMutation.error.message}` : ''}

)} ); }