update ui changes
This commit is contained in:
224
src/app/(workspace)/stores/[storeId]/page.tsx
Normal file
224
src/app/(workspace)/stores/[storeId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
61
src/app/(workspace)/stores/cameras/page.tsx
Normal file
61
src/app/(workspace)/stores/cameras/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user