'use client';
import {useMemo} from 'react';
import {Grid} from '@astryxdesign/core/Grid';
import {VStack} from '@astryxdesign/core/Layout';
import {Text} from '@astryxdesign/core/Text';
import type {TableColumn} from '@astryxdesign/core/Table';
import {BarChartView} from '@/shared/components/charts/BarChartView';
import {LineChartView} from '@/shared/components/charts/LineChartView';
import {AreaChartView} from '@/shared/components/charts/AreaChartView';
import {useBreakpoint} from '@/shared/hooks/useBreakpoint';
import {formatCount, formatDayLabel} from '@/shared/utils/format';
import type {IconKey} from '@/shared/utils/icons';
import type {AnalyticsFilters, AreaDateFootfall, FootfallDetail} from '@/features/admin/types/analytics';
import {useFootfallAnalytics} from '@/features/admin/hooks/useAnalytics';
import {
AnalyticsPanel,
ChartLegend,
DetailsTable,
StatPanel,
hasField,
type SortOption,
} from './AnalyticsPanel';
/**
* Footfall: overview → area comparison + daily trend → area × date → details.
*
* Every panel reads its own endpoint through footfallRepository with the same
* filters, so a merchant / area / shop / range change refetches all of them
* and none can show another scope's rows.
*/
export function FootfallDashboard({filters}: {filters: AnalyticsFilters}) {
const bp = useBreakpoint();
const data = useFootfallAnalytics(filters);
const twoUp = bp === 'desktop' || bp === 'ultrawide';
return (
[
{...OVERVIEW_TILES[0], value: formatCount(o.totalFootfall), caption: 'Walk-ins in the period'},
{...OVERVIEW_TILES[1], value: formatCount(Math.round(o.averageDaily)), caption: 'Per day'},
{
...OVERVIEW_TILES[2],
value: o.peakDay ? formatCount(o.peakDay.footfall) : undefined,
caption: o.peakDay ? formatDayLabel(o.peakDay.date) : undefined,
},
{...OVERVIEW_TILES[3], value: formatCount(o.activeLocations), caption: 'Areas with walk-ins'},
{...OVERVIEW_TILES[4], value: formatCount(o.reportingShops), caption: 'Shops that sent data'},
]}
/>
{(rows) => (
b.footfall - a.footfall)}
xKey="area"
series={[{key: 'footfall', label: 'Footfall', format: formatCount}]}
yFormat={formatCount}
/>
)}
{(rows) => (
)}
{(rows) => }
{(rows) => }
);
}
const OVERVIEW_TILES = [
{label: 'Total footfall', icon: 'visitors'},
{label: 'Average daily', icon: 'analytics'},
{label: 'Peak day', icon: 'up'},
{label: 'Active locations', icon: 'stores'},
{label: 'Reporting shops', icon: 'present'},
] as const satisfies {label: string; icon: IconKey}[];
/* ── Area × date ──────────────────────────────────────────────────────── */
const MAX_SERIES = 4;
const OTHER = 'Other';
/**
* Pivots the long rows into one object per date with a key per area. Series
* follow the AREA, ranked by total — never cycled: past four, the rest sum
* into "Other" rather than reusing a grey another area already owns.
*/
function pivot(rows: AreaDateFootfall[]) {
const totals = new Map();
for (const r of rows) totals.set(r.area, (totals.get(r.area) ?? 0) + r.footfall);
const ranked = [...totals.entries()].sort((a, b) => b[1] - a[1]).map(([a]) => a);
const shown = ranked.length > MAX_SERIES + 1 ? ranked.slice(0, MAX_SERIES) : ranked;
const folds = ranked.length > shown.length;
const byDate = new Map>();
for (const r of rows) {
const row = byDate.get(r.date) ?? {date: r.date};
const key = shown.includes(r.area) ? r.area : OTHER;
row[key] = ((row[key] as number | undefined) ?? 0) + r.footfall;
byDate.set(r.date, row);
}
const data = [...byDate.values()].sort((a, b) =>
String(a.date).localeCompare(String(b.date)),
);
return {data, series: folds ? [...shown, OTHER] : shown};
}
function AreaDateChart({rows}: {rows: AreaDateFootfall[]}) {
const {data, series} = useMemo(() => pivot(rows), [rows]);
return (
({key: s, label: s, format: formatCount}))}
xFormat={formatDayLabel}
yFormat={formatCount}
height={300}
/>
);
}
/* ── Details table ────────────────────────────────────────────────────── */
type FootfallRow = FootfallDetail & Record & {rowId: string};
const FOOTFALL_SORTS: SortOption[] = [
{value: 'date-desc', label: 'Newest first', compare: (a, b) => b.date.localeCompare(a.date)},
{value: 'date-asc', label: 'Oldest first', compare: (a, b) => a.date.localeCompare(b.date)},
{value: 'footfall-desc', label: 'Most footfall', compare: (a, b) => b.footfall - a.footfall},
{value: 'shop', label: 'Shop A–Z', compare: (a, b) => a.shopName.localeCompare(b.shopName)},
];
function FootfallTable({rows}: {rows: FootfallDetail[]}) {
const data: FootfallRow[] = rows.map((r) => ({
...r,
rowId: `${r.date}|${r.shopId}`,
}));
const columns: TableColumn[] = [
{key: 'date', header: 'Date', renderCell: (r) => {formatDayLabel(r.date)}},
{key: 'merchantName', header: 'Merchant'},
...(hasField(rows, 'area') ? [{key: 'area', header: 'Area'} as TableColumn] : []),
{key: 'shopName', header: 'Shop'},
{key: 'footfall', header: 'Footfall', align: 'end', renderCell: (r) => {formatCount(r.footfall)}},
...(hasField(rows, 'entries')
? [{key: 'entries', header: 'Entries', align: 'end'} as TableColumn]
: []),
...(hasField(rows, 'exits')
? [{key: 'exits', header: 'Exits', align: 'end'} as TableColumn]
: []),
];
return (
);
}