'use client'; import {useState} from 'react'; import {VStack} from '@astryxdesign/core/Layout'; import {Grid} from '@astryxdesign/core/Grid'; import {Button} from '@astryxdesign/core/Button'; import {Icon} from '@astryxdesign/core/Icon'; import {PageHeader} from '@/components/primitives/PageHeader'; import {ScopeControls} from '@/components/scope/ScopeControls'; import {ICONS} from '@/lib/icons'; import {ChartCard} from '@/components/charts/ChartCard'; import {AreaChartView} from '@/components/charts/AreaChartView'; import {LineChartView} from '@/components/charts/LineChartView'; import {BarChartView} from '@/components/charts/BarChartView'; import {HeatmapGrid} from '@/components/charts/HeatmapGrid'; import {KpiRow} from '@/features/dashboard/KpiRow'; import {ActivityTimeline} from '@/features/dashboard/ActivityTimeline'; import {RewardUsageChart} from '@/features/dashboard/RewardUsageChart'; import {StoreComparisonPanel} from '@/features/dashboard/StoreComparison'; import {PerformancePanel} from '@/features/dashboard/PerformancePanel'; import {useResource} from '@/lib/api/useResource'; import {endpoints} from '@/lib/api/client'; import {useWorkspace} from '@/components/shell/WorkspaceProvider'; import {storeName} from '@/lib/mock/stores'; import type {Granularity} from '@/lib/api/contracts'; import { formatCompact, formatDayLabel, formatInrCompact, formatPct, } from '@/lib/format'; /** * An analytics workspace. Charts are the subject, not evidence for a to-do list. * * The page reads top-down as one narrowing question: headline numbers → the two * trends that drive them → the conversion story behind those → when and on what * it happens → the period rollup → which store → what just happened. * * Operational widgets — quick actions, tasks, the AI briefing, top store/reward, * staff status — deliberately do NOT live here. The briefing endpoint they were * built on is still live and feeds the Copilot's AI tab, which is where that * class of content belongs: a panel you open to be told what to do, beside a * dashboard you read to work it out yourself. * * Recent activity keeps its compact form — six rows in a fixed box, full history * on /activity. The uncapped version reached 784px, taller than any chart on the * page, which is an operational log outweighing the analytics it sits among. * * Every panel is scoped by the same {storeId, range} from WorkspaceProvider, set * from this page's header. `series` is fetched once and shared by the four * charts drawn from it, so they cannot disagree. */ /** * Derived from the SERVER's clock, carried on the response meta. * * Calling Date.now() during render is impure and disagrees between the SSR and * hydration passes. Before meta lands there is no instant to reason about, so * the greeting is the time-independent one; both strings are a single line, so * the swap costs no layout shift. getHours() resolves the server's instant in * the viewer's zone, which is the hour the merchant is actually living in. */ function greetingFor(generatedAt?: string): string { if (!generatedAt) return 'Welcome back'; const hour = new Date(generatedAt).getHours(); if (hour < 12) return 'Good morning'; if (hour < 17) return 'Good afternoon'; return 'Good evening'; } export default function DashboardPage() { const {storeId, range} = useWorkspace(); const [granularity, setGranularity] = useState('weekly'); // On by default: store comparison has always been part of the all-stores // dashboard, so Compare starts pressed and switches the panel off, rather // than hiding a panel that used to be there until someone finds the button. const [isComparing, setIsComparing] = useState(true); const scope = {range, storeId}; const kpis = useResource(endpoints.dashboardKpis(scope)); const series = useResource(endpoints.dashboardTimeseries(scope)); const peak = useResource(endpoints.dashboardPeakHours(scope)); const activity = useResource(endpoints.dashboardActivity(scope)); const rewards = useResource(endpoints.dashboardRewardUsage(scope)); const comparison = useResource(endpoints.dashboardStoreComparison(scope)); const isAllStores = storeId === 'all'; const scopeLabel = isAllStores ? 'all stores' : storeName(storeId); return ( {/* 1 + 2 — greeting, title, and the store / period / compare filters. */}