update ui update and fix layout issue

This commit is contained in:
2026-08-06 13:21:10 +05:30
parent 92322bff16
commit e5f8144fb3
251 changed files with 9041 additions and 2119 deletions

View File

@@ -1,13 +1,11 @@
'use client';
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/components/primitives/PageHeader';
import {ScopeControls} from '@/components/scope/ScopeControls';
import {ActivityTimeline} from '@/features/dashboard/ActivityTimeline';
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 {PageHeader} from '@/shared/components/primitives/PageHeader';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {ActivityTimeline} from '@/features/dashboard/components/ActivityTimeline';
import {useDashboardActivity} from '@/features/dashboard/hooks/useDashboard';
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
/**
* The complete event history — where the dashboard's "View all" leads.
@@ -22,10 +20,8 @@ import {storeName} from '@/lib/mock/stores';
* uncapped: one feed implementation, two budgets.
*/
export default function ActivityPage() {
const {storeId, range} = useWorkspace();
const activity = useResource(endpoints.dashboardActivity({range, storeId}));
const scopeLabel = storeId === 'all' ? 'all stores' : storeName(storeId);
const activity = useDashboardActivity();
const scopeLabel = useScopeLabel();
return (
<VStack gap={5}>

View File

@@ -5,30 +5,37 @@ import {VStack} from '@astryxdesign/core/Layout';
import {Grid} from '@astryxdesign/core/Grid';
import {Button} from '@astryxdesign/core/Button';
import {Icon} from '@astryxdesign/core/Icon';
import {PageHeader} from '@/components/primitives/PageHeader';
import {ScopeControls} from '@/components/scope/ScopeControls';
import {ICONS} from '@/lib/icons';
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 {PageHeader} from '@/shared/components/primitives/PageHeader';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {ICONS} from '@/shared/utils/icons';
import {ChartCard} from '@/shared/components/charts/ChartCard';
import {AreaChartView} from '@/shared/components/charts/AreaChartView';
import {LineChartView} from '@/shared/components/charts/LineChartView';
import {BarChartView} from '@/shared/components/charts/BarChartView';
import {HeatmapGrid} from '@/shared/components/charts/HeatmapGrid';
import {KpiRow} from '@/features/dashboard/components/KpiRow';
import {ActivityTimeline} from '@/features/dashboard/components/ActivityTimeline';
import {RewardUsageChart} from '@/features/dashboard/components/RewardUsageChart';
import {StoreComparisonPanel} from '@/features/dashboard/components/StoreComparison';
import {PerformancePanel} from '@/features/dashboard/components/PerformancePanel';
import {
useDashboardActivity,
useDashboardKpis,
useDashboardPeakHours,
useDashboardRewardUsage,
useDashboardStoreComparison,
useDashboardTimeseries,
} from '@/features/dashboard/hooks/useDashboard';
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
import {greetingFor} from '@/features/dashboard/services/dashboardService';
import type {Granularity} from '@/features/dashboard/types/dashboard';
import {
formatCompact,
formatDayLabel,
formatInrCompact,
formatPct,
} from '@/lib/format';
} from '@/shared/utils/format';
/**
* An analytics workspace. Charts are the subject, not evidence for a to-do list.
@@ -39,7 +46,7 @@ import {
*
* Operational widgets — quick actions, tasks, the AI briefing, top store/reward,
* staff status — deliberately do NOT live here. The briefing endpoint they were
* built on is still live and feeds the Copilot's AI tab, which is where that
* built on is still live and feeds Loyaly AI's AI tab, which is where that
* class of content belongs: a panel you open to be told what to do, beside a
* dashboard you read to work it out yourself.
*
@@ -52,75 +59,35 @@ import {
* charts drawn from it, so they cannot disagree.
*/
/**
* Derived from the SERVER's clock, carried on the response meta.
*
* Calling Date.now() during render is impure and disagrees between the SSR and
* hydration passes. Before meta lands there is no instant to reason about, so
* the greeting is the time-independent one; both strings are a single line, so
* the swap costs no layout shift. getHours() resolves the server's instant in
* the viewer's zone, which is the hour the merchant is actually living in.
*/
function greetingFor(generatedAt?: string): string {
if (!generatedAt) return 'Welcome back';
const hour = new Date(generatedAt).getHours();
if (hour < 12) return 'Good morning';
if (hour < 17) return 'Good afternoon';
return 'Good evening';
}
export default function DashboardPage() {
const {storeId, range} = useWorkspace();
const {storeId} = useWorkspace();
const [granularity, setGranularity] = useState<Granularity>('weekly');
// On by default: store comparison has always been part of the all-stores
// dashboard, so Compare starts pressed and switches the panel off, rather
// than hiding a panel that used to be there until someone finds the button.
const [isComparing, setIsComparing] = useState(true);
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 kpis = useDashboardKpis();
const series = useDashboardTimeseries();
const peak = useDashboardPeakHours();
const activity = useDashboardActivity();
const rewards = useDashboardRewardUsage();
const comparison = useDashboardStoreComparison();
const isAllStores = storeId === 'all';
const scopeLabel = isAllStores ? 'all stores' : storeName(storeId);
const scopeLabel = useScopeLabel();
return (
<VStack gap={5}>
{/* 1 + 2 — greeting, title, and the store / period / compare filters. */}
<PageHeader
eyebrow={greetingFor(kpis.meta?.generatedAt)}
title="Dashboard"
description={`Business performance across ${scopeLabel}.`}
controls={
<>
<ScopeControls />
<Button
size="sm"
// Pressed state is the raised gray of `primary` in this theme —
// a toggle needs a visible on-state, and it must not be colour.
variant={isComparing ? 'primary' : 'secondary'}
label="Compare"
icon={<Icon icon={ICONS.compare} size="sm" />}
aria-pressed={isComparing}
isDisabled={!isAllStores}
// With a tooltip present, Button uses aria-disabled rather than
// the native attribute, so the reason stays reachable by keyboard
// instead of the control just going dead.
tooltip={
isAllStores
? undefined
: 'Comparing stores needs the All stores scope'
}
onClick={() => setIsComparing((v) => !v)}
/>
</>
}
/>
{/* 1 + 2 — Sticky header: greeting, title, description, and filter controls. */}
<div className="sticky -top-5 z-40 -mx-5 px-5 pt-5 pb-4 bg-[#0F0F10] border-b border-[#2F2F2F] shadow-sm">
<PageHeader
eyebrow={greetingFor(kpis.meta?.generatedAt)}
title="Dashboard"
description={`Business performance across ${scopeLabel}.`}
controls={<ScopeControls />}
/>
</div>
{/* 3 — headline numbers. */}
<KpiRow resource={kpis} />
@@ -209,7 +176,6 @@ export default function DashboardPage() {
{/* 7 — period rollup. */}
<PerformancePanel
scope={scope}
granularity={granularity}
onGranularityChange={setGranularity}
/>

View File

@@ -1,160 +1,16 @@
'use client';
import {AppShell} from '@astryxdesign/core/AppShell';
import {
Layout,
LayoutContent,
LayoutHeader,
LayoutPanel,
VStack,
} from '@astryxdesign/core/Layout';
import {AppSideNav} from '@/components/shell/AppSideNav';
import {AppTopNav} from '@/components/shell/AppTopNav';
import {MobileMenu} from '@/components/shell/MobileMenu';
import {SidebarProvider, useSidebar} from '@/components/shell/SidebarProvider';
import {CopilotPanel} from '@/features/copilot/CopilotPanel';
import {CopilotSlideOver} from '@/features/copilot/CopilotSlideOver';
import {useCopilot} from '@/features/copilot/CopilotProvider';
import {
useBreakpoint,
isPanelInline,
isSideNavInline,
copilotWidth,
contentMaxWidth,
} from '@/lib/breakpoints';
import {ProtectedLayout} from '@/shared/layouts/ProtectedLayout';
/**
* The three-column shell, and the reason the Copilot survives navigation.
* The route-group boundary for every authenticated screen.
*
* Next's App Router preserves a layout's element identity across sibling route
* changes, so /dashboard → /staff swaps only {children}. Everything else here —
* nav, scroll position, the Copilot — is untouched.
*
* Structure, and why AppShell alone is not enough: AppShell renders
* Layout{header, start, content} internally and exposes no end/panel slot. The
* Copilot column therefore comes from a NESTED Layout inside its children.
*
* AppShell(sideNav)
* └─ Layout(header = TopNav, content = page, end = LayoutPanel > Copilot)
*
* The top nav is deliberately NOT passed to AppShell's `topNav` slot. That slot
* renders Layout{header} at the shell root, which spans the full viewport width
* and pushes the sidebar — and therefore the branding — 48px down the screen.
* Measured in the browser, the logo sat at y=64 with the top-left 260x48 region
* empty. Moving the bar into the content column's own header starts the sidebar
* at y=0, so the brand anchors in the window's top-left corner and the bar
* begins where the content does. This is the arrangement Linear, Cursor and
* Stripe use, and no amount of padding inside the sidebar could produce it.
*
* The one thing given up: with no `topNav`, AppShell drops the --radius-page
* corner it draws where the top bar meets the sidebar. The content area still
* paints elevated --color-background-surface (#0F0F10) against the #000 rails,
* and with a full-height sidebar that corner had nothing left to round.
* Deliberately thin: the composition lives in ProtectedLayout, so this file
* never needs editing again and the shell can be reused (tests, a future
* embedded view) without a route existing for it.
*/
export default function WorkspaceLayout({
export default function WorkspaceRouteLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<SidebarProvider>
<WorkspaceShell>{children}</WorkspaceShell>
</SidebarProvider>
);
}
/**
* Split out from the layout only so it can READ the sidebar context that the
* layout mounts — a provider's own component sits above its value.
*/
function WorkspaceShell({children}: {children: React.ReactNode}) {
const bp = useBreakpoint();
const {isOpen} = useCopilot();
const inline = isPanelInline(bp);
const showInlinePanel = inline && isOpen;
// Both flags come from SidebarProvider now. AppShell keeps a copy of the
// open state for its own context (MobileNav reads it for aria wiring), but
// ours is the one that decides — uncontrolled, there would be no way to
// dismiss the drawer when a nav item is selected.
const {isDrawerOpen, setDrawerOpen} = useSidebar();
// Below the breakpoint the rail is gone — it lives in the drawer — so there
// is no top-left corner to anchor the brand to and nothing for the bar to
// start after. Hand the top nav back to AppShell there. Keeping it in the
// content column instead pushed the SideNav into AppShell's
// `autoMobileTopBar` fallback, which crops the lockup into a 48px bar.
const sideNavInline = isSideNavInline(bp);
return (
<AppShell
variant="elevated"
height="fill"
// Layout/LayoutContent own the padding so the Copilot panel can sit
// flush against the workspace edge.
contentPadding={0}
topNav={sideNavInline ? undefined : <AppTopNav />}
sideNav={<AppSideNav />}
// 'sm' (640px) matches the mobile/tablet line in lib/breakpoints —
// AppShell's own threshold and ours have to agree or the hamburger and
// the rail are both visible, or neither is.
mobileNav={{
breakpoint: 'sm',
isOpen: isDrawerOpen,
onOpenChange: setDrawerOpen,
// hasToggle: false — AppShell would otherwise inject a SECOND
// hamburger into TopNav's mobile bar, next to the one in `heading`
// that works at every width. See AppTopNav.
hasToggle: false,
content: <MobileMenu />,
}}
>
<Layout
height="fill"
header={
sideNavInline ? (
<LayoutHeader padding={0}>
<AppTopNav />
</LayoutHeader>
) : undefined
}
content={
<LayoutContent padding={5}>
{/*
Above 1920 the workspace is capped and centred. Uncapped, a
30-day line chart stretches across ~1900px, which flattens every
trend it exists to show, and body copy runs well past the ~90ch
where reading breaks down. Below the cap this is a no-op —
maxWidth only binds once there is more width than content wants.
*/}
<VStack
width="100%"
maxWidth={contentMaxWidth(bp)}
className={bp === 'ultrawide' ? 'mx-auto' : undefined}
>
{children}
</VStack>
</LayoutContent>
}
end={
showInlinePanel ? (
<LayoutPanel
hasDivider
width={copilotWidth(bp)}
// CopilotPanel supplies its own header and tab chrome.
padding={0}
role="complementary"
label="Loyaly AI"
>
<CopilotPanel />
</LayoutPanel>
) : undefined
}
/>
{/* Mounted only below the inline breakpoint. Because every piece of
Copilot state lives in CopilotProvider (above the route tree), moving
between these two presentations is lossless. */}
{!inline ? <CopilotSlideOver /> : null}
</AppShell>
);
return <ProtectedLayout>{children}</ProtectedLayout>;
}

View File

@@ -3,31 +3,33 @@
import {VStack} from '@astryxdesign/core/Layout';
import {Grid} from '@astryxdesign/core/Grid';
import {Text} from '@astryxdesign/core/Text';
import {PageHeader} from '@/components/primitives/PageHeader';
import {MetricCard} from '@/components/patterns/MetricCard';
import {SkeletonMetricGrid} from '@/components/patterns/LoadingState';
import {AsyncBoundary} from '@/components/data/AsyncBoundary';
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 {RewardGrid} from '@/features/lyts/RewardGrid';
import {ExpiryAlerts} from '@/features/lyts/ExpiryAlerts';
import {RewardPerformanceTable} from '@/features/lyts/RewardPerformanceTable';
import {ActivityTimeline} from '@/features/dashboard/ActivityTimeline';
import {useMetricColumns} from '@/features/dashboard/KpiRow';
import {useResource} from '@/lib/api/useResource';
import {endpoints} from '@/lib/api/client';
import {useWorkspace} from '@/components/shell/WorkspaceProvider';
import {ScopeControls} from '@/components/scope/ScopeControls';
import {ICONS} from '@/lib/icons';
import {usageRate} from '@/features/lyts/reward-status';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {MetricCard} from '@/shared/components/patterns/MetricCard';
import {SkeletonMetricGrid} from '@/shared/components/patterns/LoadingState';
import {AsyncBoundary} from '@/shared/components/data/AsyncBoundary';
import {ChartCard} from '@/shared/components/charts/ChartCard';
import {AreaChartView} from '@/shared/components/charts/AreaChartView';
import {LineChartView} from '@/shared/components/charts/LineChartView';
import {BarChartView} from '@/shared/components/charts/BarChartView';
import {RewardGrid} from '@/features/lyts/components/RewardGrid';
import {ExpiryAlerts} from '@/features/lyts/components/ExpiryAlerts';
import {RewardPerformanceTable} from '@/features/lyts/components/RewardPerformanceTable';
import {ActivityTimeline} from '@/features/dashboard/components/ActivityTimeline';
import {useMetricColumns} from '@/features/dashboard/components/KpiRow';
import {
useLytsActivity,
useRedemptions,
useRewards,
} from '@/features/lyts/hooks/useLyts';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {ICONS} from '@/shared/utils/icons';
import {usageRate} from '@/features/lyts/services/lytsService';
import {
formatCompact,
formatDayLabel,
formatLyt,
formatPct,
} from '@/lib/format';
} from '@/shared/utils/format';
/**
* The LYT programme.
@@ -40,12 +42,9 @@ import {
* rupee liability — which is why they lead.
*/
export default function LytsPage() {
const {storeId, range} = useWorkspace();
const scope = {range, storeId};
const rewards = useResource(endpoints.lytsRewards(scope));
const redemptions = useResource(endpoints.lytsRedemptions(scope));
const activity = useResource(endpoints.lytsActivity(scope));
const rewards = useRewards();
const redemptions = useRedemptions();
const activity = useLytsActivity();
const columns = useMetricColumns();
@@ -63,7 +62,7 @@ export default function LytsPage() {
return (
<VStack gap={5}>
<PageHeader
title="Lyts"
title="Lytsup"
description="Reward performance, redemption and outstanding LYT liability. 1 LYT = ₹1."
controls={<ScopeControls />}
/>

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {ApiWebhooksManager} from '@/features/settings/ApiWebhooksManager';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {ApiWebhooksManager} from '@/features/settings/components/ApiWebhooksManager';
export default function ApiSettingsPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {BillingOverview} from '@/features/settings/BillingOverview';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {BillingOverview} from '@/features/settings/components/BillingOverview';
export default function SettingsBillingPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {IntegrationsGrid} from '@/features/settings/IntegrationsGrid';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {IntegrationsGrid} from '@/features/settings/components/IntegrationsGrid';
export default function IntegrationsSettingsPage() {
return (

View File

@@ -11,8 +11,8 @@ import {SideNav, SideNavItem, SideNavSection} from '@astryxdesign/core/SideNav';
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
import {Icon} from '@astryxdesign/core/Icon';
import {useRouter} from 'next/navigation';
import {useBreakpoint} from '@/lib/breakpoints';
import {SETTINGS_NAV, isSettingsActive} from '@/features/settings/settings-nav';
import {useBreakpoint} from '@/shared/hooks/useBreakpoint';
import {SETTINGS_NAV, isSettingsActive} from '@/features/settings/config/settingsNav';
/**
* Settings gets its own sub-navigation.
@@ -97,7 +97,7 @@ export default function SettingsLayout({
bottom edge (measured: scrollWidth 272 in a 240px panel).
*/}
<SideNav className="w-full">
<SideNavSection title="Settings" isHeaderHidden>
<SideNavSection title="Settings">
{SETTINGS_NAV.map((s) => (
<SideNavItem
key={s.href}

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {NotificationsForm} from '@/features/settings/NotificationsForm';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {NotificationsForm} from '@/features/settings/components/NotificationsForm';
export default function NotificationsSettingsPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {BusinessForm} from '@/features/settings/BusinessForm';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {BusinessForm} from '@/features/settings/components/BusinessForm';
export default function BusinessSettingsPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {PreferencesForm} from '@/features/settings/PreferencesForm';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {PreferencesForm} from '@/features/settings/components/PreferencesForm';
export default function PreferencesSettingsPage() {
return (

View File

@@ -1,9 +1,18 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {ProfileForm} from '@/features/settings/ProfileForm';
import {readProfile} from '@/lib/mock/settings';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {ProfileForm} from '@/features/settings/components/ProfileForm';
import {settingsServerRepository} from '@/features/settings/repositories/settingsServerRepository';
export default function MerchantProfilePage() {
const profile = readProfile();
/**
* Server Component: the record is read on the server and handed to the form as
* its initial state, so the inputs paint filled rather than flashing empty.
* The form then saves through the client repository over HTTP.
*
* The read goes through a repository rather than the fixture module the page
* used to import — a page that knows the shape of a mock is a page that breaks
* the day the mock is deleted.
*/
export default async function MerchantProfilePage() {
const profile = await settingsServerRepository.getProfile();
return (
<SettingsPage

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {RoleMatrix} from '@/features/settings/RoleMatrix';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {RoleMatrix} from '@/features/settings/components/RoleMatrix';
export default function RolesSettingsPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {SecurityManager} from '@/features/settings/SecurityManager';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {SecurityManager} from '@/features/settings/components/SecurityManager';
export default function SecuritySettingsPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {StoreManagement} from '@/features/settings/StoreManagement';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {StoreManagement} from '@/features/settings/components/StoreManagement';
export default function StoreSettingsPage() {
return (

View File

@@ -1,5 +1,5 @@
import {SettingsPage} from '@/features/settings/SettingsPage';
import {TeamManagement} from '@/features/settings/TeamManagement';
import {SettingsPage} from '@/features/settings/components/SettingsPage';
import {TeamManagement} from '@/features/settings/components/TeamManagement';
export default function TeamSettingsPage() {
return (

View File

@@ -2,19 +2,21 @@
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 {BarChartView} from '@/components/charts/BarChartView';
import {StaffKpis} from '@/features/staff/StaffKpis';
import {StaffGrid} from '@/features/staff/StaffGrid';
import {AttendanceChart} from '@/features/staff/AttendanceChart';
import {Leaderboard} from '@/features/staff/Leaderboard';
import {useResource} from '@/lib/api/useResource';
import {endpoints} from '@/lib/api/client';
import {useWorkspace} from '@/components/shell/WorkspaceProvider';
import {ScopeControls} from '@/components/scope/ScopeControls';
import {storeName} from '@/lib/mock/stores';
import {formatCompact} from '@/lib/format';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {ChartCard} from '@/shared/components/charts/ChartCard';
import {BarChartView} from '@/shared/components/charts/BarChartView';
import {StaffKpis} from '@/features/staff/components/StaffKpis';
import {StaffGrid} from '@/features/staff/components/StaffGrid';
import {AttendanceChart} from '@/features/staff/components/AttendanceChart';
import {Leaderboard} from '@/features/staff/components/Leaderboard';
import {
useStaffAttendance,
useStaffList,
useStaffSummary,
} from '@/features/staff/hooks/useStaff';
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {formatCompact} from '@/shared/utils/format';
/**
* The team.
@@ -23,18 +25,10 @@ import {formatCompact} from '@/lib/format';
* morning is "who is in today", and only then "who is performing".
*/
export default function StaffPage() {
const {storeId, range} = useWorkspace();
const scope = {range, storeId};
const summary = useResource(endpoints.staffSummary(scope), {
// A summary object is never "empty" the way a list is — zero present is
// a real answer, not an absence of data.
isEmpty: () => false,
});
const staff = useResource(endpoints.staff(scope));
const attendance = useResource(endpoints.staffAttendance(scope));
const scopeLabel = storeId === 'all' ? 'all stores' : storeName(storeId);
const summary = useStaffSummary();
const staff = useStaffList();
const attendance = useStaffAttendance();
const scopeLabel = useScopeLabel();
return (
<VStack gap={5}>

View File

@@ -9,29 +9,30 @@ import {
BreadcrumbItem,
Breadcrumbs,
} from '@astryxdesign/core/Breadcrumbs';
import {PageHeader} from '@/components/primitives/PageHeader';
import {MetricCard} from '@/components/patterns/MetricCard';
import {ChartCard} from '@/components/charts/ChartCard';
import {AreaChartView} from '@/components/charts/AreaChartView';
import {BarChartView} from '@/components/charts/BarChartView';
import {LineChartView} from '@/components/charts/LineChartView';
import {HeatmapGrid} from '@/components/charts/HeatmapGrid';
import {AsyncBoundary} from '@/components/data/AsyncBoundary';
import {SkeletonMetricGrid} from '@/components/patterns/LoadingState';
import {ActivityTimeline} from '@/features/dashboard/ActivityTimeline';
import {useMetricColumns} from '@/features/dashboard/KpiRow';
import {useResource} from '@/lib/api/useResource';
import {endpoints} from '@/lib/api/client';
import {useWorkspace} from '@/components/shell/WorkspaceProvider';
import {ScopeControls} from '@/components/scope/ScopeControls';
import {ICONS} from '@/lib/icons';
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 {useResource} from '@/shared/hooks/useResource';
import {storeRepository} from '@/features/stores/repositories/storeRepository';
import {dashboardRepository} from '@/features/dashboard/repositories/dashboardRepository';
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
import {ICONS} from '@/shared/utils/icons';
import {
formatCompact,
formatDayLabel,
formatInrCompact,
formatPct,
} from '@/lib/format';
import type {StoreStatus} from '@/lib/api/contracts';
} from '@/shared/utils/format';
import type {StoreStatus} from '@/features/stores/types/store';
const STATUS: Record<
StoreStatus,
@@ -59,13 +60,13 @@ export default function StoreDetailPage({
const {range} = useWorkspace();
const scope = {range, storeId};
const store = useResource(endpoints.store(scope, storeId), {
const store = useResource(storeRepository.byId(scope, storeId), {
isEmpty: () => false,
});
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 kpis = useResource(dashboardRepository.kpis(scope));
const series = useResource(dashboardRepository.timeseries(scope));
const peak = useResource(dashboardRepository.peakHours(scope));
const activity = useResource(dashboardRepository.activity(scope));
const columns = useMetricColumns();
const name = store.data?.name ?? 'Store';

View File

@@ -1,15 +1,14 @@
'use client';
import {VStack} from '@astryxdesign/core/Layout';
import {PageHeader} from '@/components/primitives/PageHeader';
import {StoreGrid} from '@/features/stores/StoreGrid';
import {useResource} from '@/lib/api/useResource';
import {endpoints} from '@/lib/api/client';
import {PageHeader} from '@/shared/components/primitives/PageHeader';
import {StoreGrid} from '@/features/stores/components/StoreGrid';
import {useStoreList} from '@/features/stores/hooks/useStores';
import {
RANGE_LABELS,
useWorkspace,
} from '@/components/shell/WorkspaceProvider';
import {ScopeControls} from '@/components/scope/ScopeControls';
} from '@/shared/providers/WorkspaceProvider';
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
/**
* The store roster.
@@ -21,7 +20,7 @@ import {ScopeControls} from '@/components/scope/ScopeControls';
*/
export default function StoresPage() {
const {range} = useWorkspace();
const stores = useResource(endpoints.stores({range, storeId: 'all'}));
const stores = useStoreList();
return (
<VStack gap={5}>