/** * record-history.tsx * * — chronological timeline of all ActivityEvents for a single * record. Most recent event first. Each event is shown as a collapsible card * with actor, relative date, and the inline. * * Intended to be embedded in a record dialog tab ("Historial"). * * Transport-agnostic: events and column metadata arrive via props. No fetching. */ import * as React from 'react' import { formatDistanceToNow } from 'date-fns' import { es, enUS } from 'date-fns/locale' import { ChevronDown, ChevronRight, Clock, ExternalLink } from 'lucide-react' import { cn } from '@asteby/metacore-ui/lib' import { Avatar, AvatarFallback, AvatarImage, Badge, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives' import { getInitials } from '@asteby/metacore-ui/lib' import type { ColumnDefinition } from './types' import type { ActivityEvent } from './activity-diff' import { ActivityDiff } from './activity-diff' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface RecordHistoryProps { /** * All activity events for the record, in any order. The component sorts * them chronologically (most recent first). */ events: ActivityEvent[] /** * Column metadata for the record's model. Forwarded to so * field labels and display types are resolved correctly. */ columns?: ColumnDefinition[] /** IANA timezone for datetime cells. */ timeZone?: string /** ISO 4217 currency for money cells. */ currency?: string /** BCP-47 locale. Defaults to 'es'. */ locale?: string /** Class applied to the root element. */ className?: string /** * When provided, each event header shows an "open in activity log" button * that invokes this with the event — the host navigates to its activity * detail page (e.g. `/activity/:id`). Omitted → no button. */ onOpenEvent?: (event: ActivityEvent) => void /** * Resolves an event's `actor_avatar` storage path to a fetchable URL * (e.g. ops' `getStorageUrl(path, 'avatars')`). Identity when omitted — * fine for absolute same-origin paths. */ resolveAvatarUrl?: (path: string) => string /** * Localized label for the badge on each event header (e.g. "Clientes"). * Falls back to the event's raw `addon_key` when omitted. */ moduleLabel?: string } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const ACTION_LABELS: Record = { created: 'Creó el registro', create: 'Creó el registro', updated: 'Actualizó el registro', update: 'Actualizó el registro', deleted: 'Eliminó el registro', delete: 'Eliminó el registro', } function actionLabel(action: string): string { return ACTION_LABELS[action.toLowerCase()] ?? action } const ACTION_DOT_COLOR: Record = { created: '#22c55e', create: '#22c55e', updated: '#eab308', update: '#eab308', deleted: '#ef4444', delete: '#ef4444', } function actionDotColor(action: string): string { return ACTION_DOT_COLOR[action.toLowerCase()] ?? '#6b7280' } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- /** * Shows the full activity history of a single record as a vertical timeline. * Each event is collapsible — the header shows actor + time; expanding reveals * the with field-level changes. */ export const RecordHistory: React.FC = ({ events, columns, timeZone, currency, locale = 'es', className, onOpenEvent, resolveAvatarUrl, moduleLabel, }) => { const dateLocale = locale === 'en' ? enUS : es // Sort: most recent first const sorted = React.useMemo( () => [...events].sort((a, b) => new Date(b.occurred_at).getTime() - new Date(a.occurred_at).getTime()), [events], ) // Expand the most-recent event by default const [openIds, setOpenIds] = React.useState>(() => sorted.length > 0 ? new Set([sorted[0].id]) : new Set(), ) const toggle = (id: string) => { setOpenIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } if (sorted.length === 0) { return (

Sin historial de cambios.

) } return (
{/* Vertical line */}
) }