'use client'; import { useQuery } from '@tanstack/react-query'; import { formatDistanceToNow } from 'date-fns'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { apiFetch } from '@/lib/api/client'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { CardSkeleton } from '@/components/shared/loading-skeleton'; import { usePermissions } from '@/hooks/use-permissions'; import { WidgetErrorBoundary } from './widget-error-boundary'; import { actionVariant, actionVerb, buildDiffLine, humanizeEntityType, } from '@/components/shared/activity-formatting'; interface ActivityItem { id: string; action: string; entityType: string; entityId: string | null; /** Server-resolved human label (client name, yacht name, …) when the * underlying entity still exists. Falls back to the id prefix in the UI. */ label: string | null; userId: string | null; /** Server-resolved actor display name (from user_profiles). When null, * the actor row no longer exists - render falls back to a "Unknown * user" sentinel rather than the raw UUID prefix. */ actorName: string | null; fieldChanged: string | null; /** For user-FK diff rows (assignedTo, ownerId, etc.) the service * already replaces these with display names. Non-user-FK rows pass * through verbatim. */ oldValue: unknown; newValue: unknown; metadata: Record | null; createdAt: string; } function ActionBadge({ action }: { action: string }) { return ( {actionVerb(action)} ); } function ActivityFeedInner() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const { can } = usePermissions(); const canViewAuditLog = can('admin', 'view_audit_log'); const { data, isLoading } = useQuery({ queryKey: ['dashboard', 'activity'], queryFn: () => apiFetch('/api/v1/dashboard/activity'), staleTime: 30_000, retry: 2, }); if (isLoading) { return ; } // A1: permission_denied rows on the activity feed render as a bare // action badge with no entity name (they target `admin.X` with empty // entityId). They're noise for the rep - keep them in the audit log // page but hide them from the dashboard feed. const items = (data ?? []).filter((i) => i.action !== 'permission_denied'); return ( Recent Activity {canViewAuditLog && portSlug ? ( See all ) : null} {items.length === 0 ? (

No recent activity yet - your team's actions (interests created, stages changed, invoices sent) will appear here.

) : (
{items.map((item) => { const diffLine = buildDiffLine(item); return (

{item.label ? ( <> {item.label} {/* M-NEW-2: explicit middle-dot separator. The prior `ml-1.5` was getting collapsed under `truncate` so the label + type rendered as "Test Person 1interest" with no visible space between them. */} · {humanizeEntityType(item.entityType)} ) : ( // No resolvable label - either the entity was // deleted or the type isn't in the server-side // resolver yet. Either way we never expose a // UUID fragment: it reads as noise to the rep // and leaks an internal identifier. {humanizeEntityType(item.entityType)} {item.entityId ? ( (removed) ) : null} )}

{diffLine ? (

{diffLine}

) : null}

{formatDistanceToNow(new Date(item.createdAt), { addSuffix: true })}

); })}
)}
); } export function ActivityFeed() { return ( ); }