update ui changes

This commit is contained in:
2026-09-28 16:35:34 +05:30
parent 88f658fba9
commit f6d81ceaa8
87 changed files with 7157 additions and 1237 deletions

View File

@@ -0,0 +1,224 @@
'use client';
import {use} from 'react';
import {VStack, HStack} from '@astryxdesign/core/Layout';
import {Grid} from '@astryxdesign/core/Grid';
import {Badge} from '@astryxdesign/core/Badge';
import {Button} from '@astryxdesign/core/Button';
import {BreadcrumbItem, Breadcrumbs} from '@astryxdesign/core/Breadcrumbs';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {MetricCard} from '@/shared/components/patterns/MetricCard';
import {ChartCard} from '@/shared/components/charts/ChartCard';
import {AreaChartView} from '@/shared/components/charts/AreaChartView';
import {BarChartView} from '@/shared/components/charts/BarChartView';
import {LineChartView} from '@/shared/components/charts/LineChartView';
import {HeatmapGrid} from '@/shared/components/charts/HeatmapGrid';
import {AsyncBoundary} from '@/shared/components/data/AsyncBoundary';
import {SkeletonMetricGrid} from '@/shared/components/patterns/LoadingState';
import {ActivityTimeline} from '@/features/dashboard/components/ActivityTimeline';
import {useMetricColumns} from '@/features/dashboard/components/KpiRow';
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
import {useStore} from '@/features/stores/hooks/useStores';
import {
useDashboardActivity,
useDashboardPeakHours,
useDashboardTimeseries,
useSampleKpis,
} from '@/features/dashboard/hooks/useDashboard';
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {ICONS} from '@/shared/utils/icons';
import {SampleScope} from '@/shared/mocks/SampleScope';
import {SampleNotice} from '@/shared/mocks/SampleNotice';
import {
formatCompact,
formatDayLabel,
formatInrCompact,
formatPct,
} from '@/shared/utils/format';
import type {StoreStatus} from '@/features/stores/types/store';
const STATUS: Record<
StoreStatus,
{label: string; variant: 'success' | 'warning' | 'error'}
> = {
open: {label: 'Open', variant: 'success'},
maintenance: {label: 'Maintenance', variant: 'warning'},
closed: {label: 'Closed', variant: 'error'},
};
/**
* One store's dashboard.
*
* Deliberately the SAME chart components as the main dashboard, scoped by the
* route's storeId rather than the shared scope's. Building store-specific chart
* variants would double the surface area and guarantee the two drift apart —
* the only thing that differs here is which store the query asks about.
*/
export default function StoreDetailPage({
params,
}: {
params: Promise<{storeId: string}>;
}) {
const {storeId} = use(params);
const {range} = useWorkspace();
const scope = {range, storeId};
const store = useStore(storeId);
const kpis = useSampleKpis(scope);
const series = useDashboardTimeseries(scope);
const peak = useDashboardPeakHours(scope);
const activity = useDashboardActivity(scope);
const columns = useMetricColumns();
const name = store.data?.name ?? 'Store';
// SAMPLE — every figure on this page reads shared/mocks/sampleData.ts; the
// platform reports no per-store analytics. A real shop id is not a sample
// store, so it gets the not-found state rather than invented numbers.
if (store.data === null) {
return (
<EmptyPanel
icon="stores"
title="Store not found"
description="Your shops and their cameras are under Store → Live cameras."
/>
);
}
return (
<SampleScope>
<VStack gap={5}>
<Breadcrumbs variant="supporting">
<BreadcrumbItem href="/stores">Store</BreadcrumbItem>
<BreadcrumbItem isCurrent>{name}</BreadcrumbItem>
</Breadcrumbs>
<PageHeader
title={name}
description={
store.data
? `${store.data.staffCount} staff · ${formatPct(store.data.conversionPct)} conversion`
: 'Loading store…'
}
// Store scope is the route here, so only the period is selectable.
controls={<ScopeControls hasStore={false} />}
actions={
<HStack gap={2} vAlign="center">
{store.data ? (
<Badge
variant={STATUS[store.data.status].variant}
label={STATUS[store.data.status].label}
/>
) : null}
<Button
variant="secondary"
size="sm"
label="All stores"
href="/stores"
/>
</HStack>
}
/>
<SampleNotice>
The platform does not report per-store sales, staffing or trends yet.
This page shows an example of the store view; your real shops and
cameras are on the Store page.
</SampleNotice>
<AsyncBoundary
resource={kpis}
loading={<SkeletonMetricGrid columns={columns} />}
>
{(rows) => (
<Grid columns={columns} gap={4}>
{rows.map((k) => (
<MetricCard
key={k.id}
label={k.label}
value={k.value}
format={k.unit === 'inr' ? formatInrCompact : formatCompact}
icon={
k.id === 'visitors'
? ICONS.visitors
: k.id === 'purchases'
? ICONS.purchases
: k.id === 'revenue'
? ICONS.revenue
: ICONS.activeRewards
}
deltaPct={k.deltaPct}
isRiseGood={k.isRiseGood}
trend={k.trend}
/>
))}
</Grid>
)}
</AsyncBoundary>
<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="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'}]}
reference={{
y: 22,
label: 'Network benchmark',
tone: 'positive',
}}
/>
)}
</ChartCard>
<ChartCard
title="Peak hours"
subtitle="Where this store's week actually lands"
resource={peak}
height={200}
>
{(d) => <HeatmapGrid data={d} />}
</ChartCard>
</Grid>
<ActivityTimeline resource={activity} limit={6} height={285} />
</VStack>
</SampleScope>
);
}

View File

@@ -0,0 +1,61 @@
'use client';
import {VStack} from '@astryxdesign/core/Layout';
import {Breadcrumbs, BreadcrumbItem} from '@astryxdesign/core/Breadcrumbs';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {AsyncBoundary} from '@/shared/components/data/AsyncBoundary';
import {SkeletonCardGrid} from '@/shared/components/patterns/LoadingState';
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
import {useSites} from '@/features/stores/hooks/useSites';
import {ShopSection} from '@/features/stores/components/ShopSection';
/**
* Live cameras — the estate, from GET /api/sites.
*
* Reached from the "Live cameras" button on the Store page. Every shop, its
* cameras, and the shop PC that watches them: live view, health check,
* enrolment and camera management all live here, so the Store page itself can
* open on performance.
*
* Health fields are nullable and rendered as "—" when the platform does not
* report them. A deployment that sends no camera health is not a deployment
* with zero cameras up, and printing "0/0" for "not reported" makes a working
* estate look broken.
*/
export default function LiveCamerasPage() {
const sites = useSites();
return (
<VStack gap={5}>
<Breadcrumbs variant="supporting">
<BreadcrumbItem href="/stores">Store</BreadcrumbItem>
<BreadcrumbItem isCurrent>Live cameras</BreadcrumbItem>
</Breadcrumbs>
<PageHeader
title="Live cameras"
description="Every shop, its cameras, and the shop PC that watches them."
/>
<AsyncBoundary
resource={sites}
loading={<SkeletonCardGrid count={4} height={170} />}
empty={
<EmptyPanel
icon="stores"
title="No stores yet"
description="Shops appear here once they are registered on the platform."
/>
}
>
{(rows) => (
<VStack gap={6}>
{rows.map((site) => (
<ShopSection key={site.id} site={site} />
))}
</VStack>
)}
</AsyncBoundary>
</VStack>
);
}

View File

@@ -1,50 +1,55 @@
'use client';
import {VStack} from '@astryxdesign/core/Layout';
import {Button} from '@astryxdesign/core/Button';
import {Icon} from '@astryxdesign/core/Icon';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {AsyncBoundary} from '@/shared/components/data/AsyncBoundary';
import {SkeletonCardGrid} from '@/shared/components/patterns/LoadingState';
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
import {useSites} from '@/features/stores/hooks/useSites';
import {ShopSection} from '@/features/stores/components/ShopSection';
import {StoreGrid} from '@/features/stores/components/StoreGrid';
import {useStoreList} from '@/features/stores/hooks/useStores';
import {SampleScope} from '@/shared/mocks/SampleScope';
import {SampleNotice} from '@/shared/mocks/SampleNotice';
import {ICONS} from '@/shared/utils/icons';
/**
* The estate, from GET /api/sites.
* The Store page opens on performance; the cameras are one click away.
*
* Health fields are nullable and rendered as "—" when the platform does not
* report them. A deployment that sends no camera health is not a deployment
* with zero cameras up, and printing "0/0" for "not reported" makes a working
* estate look broken.
* Shops, cameras, live view and the shop PC moved to /stores/cameras behind
* the "Live cameras" action, so a merchant checking how stores are doing is
* not scrolling past camera management to get there.
*
* The performance grid is SAMPLE: the platform reports no per-store revenue,
* staffing or trend, so these cards (and the /stores/[storeId] pages they
* open) read fixtures. The cameras page is real.
*/
export default function StoresPage() {
const sites = useSites();
const performance = useStoreList();
return (
<VStack gap={5}>
<PageHeader
title="Store"
description="Every shop, its cameras, and the shop PC that watches them."
/>
<AsyncBoundary
resource={sites}
loading={<SkeletonCardGrid count={4} height={170} />}
empty={
<EmptyPanel
icon="stores"
title="No stores yet"
description="Shops appear here once they are registered on the platform."
description="Visitors, sales and staffing for every store."
actions={
<Button
variant="primary"
size="sm"
label="Live cameras"
icon={<Icon icon={ICONS.cameras} />}
href="/stores/cameras"
/>
}
>
{(rows) => (
<VStack gap={6}>
{rows.map((site) => (
<ShopSection key={site.id} site={site} />
))}
</VStack>
)}
</AsyncBoundary>
/>
<SampleScope>
<VStack gap={5}>
<SampleNotice>
The platform does not report per-store sales, staffing or trends
yet, so these store cards are examples. Your real shops and cameras
are under Live cameras.
</SampleNotice>
<StoreGrid resource={performance} />
</VStack>
</SampleScope>
</VStack>
);
}