'use client'; import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets'; import { apiFetch } from '@/lib/api/client'; import { PageHeader } from '@/components/shared/page-header'; import { CustomizeWidgetsMenu } from './customize-widgets-menu'; import { DateRangePicker } from './date-range-picker'; import { WidgetErrorBoundary } from './widget-error-boundary'; import type { DashboardWidget } from './widget-registry'; import { isCustomRange, type DateRange } from '@/lib/analytics/range'; const PRESET_LABELS: Record<'today' | '7d' | '30d' | '90d', string> = { today: 'Today', '7d': 'Last 7 days', '30d': 'Last 30 days', '90d': 'Last 90 days', }; function rangeLabel(range: DateRange): string { if (isCustomRange(range)) { const fmt: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC', }; const from = new Date(`${range.from}T00:00:00.000Z`).toLocaleDateString('en-US', fmt); const to = new Date(`${range.to}T00:00:00.000Z`).toLocaleDateString('en-US', fmt); return `${from} – ${to}`; } return PRESET_LABELS[range]; } interface MeData { data?: { profile?: { firstName?: string | null; displayName?: string | null; }; }; } function timeOfDayGreeting(): string { const hour = new Date().getHours(); if (hour < 5) return 'Up late'; if (hour < 12) return 'Good morning'; if (hour < 18) return 'Good afternoon'; return 'Good evening'; } export function DashboardShell() { const [range, setRange] = useState('30d'); const { visibleWidgets } = useDashboardWidgets(); // Bucket once so the JSX stays readable. Registry order is preserved // inside each bucket, so reordering the registry reorders the render. const charts = visibleWidgets.filter((w) => w.group === 'chart'); const rails = visibleWidgets.filter((w) => w.group === 'rail'); const feed = visibleWidgets.filter((w) => w.group === 'feed'); // Reuses the existing ['me'] cache (5-minute staleTime) populated by // useTablePreferences elsewhere — usually a cache hit, so no extra // request. Falls back to a generic greeting if the profile isn't // available yet so we never block the dashboard render. const me = useQuery({ queryKey: ['me'], queryFn: ({ signal }) => apiFetch('/api/v1/me', { signal }), staleTime: 5 * 60_000, }); const firstName = me.data?.data?.profile?.firstName?.trim(); // Time-aware greeting line, falls back to a generic "Welcome back" when // we don't know the user's first name yet (e.g. profile not filled out). const greeting = firstName ? `${timeOfDayGreeting()}, ${firstName}` : 'Welcome back'; // Use a partial query-key prefix (no range segment) for invalidations. // Reading: "any cached analytics result, regardless of range, please // refetch on this event." This avoids any chance that a custom-range // object literal hashes differently than the one stored in the cache, // and keeps the invalidation surface broad enough to refresh whichever // range the user is currently looking at. useRealtimeInvalidation({ 'interest:stageChanged': [ ['analytics', 'pipeline_funnel'], ['analytics', 'lead_source_attribution'], ['dashboard', 'kpis'], ], 'client:created': [['dashboard', 'kpis']], 'berth:statusChanged': [ ['analytics', 'occupancy_timeline'], ['dashboard', 'kpis'], ], }); return (
{/* Mobile-only greeting strip. The shared PageHeader is hidden below `sm` (its title is normally duplicated by the topbar), so we render the welcome message inline here for mobile — keeps the personalized touch from desktop without polluting the topbar (which stays "Dashboard" for wayfinding). */}

Dashboard

{greeting}

); } /** * Placeholder shown when the rep has hidden every widget. Without this, * the dashboard collapses to just the gradient header strip and looks * like a broken page — this hints at the "Customize" button to bring * widgets back. */ function EmptyDashboardHint() { return (

No widgets on your dashboard yet

Click Customize above to pick which analytics cards appear here.

); } function WidgetCell({ widget, range }: { widget: DashboardWidget; range: DateRange }) { return {widget.render(range)}; }