update the adminpage ui

This commit is contained in:
2026-09-25 16:29:41 +05:30
parent 91e4db8217
commit b36a30385c
168 changed files with 10535 additions and 1008 deletions

View File

@@ -5,6 +5,7 @@ import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {ArrivalsFeed} from '@/features/dashboard/components/ArrivalsFeed';
import {useRecentVisits} from '@/features/dashboard/hooks/useReports';
import {useArrivalStream} from '@/features/dashboard/hooks/useArrivalStream';
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
/**
@@ -14,14 +15,23 @@ import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
* size — one feed implementation, two budgets. The cursor the platform returns
* is the supported way to page further; the dashboard never needs it, so it is
* wired here first when infinite scroll lands.
*
* New arrivals are pushed over GET /api/visits/stream and each one re-reads
* the feed; "Live" shows only while that stream is actually connected.
*/
export default function ActivityPage() {
const visits = useRecentVisits(50);
const {isLive} = useArrivalStream(
visits.refetch,
// Stale rows from the previous store are not a position to resume from.
visits.isRefreshing ? undefined : visits.data?.cursor,
);
const scopeLabel = useScopeLabel();
return (
<VStack gap={5}>
<PageHeader
eyebrow={isLive ? 'Live' : undefined}
title="Activity"
description={`Every recognised arrival across ${scopeLabel}, newest first.`}
controls={<ScopeControls />}

View File

@@ -0,0 +1,24 @@
'use client';
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {CustomerDirectory} from '@/features/customers/components/CustomerDirectory';
/**
* The customer directory, from GET /api/visitors.
*
* Company-wide, not scoped by the store switcher: a customer belongs to the
* business, and somebody who first walked into one branch is the same person
* at another.
*/
export default function CustomersPage() {
return (
<VStack gap={5}>
<PageHeader
title="Customers"
description="Everyone the cameras have recognised — name them, see their visits, record a purchase."
/>
<CustomerDirectory />
</VStack>
);
}

View File

@@ -9,7 +9,7 @@ import {AreaChartView} from '@/shared/components/charts/AreaChartView';
import {BarChartView} from '@/shared/components/charts/BarChartView';
import {KpiRow} from '@/features/dashboard/components/KpiRow';
import {ArrivalsFeed} from '@/features/dashboard/components/ArrivalsFeed';
import {FeatureUnavailable} from '@/shared/components/patterns/FeatureUnavailable';
import {EngagementSection} from '@/features/engagement/components/EngagementSection';
import {CHART} from '@/shared/components/charts/palette';
import {
useConversionReport,
@@ -17,6 +17,7 @@ import {
useFootfallReport,
useRecentVisits,
} from '@/features/dashboard/hooks/useDashboard';
import {useArrivalStream} from '@/features/dashboard/hooks/useArrivalStream';
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
import {greetingFor} from '@/features/dashboard/services/dashboardService';
import {formatCompact, formatInrCompact} from '@/shared/utils/format';
@@ -30,23 +31,27 @@ import {formatCompact, formatInrCompact} from '@/shared/utils/format';
* endpoint, and no fixture: if the platform returns nothing, this page shows
* nothing rather than something plausible.
*
* ── What was removed and why ─────────────────────────────────────────────
* The engagement layer that used to sit here — ten activity types, impact
* chains, campaign funnels, a customer journey, generated insights — was built
* against a loyalty domain the platform does not expose. It rendered numbers
* with no source. Rather than keep them behind a demo flag where a merchant
* could mistake them for real, the panels are replaced by a statement of what
* they need. The layout, spacing and hierarchy are otherwise untouched.
* ── Engagement ───────────────────────────────────────────────────────────
* The engagement layer that used to sit here was generated locally and was
* removed. It is back, read from the platform's own endpoints — activities and
* their impact, campaigns, and the customer journey — so every figure has a
* source, and every attributed one says it is estimated.
*
* Bucket labels from the reports are rendered as STRINGS. They are local wall
* time with no offset; parsing one into a Date re-interprets it in the
* viewer's zone and shifts every label on the axis.
*/
export default function DashboardPage() {
const kpis = useDashboardKpis();
const footfall = useFootfallReport({bucket: 'day'});
const conversion = useConversionReport({bucket: 'day'});
// One fetch per report, shared by the KPI row and the charts.
const footfall = useFootfallReport({bucket: 'day', compare: true});
const conversion = useConversionReport({bucket: 'day', compare: true});
const kpis = useDashboardKpis(footfall, conversion);
const visits = useRecentVisits(6);
// Pushes new arrivals into the recent-arrivals panel as they happen.
useArrivalStream(
visits.refetch,
visits.isRefreshing ? undefined : visits.data?.cursor,
);
const scopeLabel = useScopeLabel();
return (
@@ -119,10 +124,7 @@ export default function DashboardPage() {
<ArrivalsFeed resource={visits} viewAllHref="/activity" />
<FeatureUnavailable
title="Customer activity and engagement"
description="Selfies, spins, scratch cards, challenges, referrals and events are not being recorded by any till or app yet, so there is nothing to measure their effect on repeat visits or revenue. As soon as they are, this section fills in on its own — no number here is estimated."
/>
<EngagementSection />
</VStack>
);
}

View File

@@ -15,8 +15,12 @@ import {SkeletonCardGrid} from '@/shared/components/patterns/LoadingState';
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
import {NameCustomerDialog} from '@/features/floor/components/NameCustomerDialog';
import {SaleEntryDialog} from '@/features/commerce/components/SaleEntryDialog';
import {useFloor} from '@/features/floor/hooks/useFloor';
import {useFloor, type FloorAction} from '@/features/floor/hooks/useFloor';
import {useTodaySummary} from '@/features/floor/hooks/useTodaySummary';
import {TodaySummaryCard} from '@/features/floor/components/TodaySummaryCard';
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
import {useSession} from '@/features/auth/providers/SessionProvider';
import {isStaffRole} from '@/features/auth/services/staffAccess';
import type {FloorVisit} from '@/features/floor/types/floor';
/**
@@ -35,7 +39,13 @@ function whenSeen(iso: string): string {
}
export default function FloorPage() {
const {resource, act, pending, conflict} = useFloor();
const {resource, act, pending, conflict, failure} = useFloor();
// Today's summary carries revenue — merchant-only (staffAccess.ts).
const isStaff = isStaffRole(useSession().user?.role);
const today = useTodaySummary(!isStaff);
// Taking, releasing or completing a customer changes "on the floor now".
const run = (visitId: string, action: FloorAction) =>
void act(visitId, action).then(today.refetch);
const scopeLabel = useScopeLabel();
const [naming, setNaming] = useState<FloorVisit | null>(null);
const [selling, setSelling] = useState<FloorVisit | null>(null);
@@ -49,9 +59,12 @@ export default function FloorPage() {
controls={<ScopeControls />}
/>
{isStaff ? null : <TodaySummaryCard resource={today} />}
{/* The platform's own refusal, shown verbatim — it names who holds the
customer, which is the part staff need. */}
{conflict ? <Banner status="warning" title={conflict.message} /> : null}
{failure ? <Banner status="error" title={failure.message} /> : null}
<AsyncBoundary
resource={resource}
@@ -120,12 +133,12 @@ export default function FloorPage() {
<Button
variant="secondary"
isDisabled={pending === v.visitId}
onClick={() => void act(v.visitId, 'release')}
onClick={() => run(v.visitId, 'release')}
label="Release"
/>
<Button
isDisabled={pending === v.visitId}
onClick={() => void act(v.visitId, 'complete')}
onClick={() => run(v.visitId, 'complete')}
label="Complete"
/>
</>
@@ -136,7 +149,7 @@ export default function FloorPage() {
// which is more useful than a control that vanishes.
variant={heldByOther ? 'secondary' : 'primary'}
isDisabled={pending === v.visitId}
onClick={() => void act(v.visitId, 'attend')}
onClick={() => run(v.visitId, 'attend')}
label={heldByOther ? 'Taken' : 'Take'}
/>
)}
@@ -178,6 +191,7 @@ export default function FloorPage() {
onSaved={() => {
setSelling(null);
resource.refetch();
today.refetch();
}}
/>
) : null}

View File

@@ -13,6 +13,7 @@ import {Icon} from '@astryxdesign/core/Icon';
import {useRouter} from 'next/navigation';
import {useBreakpoint} from '@/shared/hooks/useBreakpoint';
import {SETTINGS_NAV, isSettingsActive} from '@/features/settings/config/settingsNav';
import {useRoleFilter} from '@/shared/layouts/workspace/useRoleNav';
/**
* Settings gets its own sub-navigation.
@@ -36,13 +37,14 @@ export default function SettingsLayout({
}) {
const pathname = usePathname();
const router = useRouter();
const settingsNav = useRoleFilter(SETTINGS_NAV);
const bp = useBreakpoint();
const isNarrow = bp === 'mobile' || bp === 'tablet';
if (isNarrow) {
const active =
SETTINGS_NAV.find((s) => isSettingsActive(pathname, s.href)) ??
SETTINGS_NAV[0];
settingsNav.find((s) => isSettingsActive(pathname, s.href)) ??
settingsNav[0];
// A dropdown, not a TabList.
//
@@ -64,7 +66,7 @@ export default function SettingsLayout({
icon: <Icon icon={active.icon} size="sm" />,
}}
menuWidth={260}
items={SETTINGS_NAV.map((s) => ({
items={settingsNav.map((s) => ({
label: s.label,
icon: s.icon,
onClick: () => router.push(s.href),
@@ -98,7 +100,7 @@ export default function SettingsLayout({
*/}
<SideNav className="w-full">
<SideNavSection title="Settings">
{SETTINGS_NAV.map((s) => (
{settingsNav.map((s) => (
<SideNavItem
key={s.href}
label={s.label}

View File

@@ -5,7 +5,7 @@ export default function StoreSettingsPage() {
return (
<SettingsPage
title="Store Locations"
description="Branch operations, outlet directory, operating schedules and manager assignments."
description="Open, rename and remove the shops in your company."
>
<StoreManagement />
</SettingsPage>