first commit
This commit is contained in:
154
src/app/(workspace)/dashboard/page.tsx
Normal file
154
src/app/(workspace)/dashboard/page.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import {useState} from 'react';
|
||||
import {VStack} from '@astryxdesign/core/Layout';
|
||||
import {Grid} from '@astryxdesign/core/Grid';
|
||||
import {PageHeader} from '@/components/primitives/PageHeader';
|
||||
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';
|
||||
|
||||
/**
|
||||
* The dashboard answers three questions in order, top to bottom:
|
||||
* 1. How is the business performing? → KPI row
|
||||
* 2. What is the shape of that? → footfall / revenue / conversion
|
||||
* 3. What needs attention, and when? → peak hours, rewards, stores, feed
|
||||
*
|
||||
* Every panel is scoped by the same {storeId, range} from WorkspaceProvider,
|
||||
* so changing either control in the top nav re-queries the whole page without
|
||||
* a route change — which is what keeps the Copilot mounted alongside it.
|
||||
*/
|
||||
export default function DashboardPage() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const [granularity, setGranularity] = useState<Granularity>('weekly');
|
||||
|
||||
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 scopeLabel = storeId === 'all' ? 'all stores' : storeName(storeId);
|
||||
|
||||
return (
|
||||
<VStack gap={5}>
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
description={`How the business is performing across ${scopeLabel}, what needs attention, and what to do next.`}
|
||||
/>
|
||||
|
||||
<KpiRow resource={kpis} />
|
||||
|
||||
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
|
||||
<ChartCard title="Footfall" subtitle="Visitors per day" resource={series}>
|
||||
{(d) => (
|
||||
<AreaChartView
|
||||
data={d}
|
||||
xKey="t"
|
||||
xFormat={formatDayLabel}
|
||||
yFormat={formatCompact}
|
||||
series={[{key: 'visitors', label: 'Visitors'}]}
|
||||
/>
|
||||
)}
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard title="Revenue" subtitle="Daily takings" resource={series}>
|
||||
{(d) => (
|
||||
<BarChartView
|
||||
data={d}
|
||||
xKey="t"
|
||||
xFormat={formatDayLabel}
|
||||
yFormat={formatInrCompact}
|
||||
series={[{key: 'revenue', label: 'Revenue'}]}
|
||||
/>
|
||||
)}
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
title="Visitors vs purchases"
|
||||
subtitle="The gap is the conversion opportunity"
|
||||
resource={series}
|
||||
>
|
||||
{(d) => (
|
||||
<LineChartView
|
||||
data={d}
|
||||
xKey="t"
|
||||
xFormat={formatDayLabel}
|
||||
yFormat={formatCompact}
|
||||
series={[
|
||||
{key: 'visitors', label: 'Visitors'},
|
||||
{key: 'purchases', label: 'Purchases'},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
title="Conversion"
|
||||
subtitle="Share of visitors who bought"
|
||||
resource={series}
|
||||
>
|
||||
{(d) => (
|
||||
<LineChartView
|
||||
data={d}
|
||||
xKey="t"
|
||||
xFormat={formatDayLabel}
|
||||
yFormat={(v) => formatPct(v, 0)}
|
||||
series={[{key: 'conversion', label: 'Conversion'}]}
|
||||
// The only semantic colour on this chart: above the benchmark is
|
||||
// good, below is not, and that is what the tint communicates.
|
||||
reference={{y: 22, label: 'Network benchmark', tone: 'positive'}}
|
||||
/>
|
||||
)}
|
||||
</ChartCard>
|
||||
</Grid>
|
||||
|
||||
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
|
||||
<ChartCard
|
||||
title="Peak hours"
|
||||
subtitle="Where the week's footfall actually lands"
|
||||
resource={peak}
|
||||
height={200}
|
||||
>
|
||||
{(d) => <HeatmapGrid data={d} />}
|
||||
</ChartCard>
|
||||
|
||||
<RewardUsageChart resource={rewards} />
|
||||
</Grid>
|
||||
|
||||
<PerformancePanel
|
||||
scope={scope}
|
||||
granularity={granularity}
|
||||
onGranularityChange={setGranularity}
|
||||
/>
|
||||
|
||||
{/* Comparing stores is meaningless when scoped to one of them. */}
|
||||
{storeId === 'all' ? (
|
||||
<StoreComparisonPanel resource={comparison} />
|
||||
) : null}
|
||||
|
||||
<ActivityTimeline resource={activity} />
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user