update sales page
This commit is contained in:
10
src/app/(workspace)/commerce/page.tsx
Normal file
10
src/app/(workspace)/commerce/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {CommerceWorkspace} from '@/features/commerce/components/CommerceWorkspace';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Commerce | Loyaly Merchant OS',
|
||||
description: 'AI-first commerce workspace for sales, orders, revenue and business performance telemetry.',
|
||||
};
|
||||
|
||||
export default function CommercePage() {
|
||||
return <CommerceWorkspace />;
|
||||
}
|
||||
70
src/features/commerce/components/CommerceKpiRow.tsx
Normal file
70
src/features/commerce/components/CommerceKpiRow.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import {Grid} from '@astryxdesign/core/Grid';
|
||||
import {MetricCard} from '@/shared/components/patterns/MetricCard';
|
||||
import {ICONS} from '@/shared/utils/icons';
|
||||
import {formatCount, formatInrCompact, formatPct} from '@/shared/utils/format';
|
||||
|
||||
export function CommerceKpiRow() {
|
||||
const kpiList = [
|
||||
{
|
||||
label: 'Total Revenue',
|
||||
value: 340000,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.revenue,
|
||||
deltaPct: 14.2,
|
||||
},
|
||||
{
|
||||
label: 'Total Orders',
|
||||
value: 1284,
|
||||
format: formatCount,
|
||||
icon: ICONS.purchases,
|
||||
deltaPct: 8.5,
|
||||
},
|
||||
{
|
||||
label: 'Avg Order Value',
|
||||
value: 1420,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.purchases,
|
||||
deltaPct: 5.1,
|
||||
},
|
||||
{
|
||||
label: 'Net Sales',
|
||||
value: 318000,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.revenue,
|
||||
deltaPct: 13.9,
|
||||
},
|
||||
{
|
||||
label: 'Refunds',
|
||||
value: 22000,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.alert,
|
||||
deltaPct: -2.4,
|
||||
isRiseGood: false,
|
||||
},
|
||||
{
|
||||
label: 'Conversion',
|
||||
value: 23.8,
|
||||
format: (v: number) => formatPct(v, 1),
|
||||
icon: ICONS.conversion,
|
||||
deltaPct: 1.4,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid columns={{minWidth: 220, max: 6, repeat: 'fit'}} gap={4}>
|
||||
{kpiList.map((kpi) => (
|
||||
<MetricCard
|
||||
key={kpi.label}
|
||||
label={kpi.label}
|
||||
value={kpi.value}
|
||||
format={kpi.format}
|
||||
icon={kpi.icon}
|
||||
deltaPct={kpi.deltaPct}
|
||||
isRiseGood={kpi.isRiseGood}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
53
src/features/commerce/components/CommerceRecentOrders.tsx
Normal file
53
src/features/commerce/components/CommerceRecentOrders.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {Icon} from '@astryxdesign/core/Icon';
|
||||
import {ICONS} from '@/shared/utils/icons';
|
||||
|
||||
const RECENT_ORDERS = [
|
||||
{id: 'o1', code: '#LYT-9842', customer: 'Aarav Sharma', store: 'Indiranagar Flagship', amount: '₹2,450', method: 'UPI', status: 'completed', time: '2m ago'},
|
||||
{id: 'o2', code: '#LYT-9841', customer: 'Priya Patel', store: 'Koramangala 80ft', amount: '₹1,890', method: 'Credit Card', status: 'completed', time: '5m ago'},
|
||||
{id: 'o3', code: '#LYT-9840', customer: 'Rohan Mehta', store: 'Jayanagar 4th Block', amount: '₹3,120', method: 'Shopify', status: 'completed', time: '12m ago'},
|
||||
{id: 'o4', code: '#LYT-9839', customer: 'Ananya Roy', store: 'Whitefield Main', amount: '₹950', method: 'UPI', status: 'completed', time: '18m ago'},
|
||||
{id: 'o5', code: '#LYT-9838', customer: 'Kabir Verma', store: 'Indiranagar Flagship', amount: '₹4,200', method: 'Card', status: 'completed', time: '24m ago'},
|
||||
];
|
||||
|
||||
export function CommerceRecentOrders() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-3 shadow-sm">
|
||||
<div className="flex items-center justify-between border-b border-border pb-3">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Icon icon={ICONS.purchases} size="sm" className="text-zinc-400" />
|
||||
Recent Orders
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Real-time completed orders across your network
|
||||
</Text>
|
||||
</div>
|
||||
<Badge variant="success" label="Live Feed" />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-zinc-800/80">
|
||||
{RECENT_ORDERS.map((ord) => (
|
||||
<div key={ord.id} className="py-2.5 first:pt-0 last:pb-0 flex items-center justify-between text-sm">
|
||||
<div>
|
||||
<div className="font-bold text-white flex items-center gap-2">
|
||||
{ord.code}
|
||||
<span className="text-xs text-zinc-400 font-normal">• {ord.customer}</span>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-400 font-medium">
|
||||
{ord.store} ({ord.method})
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-bold text-white tabular-nums">{ord.amount}</div>
|
||||
<div className="text-xs text-zinc-400">{ord.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/features/commerce/components/CommerceRevenueSummary.tsx
Normal file
48
src/features/commerce/components/CommerceRevenueSummary.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {MetricCard} from '@/shared/components/patterns/MetricCard';
|
||||
import {formatInrCompact} from '@/shared/utils/format';
|
||||
|
||||
export function CommerceRevenueSummary() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-4 shadow-sm">
|
||||
<div className="border-b border-border pb-3">
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Revenue Summary
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Financial performance rollup for the selected period
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<MetricCard
|
||||
label="Gross Sales"
|
||||
value={362000}
|
||||
format={formatInrCompact}
|
||||
deltaPct={12.8}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Discounts & Promos"
|
||||
value={22000}
|
||||
format={formatInrCompact}
|
||||
deltaPct={-4.1}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Refunds Processed"
|
||||
value={22000}
|
||||
format={formatInrCompact}
|
||||
deltaPct={-2.4}
|
||||
isRiseGood={false}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Net Realized Revenue"
|
||||
value={318000}
|
||||
format={formatInrCompact}
|
||||
deltaPct={13.9}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/features/commerce/components/CommerceSalesInsights.tsx
Normal file
72
src/features/commerce/components/CommerceSalesInsights.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {Icon} from '@astryxdesign/core/Icon';
|
||||
import {ICONS} from '@/shared/utils/icons';
|
||||
|
||||
const SALES_INSIGHTS = [
|
||||
{
|
||||
id: 'si1',
|
||||
title: 'Evening Peak Surge',
|
||||
description: 'Evening hours (17:30 - 19:30) generated 38% of total daily revenue.',
|
||||
intent: 'positive',
|
||||
badge: 'Growth Opportunity',
|
||||
},
|
||||
{
|
||||
id: 'si2',
|
||||
title: 'Stock Reorder Warning',
|
||||
description: 'Artisanal Cold Brew Pack (18 left) is approaching stockout threshold.',
|
||||
intent: 'warning',
|
||||
badge: 'Action Needed',
|
||||
},
|
||||
{
|
||||
id: 'si3',
|
||||
title: 'Indiranagar Conversion Leader',
|
||||
description: 'Indiranagar delivered +18.4% MoM conversion growth after digital pick-up lanes.',
|
||||
intent: 'positive',
|
||||
badge: 'Best Performer',
|
||||
},
|
||||
];
|
||||
|
||||
export function CommerceSalesInsights() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-3 shadow-sm">
|
||||
<div className="flex items-center justify-between border-b border-border pb-3">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Icon icon={ICONS.ai} size="sm" className="text-amber-400" />
|
||||
Sales Insights
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Automated intelligence highlights & telemetry warnings
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{SALES_INSIGHTS.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`p-3 rounded-lg border flex flex-col justify-between gap-1.5 ${
|
||||
item.intent === 'warning'
|
||||
? 'border-amber-500/30 bg-amber-500/[0.03]'
|
||||
: 'border-emerald-500/30 bg-emerald-500/[0.03]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-white">{item.title}</span>
|
||||
<Badge
|
||||
variant={item.intent === 'warning' ? 'warning' : 'success'}
|
||||
label={item.badge}
|
||||
/>
|
||||
</div>
|
||||
<Text size="sm" color="secondary">
|
||||
{item.description}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
src/features/commerce/components/CommerceWorkspace.tsx
Normal file
61
src/features/commerce/components/CommerceWorkspace.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import {VStack} from '@astryxdesign/core/Layout';
|
||||
import {Grid} from '@astryxdesign/core/Grid';
|
||||
import {PageHeader} from '@/shared/components/primitives/PageHeader';
|
||||
import {ScopeControls} from '@/shared/components/scope/ScopeControls';
|
||||
import {useScopeLabel} from '@/features/stores/hooks/useStoreDirectory';
|
||||
import {CommerceKpiRow} from './CommerceKpiRow';
|
||||
import {RevenueChart} from './analytics/RevenueChart';
|
||||
import {OrdersChart} from './analytics/OrdersChart';
|
||||
import {PaymentBreakdown} from './analytics/PaymentBreakdown';
|
||||
import {StorePerformanceChart} from './analytics/StorePerformanceChart';
|
||||
import {TopProductsTable} from './analytics/TopProductsTable';
|
||||
import {CommerceSalesInsights} from './CommerceSalesInsights';
|
||||
import {CommerceRevenueSummary} from './CommerceRevenueSummary';
|
||||
import {CommerceRecentOrders} from './CommerceRecentOrders';
|
||||
|
||||
export function CommerceWorkspace() {
|
||||
const scopeLabel = useScopeLabel();
|
||||
|
||||
return (
|
||||
<VStack gap={5}>
|
||||
{/* Sticky Header matching Dashboard visual language */}
|
||||
<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="Sales & Revenue Analytics"
|
||||
title="Commerce"
|
||||
description={`Sales, orders, revenue and store performance across ${scopeLabel}.`}
|
||||
controls={<ScopeControls />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 1 — Headline KPI Cards Row */}
|
||||
<CommerceKpiRow />
|
||||
|
||||
{/* 2 — Primary Sales Trends (Revenue Trend & Orders Trend) */}
|
||||
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
|
||||
<RevenueChart />
|
||||
<OrdersChart />
|
||||
</Grid>
|
||||
|
||||
{/* 3 — Revenue Breakdown & Store Performance */}
|
||||
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
|
||||
<PaymentBreakdown />
|
||||
<StorePerformanceChart />
|
||||
</Grid>
|
||||
|
||||
{/* 4 — Top Selling Products Leaderboard & Automated Sales Insights */}
|
||||
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
|
||||
<TopProductsTable />
|
||||
<CommerceSalesInsights />
|
||||
</Grid>
|
||||
|
||||
{/* 5 — Financial Revenue Rollup & Recent Orders Live Feed */}
|
||||
<Grid columns={{minWidth: 360, max: 2, repeat: 'fit'}} gap={4}>
|
||||
<CommerceRevenueSummary />
|
||||
<CommerceRecentOrders />
|
||||
</Grid>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {Icon} from '@astryxdesign/core/Icon';
|
||||
import {ICONS} from '@/shared/utils/icons';
|
||||
import {BUSINESS_ALERTS} from '../../services/commerceService';
|
||||
|
||||
export function BusinessAlerts() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Real-Time Business & Operational Alerts
|
||||
</Heading>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{BUSINESS_ALERTS.map((alert) => {
|
||||
const isCritical = alert.severity === 'critical';
|
||||
const isWarning = alert.severity === 'warning';
|
||||
const isSuccess = alert.severity === 'success';
|
||||
|
||||
const borderClass = isCritical
|
||||
? 'border-rose-500/30 bg-rose-500/[0.03]'
|
||||
: isWarning
|
||||
? 'border-amber-500/30 bg-amber-500/[0.03]'
|
||||
: isSuccess
|
||||
? 'border-emerald-500/30 bg-emerald-500/[0.03]'
|
||||
: 'border-blue-500/30 bg-blue-500/[0.03]';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`rounded-xl border p-4 shadow-sm flex flex-col justify-between gap-3 ${borderClass}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<Icon
|
||||
icon={isCritical || isWarning ? ICONS.alert : isSuccess ? ICONS.up : ICONS.info}
|
||||
size="sm"
|
||||
className={
|
||||
isCritical
|
||||
? 'text-rose-400'
|
||||
: isWarning
|
||||
? 'text-amber-400'
|
||||
: isSuccess
|
||||
? 'text-emerald-400'
|
||||
: 'text-blue-400'
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-bold text-white block">{alert.title}</span>
|
||||
<Text size="sm" color="secondary" className="mt-0.5">
|
||||
{alert.description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant={isCritical ? 'error' : isWarning ? 'warning' : isSuccess ? 'success' : 'info'}
|
||||
label={alert.type.replace('_', ' ').toUpperCase()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-white/5 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-bold text-white hover:text-amber-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
{alert.actionText} →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {Icon} from '@astryxdesign/core/Icon';
|
||||
import {ICONS} from '@/shared/utils/icons';
|
||||
import {COMMERCE_KPIS, PAYMENT_BREAKDOWN_DATA} from '../../services/commerceService';
|
||||
|
||||
export function BusinessPulsePanel() {
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto p-4 space-y-4 bg-background text-white">
|
||||
{/* Pulse Header */}
|
||||
<div className="flex items-center justify-between pb-2 border-b border-border">
|
||||
<div>
|
||||
<Heading level={3} className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<span className="size-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
Business Pulse
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Live telemetry stream
|
||||
</Text>
|
||||
</div>
|
||||
<Badge variant="success" label="Active" />
|
||||
</div>
|
||||
|
||||
{/* Pulse Cards */}
|
||||
<div className="space-y-3">
|
||||
{/* Today's Revenue */}
|
||||
<div className="rounded-xl border border-border bg-card p-3 flex justify-between items-center shadow-sm">
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-zinc-400">Today's Revenue</div>
|
||||
<div className="text-lg font-bold text-white tabular-nums">{COMMERCE_KPIS.revenue.value}</div>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
||||
{COMMERCE_KPIS.revenue.change}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Today's Orders */}
|
||||
<div className="rounded-xl border border-border bg-card p-3 flex justify-between items-center shadow-sm">
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-zinc-400">Today's Orders</div>
|
||||
<div className="text-lg font-bold text-white tabular-nums">{COMMERCE_KPIS.orders.value}</div>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
||||
{COMMERCE_KPIS.orders.change}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Active Promotions */}
|
||||
<div className="rounded-xl border border-border bg-card p-3 space-y-1 shadow-sm">
|
||||
<div className="flex items-center justify-between text-xs font-bold text-white">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Icon icon={ICONS.activeRewards} size="sm" className="text-amber-400" />
|
||||
Active Promotions
|
||||
</span>
|
||||
<Badge variant="info" label="3 Running" />
|
||||
</div>
|
||||
<div className="text-[11px] text-zinc-400 font-medium">
|
||||
Weekend Double LYT & Espresso Bundle driving +14% lift
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inventory Alerts */}
|
||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/[0.04] p-3 space-y-1 shadow-sm">
|
||||
<div className="flex items-center justify-between text-xs font-bold text-amber-300">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Icon icon={ICONS.alert} size="sm" className="text-amber-400" />
|
||||
Inventory Alert
|
||||
</span>
|
||||
<Badge variant="warning" label="2 Items" />
|
||||
</div>
|
||||
<div className="text-[11px] text-zinc-400">
|
||||
Cold Brew Pack (18 left) & Barista Mug (8 left)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Split */}
|
||||
<div className="rounded-xl border border-border bg-card p-3 space-y-2 shadow-sm">
|
||||
<div className="text-xs font-bold text-white flex justify-between">
|
||||
<span>Payment Method Split</span>
|
||||
<span className="text-emerald-400">UPI 58%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full rounded-full bg-zinc-800 overflow-hidden flex">
|
||||
{PAYMENT_BREAKDOWN_DATA.map((p) => (
|
||||
<div
|
||||
key={p.method}
|
||||
style={{width: `${p.percentage}%`, backgroundColor: p.color}}
|
||||
className="h-full"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Store */}
|
||||
<div className="rounded-xl border border-border bg-card p-3 space-y-1 shadow-sm">
|
||||
<div className="text-xs font-semibold text-zinc-400">Top Performing Store</div>
|
||||
<div className="text-sm font-bold text-white">{COMMERCE_KPIS.topStore?.value ?? 'Indiranagar Flagship'}</div>
|
||||
<div className="text-[11px] text-zinc-400">{COMMERCE_KPIS.topStore?.subtitle ?? '₹9.1L Revenue (23.8% Conv)'}</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Transactions */}
|
||||
<div className="rounded-xl border border-border bg-card p-3 space-y-2 shadow-sm">
|
||||
<div className="text-xs font-bold text-white">Recent Transactions</div>
|
||||
<div className="space-y-2 divide-y divide-zinc-800">
|
||||
{[
|
||||
{id: 't1', code: '#LYT-9842', name: 'Aarav Sharma', amount: '₹2,450', time: '2m ago'},
|
||||
{id: 't2', code: '#LYT-9841', name: 'Priya Patel', amount: '₹1,890', time: '5m ago'},
|
||||
{id: 't3', code: '#LYT-9840', name: 'Rohan Mehta', amount: '₹3,120', time: '12m ago'},
|
||||
].map((t) => (
|
||||
<div key={t.id} className="pt-1.5 first:pt-0 flex items-center justify-between text-xs">
|
||||
<div>
|
||||
<div className="font-bold text-white">{t.code}</div>
|
||||
<div className="text-[10px] text-zinc-400">{t.name}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-bold text-white tabular-nums">{t.amount}</div>
|
||||
<div className="text-[10px] text-zinc-400">{t.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/features/commerce/components/analytics/ForecastChart.tsx
Normal file
38
src/features/commerce/components/analytics/ForecastChart.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {LineChartView} from '@/shared/components/charts/LineChartView';
|
||||
import {FORECAST_TREND_DATA} from '../../services/commerceService';
|
||||
|
||||
export function ForecastChart() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-3 shadow-sm overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Predictive Revenue & Demand Forecast
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
AI-driven 7-day revenue, demand and stock requirement projections
|
||||
</Text>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-indigo-300 bg-indigo-500/10 px-3 py-1 rounded-full border border-indigo-500/20">
|
||||
96% Confidence Band
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-64 w-full pt-2">
|
||||
<LineChartView
|
||||
data={FORECAST_TREND_DATA}
|
||||
xKey="date"
|
||||
series={[
|
||||
{key: 'actual', label: 'Actual Revenue (₹)'},
|
||||
{key: 'forecast', label: 'Forecasted Projection (₹)'},
|
||||
{key: 'upperBound', label: 'Upper Bound (₹)'},
|
||||
]}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/features/commerce/components/analytics/HeatmapChart.tsx
Normal file
72
src/features/commerce/components/analytics/HeatmapChart.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {HEATMAP_DAYS, HEATMAP_HOURS} from '../../services/commerceService';
|
||||
|
||||
function getIntensityBg(day: string, hour: string): string {
|
||||
if (hour === '18:00') return 'bg-emerald-500 text-black font-bold';
|
||||
if (hour === '12:00' || hour === '14:00') return 'bg-emerald-600/80 text-white font-semibold';
|
||||
if (day === 'Sat' || day === 'Sun') return 'bg-emerald-700/60 text-white font-medium';
|
||||
return 'bg-zinc-800/80 text-zinc-400';
|
||||
}
|
||||
|
||||
export function HeatmapChart() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-4 shadow-sm overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Hourly Sales & Footfall Heatmap
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Sales volume intensity matrix mapped across days and operating hours
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-zinc-300">
|
||||
<span>Low</span>
|
||||
<div className="flex gap-1">
|
||||
<span className="size-3 rounded bg-zinc-800" />
|
||||
<span className="size-3 rounded bg-emerald-700/60" />
|
||||
<span className="size-3 rounded bg-emerald-600/80" />
|
||||
<span className="size-3 rounded bg-emerald-500" />
|
||||
</div>
|
||||
<span>Peak</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto w-full pt-1">
|
||||
<table className="w-full text-center text-xs border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 text-left text-zinc-400 font-bold">Day / Hour</th>
|
||||
{HEATMAP_HOURS.map((h) => (
|
||||
<th key={h} className="p-2 font-bold text-zinc-300">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{HEATMAP_DAYS.map((day) => (
|
||||
<tr key={day} className="border-t border-zinc-800/40">
|
||||
<td className="p-2 text-left font-bold text-white">{day}</td>
|
||||
{HEATMAP_HOURS.map((hour) => {
|
||||
const styleClass = getIntensityBg(day, hour);
|
||||
return (
|
||||
<td key={hour} className="p-1">
|
||||
<div
|
||||
className={`h-8 rounded flex items-center justify-center text-[11px] transition-all hover:scale-105 ${styleClass}`}
|
||||
>
|
||||
{hour === '18:00' ? '98%' : hour === '14:00' ? '74%' : '42%'}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
|
||||
const ORDER_FLOW = [
|
||||
{step: 'Customer', detail: 'Identified / Guest'},
|
||||
{step: 'Purchase', detail: 'Cart Checkout'},
|
||||
{step: 'Payment', detail: 'UPI / Card Auth'},
|
||||
{step: 'Order', detail: 'KDS & POS Created'},
|
||||
{step: 'Packing', detail: 'Fulfillment Prep'},
|
||||
{step: 'Delivery', detail: 'Dispatch / Pickup'},
|
||||
{step: 'Completed', detail: 'Ledger Settled'},
|
||||
];
|
||||
|
||||
const REVENUE_FLOW = [
|
||||
{step: 'Visitor', detail: 'Store / Web Traffic'},
|
||||
{step: 'Conversion', detail: 'Checkout Rate'},
|
||||
{step: 'Order', detail: 'Gross Volume'},
|
||||
{step: 'Revenue', detail: 'Net Payment'},
|
||||
{step: 'Profit', detail: 'Margin Settled'},
|
||||
];
|
||||
|
||||
const INVENTORY_FLOW = [
|
||||
{step: 'Supplier', detail: 'PO Placed'},
|
||||
{step: 'Warehouse', detail: 'Stock Received'},
|
||||
{step: 'Store', detail: 'Shelf Allocation'},
|
||||
{step: 'Customer', detail: 'Item Fulfilled'},
|
||||
];
|
||||
|
||||
export function OperationalFlowcharts() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-6 shadow-sm overflow-hidden">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Operational Flow Diagrams
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Visual operational pipeline lifecycle maps for orders, revenue settlements and inventory movement
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Order Flow Diagram */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-bold text-amber-400 uppercase tracking-wider">
|
||||
1. Order Lifecycle Flow
|
||||
</span>
|
||||
<div className="overflow-x-auto pb-2">
|
||||
<div className="flex items-center gap-2 min-w-[640px]">
|
||||
{ORDER_FLOW.map((node, i) => (
|
||||
<div key={node.step} className="flex items-center gap-2 flex-1">
|
||||
<div className="flex-1 rounded-lg border border-zinc-700 bg-zinc-900/90 p-2.5 text-center shadow-sm">
|
||||
<div className="text-xs font-bold text-white">{node.step}</div>
|
||||
<div className="text-[10px] text-zinc-400 font-medium mt-0.5">{node.detail}</div>
|
||||
</div>
|
||||
{i < ORDER_FLOW.length - 1 ? (
|
||||
<span className="text-zinc-500 font-bold text-sm shrink-0">→</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revenue Flow Diagram */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-bold text-emerald-400 uppercase tracking-wider">
|
||||
2. Revenue Settlement Flow
|
||||
</span>
|
||||
<div className="overflow-x-auto pb-2">
|
||||
<div className="flex items-center gap-2 min-w-[500px]">
|
||||
{REVENUE_FLOW.map((node, i) => (
|
||||
<div key={node.step} className="flex items-center gap-2 flex-1">
|
||||
<div className="flex-1 rounded-lg border border-emerald-950/60 bg-emerald-950/20 p-2.5 text-center shadow-sm">
|
||||
<div className="text-xs font-bold text-emerald-300">{node.step}</div>
|
||||
<div className="text-[10px] text-zinc-400 font-medium mt-0.5">{node.detail}</div>
|
||||
</div>
|
||||
{i < REVENUE_FLOW.length - 1 ? (
|
||||
<span className="text-emerald-600 font-bold text-sm shrink-0">→</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inventory Flow Diagram */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs font-bold text-indigo-400 uppercase tracking-wider">
|
||||
3. Inventory & Supply Chain Flow
|
||||
</span>
|
||||
<div className="overflow-x-auto pb-2">
|
||||
<div className="flex items-center gap-2 min-w-[450px]">
|
||||
{INVENTORY_FLOW.map((node, i) => (
|
||||
<div key={node.step} className="flex items-center gap-2 flex-1">
|
||||
<div className="flex-1 rounded-lg border border-indigo-950/60 bg-indigo-950/20 p-2.5 text-center shadow-sm">
|
||||
<div className="text-xs font-bold text-indigo-300">{node.step}</div>
|
||||
<div className="text-[10px] text-zinc-400 font-medium mt-0.5">{node.detail}</div>
|
||||
</div>
|
||||
{i < INVENTORY_FLOW.length - 1 ? (
|
||||
<span className="text-indigo-600 font-bold text-sm shrink-0">→</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/features/commerce/components/analytics/OrdersChart.tsx
Normal file
34
src/features/commerce/components/analytics/OrdersChart.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {BarChartView} from '@/shared/components/charts/BarChartView';
|
||||
import {ORDERS_TREND_DATA} from '../../services/commerceService';
|
||||
|
||||
export function OrdersChart() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-3 overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Orders Volume Trend
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Completed order count grouped by hourly buckets
|
||||
</Text>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-white bg-zinc-800 px-2.5 py-1 rounded-full border border-zinc-700">
|
||||
1,284 Orders Today
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-64 w-full pt-2">
|
||||
<BarChartView
|
||||
data={ORDERS_TREND_DATA}
|
||||
xKey="time"
|
||||
series={[{key: 'Orders', label: 'Completed Orders'}]}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
src/features/commerce/components/analytics/PaymentBreakdown.tsx
Normal file
162
src/features/commerce/components/analytics/PaymentBreakdown.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import {useState} from 'react';
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {PAYMENT_BREAKDOWN_DATA} from '../../services/commerceService';
|
||||
|
||||
export function PaymentBreakdown() {
|
||||
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
||||
|
||||
const activeItem = selectedMethod
|
||||
? PAYMENT_BREAKDOWN_DATA.find((p) => p.method === selectedMethod)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-4 shadow-sm overflow-hidden">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Payment Methods Breakdown
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Revenue share distribution by payment channel
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Interactive Channel Filter Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedMethod(null)}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-bold transition-all ${
|
||||
selectedMethod === null
|
||||
? 'bg-zinc-700 text-white shadow-sm'
|
||||
: 'bg-zinc-900/80 text-zinc-400 hover:text-white border border-zinc-800'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{PAYMENT_BREAKDOWN_DATA.map((item) => (
|
||||
<button
|
||||
key={item.method}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedMethod(selectedMethod === item.method ? null : item.method)
|
||||
}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-bold transition-all flex items-center gap-1.5 ${
|
||||
selectedMethod === item.method
|
||||
? 'text-white border shadow-sm'
|
||||
: 'bg-zinc-900/80 text-zinc-400 hover:text-white border border-zinc-800'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: selectedMethod === item.method ? item.color : undefined,
|
||||
backgroundColor: selectedMethod === item.method ? `${item.color}22` : undefined,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="size-2 rounded-full shrink-0"
|
||||
style={{backgroundColor: item.color}}
|
||||
/>
|
||||
{item.method.split(' ')[0]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual Ring / Stack Bar */}
|
||||
<div className="h-4 w-full rounded-full bg-zinc-800 overflow-hidden flex shadow-inner p-0.5">
|
||||
{PAYMENT_BREAKDOWN_DATA.map((item) => {
|
||||
const isSelected = selectedMethod === null || selectedMethod === item.method;
|
||||
return (
|
||||
<div
|
||||
key={item.method}
|
||||
onClick={() =>
|
||||
setSelectedMethod(selectedMethod === item.method ? null : item.method)
|
||||
}
|
||||
style={{
|
||||
width: `${item.percentage}%`,
|
||||
backgroundColor: item.color,
|
||||
opacity: isSelected ? 1 : 0.25,
|
||||
}}
|
||||
className="h-full cursor-pointer transition-all duration-300 first:rounded-l-full last:rounded-r-full hover:brightness-125"
|
||||
title={`${item.method}: ${item.percentage}% (${item.amount}) — Click to filter`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Interactive Breakdown Cards */}
|
||||
<div className="grid grid-cols-2 gap-3 pt-1">
|
||||
{PAYMENT_BREAKDOWN_DATA.map((item) => {
|
||||
const isSelected = selectedMethod === item.method;
|
||||
return (
|
||||
<div
|
||||
key={item.method}
|
||||
onClick={() =>
|
||||
setSelectedMethod(isSelected ? null : item.method)
|
||||
}
|
||||
className={`flex items-center justify-between p-3 rounded-xl cursor-pointer transition-all border ${
|
||||
isSelected
|
||||
? 'bg-zinc-800/90 shadow-md ring-1'
|
||||
: selectedMethod !== null
|
||||
? 'bg-zinc-900/40 border-zinc-800/60 opacity-60 hover:opacity-100'
|
||||
: 'bg-zinc-900/60 border-zinc-800 hover:border-zinc-700'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: isSelected ? item.color : undefined,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<span
|
||||
className="size-3.5 rounded-full shrink-0 shadow-sm"
|
||||
style={{backgroundColor: item.color}}
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-bold text-white block truncate">
|
||||
{item.method}
|
||||
</span>
|
||||
<span className="text-[10px] font-semibold text-zinc-400">
|
||||
{item.percentage}% share
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-xs font-bold text-white tabular-nums">
|
||||
{item.amount}
|
||||
</div>
|
||||
{isSelected ? (
|
||||
<Badge variant="success" label="Selected" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Selected Channel Deep-Dive Banner */}
|
||||
{activeItem ? (
|
||||
<div className="p-3 rounded-lg bg-zinc-900/90 border border-zinc-700 flex items-center justify-between text-xs animate-fadeIn">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="size-2.5 rounded-full"
|
||||
style={{backgroundColor: activeItem.color}}
|
||||
/>
|
||||
<span className="font-bold text-white">{activeItem.method} Channel Details:</span>
|
||||
<span className="text-zinc-300">
|
||||
{activeItem.amount} collected today across all active store POS terminals.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedMethod(null)}
|
||||
className="text-zinc-400 hover:text-white font-bold text-xs"
|
||||
>
|
||||
Clear Filter ✕
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/features/commerce/components/analytics/RevenueChart.tsx
Normal file
37
src/features/commerce/components/analytics/RevenueChart.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {AreaChartView} from '@/shared/components/charts/AreaChartView';
|
||||
import {REVENUE_TREND_DATA} from '../../services/commerceService';
|
||||
|
||||
export function RevenueChart() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-4 space-y-3 overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Revenue Trend
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Hourly revenue performance against targets
|
||||
</Text>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-full border border-emerald-500/20">
|
||||
+14.2% Above Target
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-64 w-full pt-2">
|
||||
<AreaChartView
|
||||
data={REVENUE_TREND_DATA}
|
||||
xKey="time"
|
||||
series={[
|
||||
{key: 'Revenue', label: 'Actual Revenue (₹)'},
|
||||
{key: 'Target', label: 'Baseline Target (₹)'},
|
||||
]}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
src/features/commerce/components/analytics/SalesFunnel.tsx
Normal file
62
src/features/commerce/components/analytics/SalesFunnel.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {SALES_FUNNEL_STAGES} from '../../services/commerceService';
|
||||
|
||||
export function SalesFunnel() {
|
||||
const maxCount = SALES_FUNNEL_STAGES[0]?.count || 1;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-4 shadow-sm overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Sales & Conversion Funnel
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Customer journey from initial store visit to completed transaction
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-sm font-bold text-emerald-400">23.8% Overall Conversion</span>
|
||||
<div className="text-xs text-zinc-400 font-medium">2,951 of 12,400 converted</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Funnel Visual Stack */}
|
||||
<div className="space-y-2.5 pt-2">
|
||||
{SALES_FUNNEL_STAGES.map((stage, idx) => {
|
||||
const widthPercent = Math.max(25, (stage.count / maxCount) * 100);
|
||||
return (
|
||||
<div key={stage.stage} className="space-y-1">
|
||||
<div className="flex items-center justify-between text-xs font-semibold">
|
||||
<span className="text-white font-bold flex items-center gap-2">
|
||||
<span className="size-5 rounded-full bg-zinc-800 border border-zinc-700 flex items-center justify-center text-[10px] text-zinc-300">
|
||||
{idx + 1}
|
||||
</span>
|
||||
{stage.stage}
|
||||
</span>
|
||||
<span className="text-zinc-300 tabular-nums">
|
||||
{stage.count.toLocaleString()} ({stage.conversionRate})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-zinc-900/80 rounded-lg p-1.5 border border-zinc-800">
|
||||
<div
|
||||
className="h-8 rounded-md transition-all duration-500 flex items-center justify-between px-3 text-xs font-bold text-white shadow-sm"
|
||||
style={{
|
||||
width: `${widthPercent}%`,
|
||||
backgroundColor: stage.color,
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{stage.label}</span>
|
||||
<span className="tabular-nums">{stage.count.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {STORE_PERFORMANCE_DATA} from '../../services/commerceService';
|
||||
|
||||
export function StorePerformanceChart() {
|
||||
const maxRevenue = Math.max(...STORE_PERFORMANCE_DATA.map((s) => s.Revenue)) || 1;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-4 shadow-sm overflow-hidden">
|
||||
<div>
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Top Performing Outlets
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Store revenue comparison across your network
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pt-1">
|
||||
{STORE_PERFORMANCE_DATA.map((store) => {
|
||||
const widthPercent = (store.Revenue / maxRevenue) * 100;
|
||||
return (
|
||||
<div key={store.store} className="space-y-1">
|
||||
<div className="flex justify-between items-center text-xs font-semibold">
|
||||
<span className="text-white font-bold">{store.store}</span>
|
||||
<span className="text-emerald-400 font-bold tabular-nums">
|
||||
₹{(store.Revenue / 100000).toFixed(1)}L
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-6 w-full rounded-md bg-zinc-900/80 p-0.5 border border-zinc-800">
|
||||
<div
|
||||
className="h-full rounded bg-gradient-to-r from-emerald-600 to-emerald-400 transition-all duration-500"
|
||||
style={{width: `${widthPercent}%`}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {TOP_PRODUCTS_DATA} from '../../services/commerceService';
|
||||
|
||||
export function TopProductsTable() {
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card overflow-hidden shadow-sm space-y-3">
|
||||
<div className="p-4 border-b border-border bg-card">
|
||||
<Heading level={4} className="text-base font-bold text-white">
|
||||
Top Selling Products Leaderboard
|
||||
</Heading>
|
||||
<Text size="sm" color="secondary">
|
||||
Product sales volume, revenue growth and real-time inventory counts
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto w-full">
|
||||
<table className="w-full text-left text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800 bg-zinc-900/60 text-white font-bold">
|
||||
<th className="px-4 py-3 text-left">Product Name</th>
|
||||
<th className="px-4 py-3 text-left">Category</th>
|
||||
<th className="px-4 py-3 text-right">Units Sold</th>
|
||||
<th className="px-4 py-3 text-right">Total Revenue</th>
|
||||
<th className="px-4 py-3 text-right">Growth</th>
|
||||
<th className="px-4 py-3 text-center">Stock Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800/60">
|
||||
{TOP_PRODUCTS_DATA.map((prod) => (
|
||||
<tr key={prod.id} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3.5 text-white font-semibold">{prod.name}</td>
|
||||
<td className="px-4 py-3.5 text-zinc-300 font-medium">{prod.category}</td>
|
||||
<td className="px-4 py-3.5 text-right text-white font-bold tabular-nums">{prod.unitsSold}</td>
|
||||
<td className="px-4 py-3.5 text-right text-white font-bold tabular-nums">{prod.revenue}</td>
|
||||
<td className="px-4 py-3.5 text-right font-bold tabular-nums">
|
||||
<span className={prod.growthDirection === 'up' ? 'text-emerald-400' : 'text-rose-400'}>
|
||||
{prod.growth}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3.5 text-center">
|
||||
<Badge
|
||||
variant={
|
||||
prod.stockStatus === 'in_stock'
|
||||
? 'success'
|
||||
: prod.stockStatus === 'low_stock'
|
||||
? 'warning'
|
||||
: 'error'
|
||||
}
|
||||
label={`${prod.stockCount} left`}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import {HStack} from '@astryxdesign/core/Layout';
|
||||
import {Button} from '@astryxdesign/core/Button';
|
||||
import {Icon} from '@astryxdesign/core/Icon';
|
||||
import {DownloadDropdown} from '@/shared/components/patterns/DownloadDropdown';
|
||||
import {ICONS} from '@/shared/utils/icons';
|
||||
import type {CommerceFilterState} from '../../types/commerce';
|
||||
|
||||
export function CommerceFilterToolbar({
|
||||
filters,
|
||||
onFilterChange,
|
||||
onExport,
|
||||
onCompare,
|
||||
}: {
|
||||
filters: CommerceFilterState;
|
||||
onFilterChange: (f: CommerceFilterState) => void;
|
||||
onExport: (format: 'pdf' | 'excel' | 'csv') => void;
|
||||
onCompare?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<HStack gap={2} vAlign="center" className="flex-wrap">
|
||||
{/* Date Range Selector */}
|
||||
<div className="flex items-center rounded-lg border border-border bg-card p-0.5 text-xs font-semibold">
|
||||
{(['today', '7d', '30d', '90d'] as const).map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => onFilterChange({...filters, dateRange: r})}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors capitalize ${
|
||||
filters.dateRange === r
|
||||
? 'bg-zinc-800 text-white font-bold'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{r === 'today' ? 'Today' : r === '7d' ? '7 Days' : r === '30d' ? '30 Days' : '90 Days'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Store Filter */}
|
||||
<select
|
||||
value={filters.storeId}
|
||||
onChange={(e) => onFilterChange({...filters, storeId: e.target.value})}
|
||||
className="h-8 rounded-lg border border-border bg-card px-2.5 text-xs font-semibold text-white outline-none focus:border-zinc-500"
|
||||
>
|
||||
<option value="all">All Active Stores</option>
|
||||
<option value="indiranagar">Indiranagar Flagship</option>
|
||||
<option value="koramangala">Koramangala 80ft</option>
|
||||
<option value="jayanagar">Jayanagar 4th Block</option>
|
||||
<option value="whitefield">Whitefield Main</option>
|
||||
</select>
|
||||
|
||||
{/* Compare Button */}
|
||||
{onCompare ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
label="Compare"
|
||||
icon={<Icon icon={ICONS.compare} size="sm" />}
|
||||
onClick={onCompare}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Export Dropdown */}
|
||||
<DownloadDropdown
|
||||
filename={`Commerce_Report_${new Date().toISOString().slice(0, 10)}`}
|
||||
title="Loyaly Commerce Performance Report"
|
||||
subtitle="Sales, revenue and order analytics"
|
||||
columns={[
|
||||
{key: 'metric', header: 'Metric Name'},
|
||||
{key: 'value', header: 'Value'},
|
||||
]}
|
||||
data={[
|
||||
{metric: "Today's Revenue", value: '₹3,40,000'},
|
||||
{metric: 'Total Orders', value: '1,284'},
|
||||
{metric: 'Average Order Value', value: '₹1,420'},
|
||||
]}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
89
src/features/commerce/hooks/useCommerce.ts
Normal file
89
src/features/commerce/hooks/useCommerce.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import type {CommerceFilterState} from '../types/commerce';
|
||||
import {
|
||||
BUSINESS_ALERTS,
|
||||
COMMERCE_KPIS,
|
||||
PAYMENT_BREAKDOWN_DATA,
|
||||
TOP_PRODUCTS_DATA,
|
||||
} from '../services/commerceService';
|
||||
import {exportData} from '@/shared/utils/export/exportManager';
|
||||
|
||||
const STORAGE_WIDTH_KEY = 'loyaly.commerce.pulsePanelWidth';
|
||||
const DEFAULT_PANEL_WIDTH = 340;
|
||||
|
||||
export function useCommerce() {
|
||||
const [filters, setFilters] = useState<CommerceFilterState>({
|
||||
dateRange: 'today',
|
||||
storeId: 'all',
|
||||
category: 'all',
|
||||
paymentMethod: 'all',
|
||||
salesChannel: 'all',
|
||||
});
|
||||
|
||||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||||
const [panelWidth, setPanelWidth] = useState(DEFAULT_PANEL_WIDTH);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_WIDTH_KEY);
|
||||
if (saved) {
|
||||
const parsed = parseInt(saved, 10);
|
||||
if (!isNaN(parsed) && parsed >= 280 && parsed <= 500) {
|
||||
setPanelWidth(parsed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updatePanelWidth = useCallback((width: number) => {
|
||||
const clamped = Math.max(280, Math.min(width, 500));
|
||||
setPanelWidth(clamped);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_WIDTH_KEY, clamped.toString());
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleExport = useCallback(async (format: 'pdf' | 'excel' | 'csv') => {
|
||||
const dataRows = [
|
||||
{metric: 'Total Revenue', value: COMMERCE_KPIS.revenue.value, change: COMMERCE_KPIS.revenue.change},
|
||||
{metric: 'Total Orders', value: COMMERCE_KPIS.orders.value, change: COMMERCE_KPIS.orders.change},
|
||||
{metric: 'Average Order Value', value: COMMERCE_KPIS.aov.value, change: COMMERCE_KPIS.aov.change},
|
||||
{metric: 'Net Sales', value: COMMERCE_KPIS.netSales.value, change: COMMERCE_KPIS.netSales.change},
|
||||
{metric: 'Refunds', value: COMMERCE_KPIS.refunds.value, change: COMMERCE_KPIS.refunds.change},
|
||||
{metric: 'Conversion Rate', value: COMMERCE_KPIS.conversion.value, change: COMMERCE_KPIS.conversion.change},
|
||||
];
|
||||
|
||||
await exportData({
|
||||
filename: `Commerce_Analytics_${new Date().toISOString().slice(0, 10)}`,
|
||||
title: 'Loyaly Commerce Business Intelligence Report',
|
||||
subtitle: `Generated on ${new Date().toLocaleDateString()} • Date Range: ${filters.dateRange.toUpperCase()}`,
|
||||
columns: [
|
||||
{key: 'metric', header: 'Metric Name'},
|
||||
{key: 'value', header: 'Current Value'},
|
||||
{key: 'change', header: 'Trend / Delta'},
|
||||
],
|
||||
data: dataRows,
|
||||
format,
|
||||
});
|
||||
}, [filters]);
|
||||
|
||||
return {
|
||||
filters,
|
||||
setFilters,
|
||||
isPanelOpen,
|
||||
setIsPanelOpen,
|
||||
panelWidth,
|
||||
updatePanelWidth,
|
||||
handleExport,
|
||||
kpis: COMMERCE_KPIS,
|
||||
alerts: BUSINESS_ALERTS,
|
||||
topProducts: TOP_PRODUCTS_DATA,
|
||||
paymentSplits: PAYMENT_BREAKDOWN_DATA,
|
||||
};
|
||||
}
|
||||
30
src/features/commerce/repositories/commerceRepository.ts
Normal file
30
src/features/commerce/repositories/commerceRepository.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
COMMERCE_KPIS,
|
||||
FORECAST_TREND_DATA,
|
||||
ORDERS_TREND_DATA,
|
||||
PAYMENT_BREAKDOWN_DATA,
|
||||
REVENUE_TREND_DATA,
|
||||
SALES_FUNNEL_STAGES,
|
||||
STORE_PERFORMANCE_DATA,
|
||||
TOP_PRODUCTS_DATA,
|
||||
} from '../services/commerceService';
|
||||
import type {CommerceFilterState} from '../types/commerce';
|
||||
|
||||
export class CommerceRepository {
|
||||
async getAnalytics(filters: CommerceFilterState) {
|
||||
// Simulates an API call
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
return {
|
||||
kpis: COMMERCE_KPIS,
|
||||
revenueTrend: REVENUE_TREND_DATA,
|
||||
ordersTrend: ORDERS_TREND_DATA,
|
||||
funnel: SALES_FUNNEL_STAGES,
|
||||
paymentBreakdown: PAYMENT_BREAKDOWN_DATA,
|
||||
storePerformance: STORE_PERFORMANCE_DATA,
|
||||
topProducts: TOP_PRODUCTS_DATA,
|
||||
forecast: FORECAST_TREND_DATA,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const commerceRepository = new CommerceRepository();
|
||||
128
src/features/commerce/services/commerceService.ts
Normal file
128
src/features/commerce/services/commerceService.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type {
|
||||
BusinessAlert,
|
||||
CommerceFilterState,
|
||||
ForecastDataPoint,
|
||||
FunnelStage,
|
||||
HeatmapCell,
|
||||
MetricCardData,
|
||||
PaymentBreakdownItem,
|
||||
TopProductRow,
|
||||
} from '../types/commerce';
|
||||
|
||||
export const COMMERCE_KPIS: Record<string, MetricCardData> = {
|
||||
revenue: {title: 'Total Revenue', value: '₹3,40,000', change: '+14.2% vs prev', trendDirection: 'up', subtitle: 'Net revenue across active stores'},
|
||||
orders: {title: 'Total Orders', value: '1,284', change: '+8.5%', trendDirection: 'up', subtitle: 'Completed order volume'},
|
||||
aov: {title: 'Average Order Value', value: '₹1,420', change: '+5.1%', trendDirection: 'up', subtitle: 'Average spend per cart'},
|
||||
netSales: {title: 'Net Sales', value: '₹3,18,000', change: '+13.9%', trendDirection: 'up', subtitle: 'Revenue after discounts'},
|
||||
refunds: {title: 'Refunds & Returns', value: '₹22,000', change: '-2.4%', trendDirection: 'down', subtitle: '6.4% total return rate'},
|
||||
conversion: {title: 'Store Conversion', value: '23.8%', change: '+1.4%', trendDirection: 'up', subtitle: 'Visits to purchase ratio'},
|
||||
topStore: {title: 'Top Performing Store', value: 'Indiranagar Flagship', change: '+18.4%', trendDirection: 'up', subtitle: '₹9.1L Revenue (23.8% Conv)'},
|
||||
};
|
||||
|
||||
export const REVENUE_TREND_DATA = [
|
||||
{time: '08:00', Revenue: 18000, Target: 15000},
|
||||
{time: '10:00', Revenue: 35000, Target: 30000},
|
||||
{time: '12:00', Revenue: 72000, Target: 60000},
|
||||
{time: '14:00', Revenue: 95000, Target: 80000},
|
||||
{time: '16:00', Revenue: 82000, Target: 75000},
|
||||
{time: '18:00', Revenue: 110000, Target: 90000},
|
||||
{time: '20:00', Revenue: 68000, Target: 60000},
|
||||
];
|
||||
|
||||
export const ORDERS_TREND_DATA = [
|
||||
{time: '08:00', Orders: 42},
|
||||
{time: '10:00', Orders: 88},
|
||||
{time: '12:00', Orders: 165},
|
||||
{time: '14:00', Orders: 210},
|
||||
{time: '16:00', Orders: 184},
|
||||
{time: '18:00', Orders: 245},
|
||||
{time: '20:00', Orders: 135},
|
||||
];
|
||||
|
||||
export const SALES_FUNNEL_STAGES: FunnelStage[] = [
|
||||
{stage: 'Visitors', count: 12400, label: 'Store & Web Visitors', conversionRate: '100%', color: '#3b82f6'},
|
||||
{stage: 'Product Views', count: 8680, label: 'Browsed Items', conversionRate: '70%', color: '#6366f1'},
|
||||
{stage: 'Add to Cart', count: 4340, label: 'Cart Created', conversionRate: '50%', color: '#8b5cf6'},
|
||||
{stage: 'Checkout Initiated', count: 3100, label: 'Entered Checkout', conversionRate: '71.4%', color: '#ec4899'},
|
||||
{stage: 'Purchased', count: 2951, label: 'Order Completed', conversionRate: '95.2%', color: '#10b981'},
|
||||
];
|
||||
|
||||
export const PAYMENT_BREAKDOWN_DATA: PaymentBreakdownItem[] = [
|
||||
{method: 'UPI', amount: '₹1,97,200', percentage: 58, color: '#10b981'},
|
||||
{method: 'Credit & Debit Cards', amount: '₹95,200', percentage: 28, color: '#3b82f6'},
|
||||
{method: 'Store Credit / LYT', amount: '₹34,000', percentage: 10, color: '#f59e0b'},
|
||||
{method: 'Cash & POS', amount: '₹13,600', percentage: 4, color: '#8b5cf6'},
|
||||
];
|
||||
|
||||
export const STORE_PERFORMANCE_DATA = [
|
||||
{store: 'Indiranagar Flagship', Revenue: 910000},
|
||||
{store: 'Koramangala 80ft', Revenue: 520000},
|
||||
{store: 'Jayanagar 4th Block', Revenue: 400000},
|
||||
{store: 'Whitefield Main', Revenue: 330000},
|
||||
];
|
||||
|
||||
export const TOP_PRODUCTS_DATA: TopProductRow[] = [
|
||||
{id: 'tp1', name: 'Signature Espresso Blend (1kg)', category: 'Beverage & Beans', unitsSold: 482, revenue: '₹1,44,600', growth: '+18.4%', growthDirection: 'up', stockCount: 142, stockStatus: 'in_stock'},
|
||||
{id: 'tp2', name: 'Artisanal Cold Brew Pack (6x)', category: 'Ready to Drink', unitsSold: 310, revenue: '₹93,000', growth: '+12.1%', growthDirection: 'up', stockCount: 18, stockStatus: 'low_stock'},
|
||||
{id: 'tp3', name: 'Ceramic Barista Mug (Matte Black)', category: 'Merchandise', unitsSold: 215, revenue: '₹75,250', growth: '+8.9%', growthDirection: 'up', stockCount: 8, stockStatus: 'low_stock'},
|
||||
{id: 'tp4', name: 'Vanilla Bean Syrup (750ml)', category: 'Syrups', unitsSold: 184, revenue: '₹46,000', growth: '+5.4%', growthDirection: 'up', stockCount: 85, stockStatus: 'in_stock'},
|
||||
{id: 'tp5', name: 'French Press Brewer (8-Cup)', category: 'Equipment', unitsSold: 94, revenue: '₹37,600', growth: '-2.1%', growthDirection: 'down', stockCount: 0, stockStatus: 'out_of_stock'},
|
||||
];
|
||||
|
||||
export const HEATMAP_HOURS = ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00'];
|
||||
export const HEATMAP_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
|
||||
export const HEATMAP_DATA: HeatmapCell[] = [
|
||||
{day: 'Mon', hour: '12:00', value: 65},
|
||||
{day: 'Mon', hour: '18:00', value: 85},
|
||||
{day: 'Fri', hour: '18:00', value: 98},
|
||||
{day: 'Sat', hour: '14:00', value: 92},
|
||||
{day: 'Sat', hour: '18:00', value: 100},
|
||||
{day: 'Sun', hour: '12:00', value: 78},
|
||||
{day: 'Sun', hour: '18:00', value: 95},
|
||||
];
|
||||
|
||||
export const FORECAST_TREND_DATA: ForecastDataPoint[] = [
|
||||
{date: 'Mon', actual: 280000, forecast: 275000, lowerBound: 260000, upperBound: 290000},
|
||||
{date: 'Tue', actual: 310000, forecast: 300000, lowerBound: 285000, upperBound: 315000},
|
||||
{date: 'Wed', actual: 295000, forecast: 290000, lowerBound: 275000, upperBound: 305000},
|
||||
{date: 'Thu', actual: 340000, forecast: 330000, lowerBound: 310000, upperBound: 350000},
|
||||
{date: 'Fri (Est)', forecast: 380000, lowerBound: 355000, upperBound: 405000},
|
||||
{date: 'Sat (Est)', forecast: 420000, lowerBound: 390000, upperBound: 450000},
|
||||
{date: 'Sun (Est)', forecast: 390000, lowerBound: 360000, upperBound: 420000},
|
||||
];
|
||||
|
||||
export const BUSINESS_ALERTS: BusinessAlert[] = [
|
||||
{
|
||||
id: 'ba1',
|
||||
type: 'low_stock',
|
||||
title: 'Low Stock Reorder Alert',
|
||||
description: 'Ceramic Barista Mug has only 8 units left at Koramangala 80ft.',
|
||||
actionText: 'Reorder Stock',
|
||||
severity: 'critical',
|
||||
},
|
||||
{
|
||||
id: 'ba2',
|
||||
type: 'refund_spike',
|
||||
title: 'Refund Spike Detected',
|
||||
description: 'Whitefield store processed 4 return requests in the last 2 hours.',
|
||||
actionText: 'Review Return Audit',
|
||||
severity: 'warning',
|
||||
},
|
||||
{
|
||||
id: 'ba3',
|
||||
type: 'best_seller',
|
||||
title: 'Best Seller Momentum',
|
||||
description: 'Signature Espresso Blend sales spiked +18.4% above weekly baseline.',
|
||||
actionText: 'Promote Banner',
|
||||
severity: 'success',
|
||||
},
|
||||
{
|
||||
id: 'ba4',
|
||||
type: 'promotion_opportunity',
|
||||
title: 'Weekend Upsell Opportunity',
|
||||
description: 'Combine Cold Brew Pack with Syrups for a +₹250 AOV expansion.',
|
||||
actionText: 'Launch Campaign',
|
||||
severity: 'info',
|
||||
},
|
||||
];
|
||||
72
src/features/commerce/types/commerce.ts
Normal file
72
src/features/commerce/types/commerce.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export interface CommerceFilterState {
|
||||
dateRange: 'today' | '7d' | '30d' | '90d' | 'custom';
|
||||
storeId: string;
|
||||
category: string;
|
||||
paymentMethod: string;
|
||||
salesChannel: string;
|
||||
}
|
||||
|
||||
export interface MetricCardData {
|
||||
title: string;
|
||||
value: string;
|
||||
change: string;
|
||||
trendDirection: 'up' | 'down' | 'neutral';
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export interface FunnelStage {
|
||||
stage: string;
|
||||
count: number;
|
||||
label: string;
|
||||
conversionRate: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface PaymentBreakdownItem {
|
||||
method: string;
|
||||
amount: string;
|
||||
percentage: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface TopProductRow {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
unitsSold: number;
|
||||
revenue: string;
|
||||
growth: string;
|
||||
growthDirection: 'up' | 'down';
|
||||
stockCount: number;
|
||||
stockStatus: 'in_stock' | 'low_stock' | 'out_of_stock';
|
||||
}
|
||||
|
||||
export interface HeatmapCell {
|
||||
day: string;
|
||||
hour: string;
|
||||
value: number; // intensity 0 - 100
|
||||
}
|
||||
|
||||
export interface ForecastDataPoint {
|
||||
date: string;
|
||||
actual?: number;
|
||||
forecast: number;
|
||||
lowerBound?: number;
|
||||
upperBound?: number;
|
||||
}
|
||||
|
||||
export interface BusinessAlert {
|
||||
id: string;
|
||||
type: 'low_stock' | 'revenue_drop' | 'refund_spike' | 'best_seller' | 'promotion_opportunity';
|
||||
title: string;
|
||||
description: string;
|
||||
actionText: string;
|
||||
severity: 'critical' | 'warning' | 'info' | 'success';
|
||||
}
|
||||
|
||||
export interface OperationalFlowStep {
|
||||
step: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
iconName: string;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export function MetricCard({
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack gap={2} vAlign="center" hAlign="between">
|
||||
<HStack gap={2} vAlign="center" hAlign="between" className="min-w-0 flex-wrap gap-y-1">
|
||||
{/*
|
||||
Text, NOT Heading. A KPI value is data, not a section title —
|
||||
marking it up as an h2 puts "32.7k" into the document outline
|
||||
@@ -66,11 +66,13 @@ export function MetricCard({
|
||||
numbers. `display-3` gives the visual weight without the
|
||||
semantics.
|
||||
*/}
|
||||
<Text type="display-3">
|
||||
<Text type="display-3" className="truncate shrink min-w-0">
|
||||
<AnimatedNumber value={value} format={format} />
|
||||
</Text>
|
||||
{deltaPct !== undefined ? (
|
||||
<div className="shrink-0">
|
||||
<MetricDelta value={deltaPct} isRiseGood={isRiseGood} />
|
||||
</div>
|
||||
) : null}
|
||||
</HStack>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface NavEntry {
|
||||
*/
|
||||
export const PRIMARY_NAV: NavEntry[] = [
|
||||
{label: 'Dashboard', href: '/dashboard', icon: ICONS.dashboard},
|
||||
{label: 'Commerce', href: '/commerce', icon: ICONS.commerce},
|
||||
{label: 'Store', href: '/stores', icon: ICONS.stores},
|
||||
{label: 'Lyts', href: '/lyts', icon: ICONS.lyts},
|
||||
{label: 'Leaderboard', href: '/staff', icon: ICONS.staff},
|
||||
|
||||
@@ -78,6 +78,7 @@ export const ICONS = {
|
||||
minimize: Minimize2,
|
||||
// Navigation
|
||||
dashboard: LayoutDashboard,
|
||||
commerce: ShoppingCart,
|
||||
stores: Store,
|
||||
lyts: Gift,
|
||||
staff: Users,
|
||||
|
||||
Reference in New Issue
Block a user