update ui update and fix layout issue
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 />}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user