fix sales issue
This commit is contained in:
@@ -4,40 +4,45 @@ 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';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {getCommerceKpis} from '../services/commerceService';
|
||||
|
||||
export function CommerceKpiRow() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const kpis = getCommerceKpis(storeId, range);
|
||||
|
||||
const kpiList = [
|
||||
{
|
||||
label: 'Total Revenue',
|
||||
value: 340000,
|
||||
value: kpis.revenue.rawValue,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.revenue,
|
||||
deltaPct: 14.2,
|
||||
},
|
||||
{
|
||||
label: 'Total Orders',
|
||||
value: 1284,
|
||||
value: kpis.orders.rawValue,
|
||||
format: formatCount,
|
||||
icon: ICONS.purchases,
|
||||
deltaPct: 8.5,
|
||||
},
|
||||
{
|
||||
label: 'Avg Order Value',
|
||||
value: 1420,
|
||||
value: kpis.aov.rawValue,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.purchases,
|
||||
deltaPct: 5.1,
|
||||
},
|
||||
{
|
||||
label: 'Net Sales',
|
||||
value: 318000,
|
||||
value: kpis.netSales.rawValue,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.revenue,
|
||||
deltaPct: 13.9,
|
||||
},
|
||||
{
|
||||
label: 'Refunds',
|
||||
value: 22000,
|
||||
value: kpis.refunds.rawValue,
|
||||
format: formatInrCompact,
|
||||
icon: ICONS.alert,
|
||||
deltaPct: -2.4,
|
||||
@@ -45,7 +50,7 @@ export function CommerceKpiRow() {
|
||||
},
|
||||
{
|
||||
label: 'Conversion',
|
||||
value: 23.8,
|
||||
value: kpis.conversion.rawValue,
|
||||
format: (v: number) => formatPct(v, 1),
|
||||
icon: ICONS.conversion,
|
||||
deltaPct: 1.4,
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {MetricCard} from '@/shared/components/patterns/MetricCard';
|
||||
import {formatInrCompact} from '@/shared/utils/format';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {getCommerceKpis} from '../services/commerceService';
|
||||
|
||||
export function CommerceRevenueSummary() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const kpis = getCommerceKpis(storeId, range);
|
||||
|
||||
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">
|
||||
@@ -19,26 +24,26 @@ export function CommerceRevenueSummary() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<MetricCard
|
||||
label="Gross Sales"
|
||||
value={362000}
|
||||
value={kpis.grossSales.rawValue}
|
||||
format={formatInrCompact}
|
||||
deltaPct={12.8}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Discounts & Promos"
|
||||
value={22000}
|
||||
value={kpis.discounts.rawValue}
|
||||
format={formatInrCompact}
|
||||
deltaPct={-4.1}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Refunds Processed"
|
||||
value={22000}
|
||||
value={kpis.refunds.rawValue}
|
||||
format={formatInrCompact}
|
||||
deltaPct={-2.4}
|
||||
isRiseGood={false}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Net Realized Revenue"
|
||||
value={318000}
|
||||
value={kpis.netSales.rawValue}
|
||||
format={formatInrCompact}
|
||||
deltaPct={13.9}
|
||||
/>
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {BarChartView} from '@/shared/components/charts/BarChartView';
|
||||
import {ORDERS_TREND_DATA} from '../../services/commerceService';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {getOrdersTrendData} from '../../services/commerceService';
|
||||
|
||||
export function OrdersChart() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const data = getOrdersTrendData(storeId, range);
|
||||
|
||||
const totalOrders = data.reduce((acc, curr) => acc + curr.Orders, 0);
|
||||
|
||||
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">
|
||||
@@ -17,13 +23,13 @@ export function OrdersChart() {
|
||||
</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
|
||||
{totalOrders.toLocaleString()} Orders
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-64 w-full pt-2">
|
||||
<BarChartView
|
||||
data={ORDERS_TREND_DATA}
|
||||
data={data}
|
||||
xKey="time"
|
||||
series={[{key: 'Orders', label: 'Completed Orders'}]}
|
||||
height="100%"
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
import {useState} from 'react';
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {PAYMENT_BREAKDOWN_DATA} from '../../services/commerceService';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {getPaymentBreakdownData} from '../../services/commerceService';
|
||||
|
||||
export function PaymentBreakdown() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const paymentData = getPaymentBreakdownData(storeId, range);
|
||||
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
||||
|
||||
const activeItem = selectedMethod
|
||||
? PAYMENT_BREAKDOWN_DATA.find((p) => p.method === selectedMethod)
|
||||
? paymentData.find((p) => p.method === selectedMethod)
|
||||
: null;
|
||||
|
||||
return (
|
||||
@@ -37,7 +40,7 @@ export function PaymentBreakdown() {
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{PAYMENT_BREAKDOWN_DATA.map((item) => (
|
||||
{paymentData.map((item) => (
|
||||
<button
|
||||
key={item.method}
|
||||
type="button"
|
||||
@@ -66,7 +69,7 @@ export function PaymentBreakdown() {
|
||||
|
||||
{/* 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) => {
|
||||
{paymentData.map((item) => {
|
||||
const isSelected = selectedMethod === null || selectedMethod === item.method;
|
||||
return (
|
||||
<div
|
||||
@@ -88,7 +91,7 @@ export function PaymentBreakdown() {
|
||||
|
||||
{/* Interactive Breakdown Cards */}
|
||||
<div className="grid grid-cols-2 gap-3 pt-1">
|
||||
{PAYMENT_BREAKDOWN_DATA.map((item) => {
|
||||
{paymentData.map((item) => {
|
||||
const isSelected = selectedMethod === item.method;
|
||||
return (
|
||||
<div
|
||||
@@ -145,7 +148,7 @@ export function PaymentBreakdown() {
|
||||
/>
|
||||
<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.
|
||||
{activeItem.amount} collected across selected scope terminals.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {AreaChartView} from '@/shared/components/charts/AreaChartView';
|
||||
import {REVENUE_TREND_DATA} from '../../services/commerceService';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {getRevenueTrendData} from '../../services/commerceService';
|
||||
|
||||
export function RevenueChart() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const data = getRevenueTrendData(storeId, range);
|
||||
|
||||
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">
|
||||
@@ -23,7 +27,7 @@ export function RevenueChart() {
|
||||
|
||||
<div className="h-64 w-full pt-2">
|
||||
<AreaChartView
|
||||
data={REVENUE_TREND_DATA}
|
||||
data={data}
|
||||
xKey="time"
|
||||
series={[
|
||||
{key: 'Revenue', label: 'Actual Revenue (₹)'},
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
import {Heading, Text} from '@astryxdesign/core/Text';
|
||||
import {Badge} from '@astryxdesign/core/Badge';
|
||||
import {TOP_PRODUCTS_DATA} from '../../services/commerceService';
|
||||
import {useWorkspace} from '@/shared/providers/WorkspaceProvider';
|
||||
import {getTopProductsData} from '../../services/commerceService';
|
||||
|
||||
export function TopProductsTable() {
|
||||
const {storeId, range} = useWorkspace();
|
||||
const products = getTopProductsData(storeId, range);
|
||||
|
||||
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">
|
||||
@@ -29,11 +33,11 @@ export function TopProductsTable() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800/60">
|
||||
{TOP_PRODUCTS_DATA.map((prod) => (
|
||||
{products.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.unitsSold.toLocaleString()}</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'}>
|
||||
|
||||
@@ -4,9 +4,9 @@ import {useCallback, useEffect, useState} from 'react';
|
||||
import type {CommerceFilterState} from '../types/commerce';
|
||||
import {
|
||||
BUSINESS_ALERTS,
|
||||
COMMERCE_KPIS,
|
||||
PAYMENT_BREAKDOWN_DATA,
|
||||
TOP_PRODUCTS_DATA,
|
||||
getCommerceKpis,
|
||||
getPaymentBreakdownData,
|
||||
getTopProductsData,
|
||||
} from '../services/commerceService';
|
||||
import {exportData} from '@/shared/utils/export/exportManager';
|
||||
|
||||
@@ -49,14 +49,16 @@ export function useCommerce() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const kpis = getCommerceKpis(filters.storeId, filters.dateRange);
|
||||
|
||||
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},
|
||||
{metric: 'Total Revenue', value: kpis.revenue.title, change: kpis.revenue.change},
|
||||
{metric: 'Total Orders', value: kpis.orders.title, change: kpis.orders.change},
|
||||
{metric: 'Average Order Value', value: kpis.aov.title, change: kpis.aov.change},
|
||||
{metric: 'Net Sales', value: kpis.netSales.title, change: kpis.netSales.change},
|
||||
{metric: 'Refunds', value: kpis.refunds.title, change: kpis.refunds.change},
|
||||
{metric: 'Conversion Rate', value: kpis.conversion.title, change: kpis.conversion.change},
|
||||
];
|
||||
|
||||
await exportData({
|
||||
@@ -71,7 +73,7 @@ export function useCommerce() {
|
||||
data: dataRows,
|
||||
format,
|
||||
});
|
||||
}, [filters]);
|
||||
}, [filters, kpis]);
|
||||
|
||||
return {
|
||||
filters,
|
||||
@@ -81,9 +83,9 @@ export function useCommerce() {
|
||||
panelWidth,
|
||||
updatePanelWidth,
|
||||
handleExport,
|
||||
kpis: COMMERCE_KPIS,
|
||||
kpis,
|
||||
alerts: BUSINESS_ALERTS,
|
||||
topProducts: TOP_PRODUCTS_DATA,
|
||||
paymentSplits: PAYMENT_BREAKDOWN_DATA,
|
||||
topProducts: getTopProductsData(filters.storeId, filters.dateRange),
|
||||
paymentSplits: getPaymentBreakdownData(filters.storeId, filters.dateRange),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
import {
|
||||
COMMERCE_KPIS,
|
||||
FORECAST_TREND_DATA,
|
||||
ORDERS_TREND_DATA,
|
||||
PAYMENT_BREAKDOWN_DATA,
|
||||
REVENUE_TREND_DATA,
|
||||
SALES_FUNNEL_STAGES,
|
||||
STORE_PERFORMANCE_DATA,
|
||||
TOP_PRODUCTS_DATA,
|
||||
getCommerceKpis,
|
||||
getOrdersTrendData,
|
||||
getPaymentBreakdownData,
|
||||
getRevenueTrendData,
|
||||
getTopProductsData,
|
||||
} 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));
|
||||
const storeId = filters.storeId || 'all';
|
||||
const range = filters.dateRange || '7d';
|
||||
|
||||
return {
|
||||
kpis: COMMERCE_KPIS,
|
||||
revenueTrend: REVENUE_TREND_DATA,
|
||||
ordersTrend: ORDERS_TREND_DATA,
|
||||
funnel: SALES_FUNNEL_STAGES,
|
||||
paymentBreakdown: PAYMENT_BREAKDOWN_DATA,
|
||||
kpis: getCommerceKpis(storeId, range),
|
||||
revenueTrend: getRevenueTrendData(storeId, range),
|
||||
ordersTrend: getOrdersTrendData(storeId, range),
|
||||
paymentBreakdown: getPaymentBreakdownData(storeId, range),
|
||||
storePerformance: STORE_PERFORMANCE_DATA,
|
||||
topProducts: TOP_PRODUCTS_DATA,
|
||||
forecast: FORECAST_TREND_DATA,
|
||||
topProducts: getTopProductsData(storeId, range),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,139 @@
|
||||
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)'},
|
||||
const STORE_MULTIPLIERS: Record<string, number> = {
|
||||
all: 1.0,
|
||||
'blr-indiranagar': 0.42,
|
||||
'blr-koramangala': 0.28,
|
||||
'blr-whitefield': 0.18,
|
||||
'che-anna-nagar': 0.12,
|
||||
'hyd-jubilee': 0.1,
|
||||
};
|
||||
|
||||
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},
|
||||
];
|
||||
const RANGE_MULTIPLIERS: Record<string, number> = {
|
||||
'7d': 1.0,
|
||||
'30d': 3.8,
|
||||
'90d': 11.2,
|
||||
mtd: 2.1,
|
||||
ytd: 24.5,
|
||||
custom: 1.5,
|
||||
};
|
||||
|
||||
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},
|
||||
];
|
||||
function getMult(storeId: string, range: string): {storeMult: number; rangeMult: number; totalMult: number} {
|
||||
const storeMult = STORE_MULTIPLIERS[storeId] ?? 1.0;
|
||||
const rangeMult = RANGE_MULTIPLIERS[range] ?? 1.0;
|
||||
return {storeMult, rangeMult, totalMult: storeMult * rangeMult};
|
||||
}
|
||||
|
||||
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 function getCommerceKpis(storeId = 'all', range = '7d') {
|
||||
const {totalMult, storeMult} = getMult(storeId, range);
|
||||
|
||||
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'},
|
||||
];
|
||||
const baseRevenue = 340000 * totalMult;
|
||||
const baseOrders = Math.round(1284 * totalMult);
|
||||
const baseAov = Math.round(1420 * (0.9 + storeMult * 0.25));
|
||||
const baseNetSales = 318000 * totalMult;
|
||||
const baseRefunds = 22000 * totalMult;
|
||||
const baseConversion = Number((23.8 * (0.85 + storeMult * 0.3)).toFixed(1));
|
||||
|
||||
return {
|
||||
revenue: {
|
||||
title: 'Total Revenue',
|
||||
rawValue: baseRevenue,
|
||||
change: '+14.2% vs prev',
|
||||
trendDirection: 'up' as const,
|
||||
subtitle: 'Net revenue across selected store',
|
||||
},
|
||||
orders: {
|
||||
title: 'Total Orders',
|
||||
rawValue: baseOrders,
|
||||
change: '+8.5%',
|
||||
trendDirection: 'up' as const,
|
||||
subtitle: 'Completed order volume',
|
||||
},
|
||||
aov: {
|
||||
title: 'Avg Order Value',
|
||||
rawValue: baseAov,
|
||||
change: '+5.1%',
|
||||
trendDirection: 'up' as const,
|
||||
subtitle: 'Average spend per cart',
|
||||
},
|
||||
netSales: {
|
||||
title: 'Net Sales',
|
||||
rawValue: baseNetSales,
|
||||
change: '+13.9%',
|
||||
trendDirection: 'up' as const,
|
||||
subtitle: 'Revenue after discounts',
|
||||
},
|
||||
refunds: {
|
||||
title: 'Refunds',
|
||||
rawValue: baseRefunds,
|
||||
change: '-2.4%',
|
||||
trendDirection: 'down' as const,
|
||||
subtitle: 'Total returns rate',
|
||||
},
|
||||
conversion: {
|
||||
title: 'Conversion',
|
||||
rawValue: baseConversion,
|
||||
change: '+1.4%',
|
||||
trendDirection: 'up' as const,
|
||||
subtitle: 'Visits to purchase ratio',
|
||||
},
|
||||
grossSales: {
|
||||
title: 'Gross Sales',
|
||||
rawValue: baseRevenue * 1.065,
|
||||
change: '+12.8%',
|
||||
trendDirection: 'up' as const,
|
||||
},
|
||||
discounts: {
|
||||
title: 'Discounts & Promos',
|
||||
rawValue: baseRefunds,
|
||||
change: '-4.1%',
|
||||
trendDirection: 'down' as const,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getRevenueTrendData(storeId = 'all', range = '7d') {
|
||||
const {totalMult} = getMult(storeId, range);
|
||||
return [
|
||||
{time: '08:00', Revenue: Math.round(18000 * totalMult), Target: Math.round(15000 * totalMult)},
|
||||
{time: '10:00', Revenue: Math.round(35000 * totalMult), Target: Math.round(30000 * totalMult)},
|
||||
{time: '12:00', Revenue: Math.round(72000 * totalMult), Target: Math.round(60000 * totalMult)},
|
||||
{time: '14:00', Revenue: Math.round(95000 * totalMult), Target: Math.round(80000 * totalMult)},
|
||||
{time: '16:00', Revenue: Math.round(82000 * totalMult), Target: Math.round(75000 * totalMult)},
|
||||
{time: '18:00', Revenue: Math.round(110000 * totalMult), Target: Math.round(90000 * totalMult)},
|
||||
{time: '20:00', Revenue: Math.round(68000 * totalMult), Target: Math.round(60000 * totalMult)},
|
||||
];
|
||||
}
|
||||
|
||||
export function getOrdersTrendData(storeId = 'all', range = '7d') {
|
||||
const {totalMult} = getMult(storeId, range);
|
||||
return [
|
||||
{time: '08:00', Orders: Math.max(1, Math.round(42 * totalMult))},
|
||||
{time: '10:00', Orders: Math.max(1, Math.round(88 * totalMult))},
|
||||
{time: '12:00', Orders: Math.max(1, Math.round(165 * totalMult))},
|
||||
{time: '14:00', Orders: Math.max(1, Math.round(210 * totalMult))},
|
||||
{time: '16:00', Orders: Math.max(1, Math.round(184 * totalMult))},
|
||||
{time: '18:00', Orders: Math.max(1, Math.round(245 * totalMult))},
|
||||
{time: '20:00', Orders: Math.max(1, Math.round(135 * totalMult))},
|
||||
];
|
||||
}
|
||||
|
||||
export function getPaymentBreakdownData(storeId = 'all', range = '7d'): PaymentBreakdownItem[] {
|
||||
const {totalMult} = getMult(storeId, range);
|
||||
const totalInr = 340000 * totalMult;
|
||||
|
||||
return [
|
||||
{method: 'UPI', amount: `₹${(totalInr * 0.58).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, percentage: 58, color: '#10b981'},
|
||||
{method: 'Credit & Debit Cards', amount: `₹${(totalInr * 0.28).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, percentage: 28, color: '#3b82f6'},
|
||||
{method: 'Store Credit / LYT', amount: `₹${(totalInr * 0.10).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, percentage: 10, color: '#f59e0b'},
|
||||
{method: 'Cash & POS', amount: `₹${(totalInr * 0.04).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, percentage: 4, color: '#8b5cf6'},
|
||||
];
|
||||
}
|
||||
|
||||
export const STORE_PERFORMANCE_DATA = [
|
||||
{store: 'Indiranagar Flagship', Revenue: 910000},
|
||||
@@ -61,36 +142,16 @@ export const STORE_PERFORMANCE_DATA = [
|
||||
{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 function getTopProductsData(storeId = 'all', range = '7d'): TopProductRow[] {
|
||||
const {totalMult} = getMult(storeId, range);
|
||||
return [
|
||||
{id: 'tp1', name: 'Signature Espresso Blend (1kg)', category: 'Beverage & Beans', unitsSold: Math.round(482 * totalMult), revenue: `₹${(144600 * totalMult).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, growth: '+18.4%', growthDirection: 'up', stockCount: 142, stockStatus: 'in_stock'},
|
||||
{id: 'tp2', name: 'Artisanal Cold Brew Pack (6x)', category: 'Ready to Drink', unitsSold: Math.round(310 * totalMult), revenue: `₹${(93000 * totalMult).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, growth: '+12.1%', growthDirection: 'up', stockCount: 18, stockStatus: 'low_stock'},
|
||||
{id: 'tp3', name: 'Ceramic Barista Mug (Matte Black)', category: 'Merchandise', unitsSold: Math.round(215 * totalMult), revenue: `₹${(75250 * totalMult).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, growth: '+8.9%', growthDirection: 'up', stockCount: 8, stockStatus: 'low_stock'},
|
||||
{id: 'tp4', name: 'Vanilla Bean Syrup (750ml)', category: 'Syrups', unitsSold: Math.round(184 * totalMult), revenue: `₹${(46000 * totalMult).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, growth: '+5.4%', growthDirection: 'up', stockCount: 85, stockStatus: 'in_stock'},
|
||||
{id: 'tp5', name: 'French Press Brewer (8-Cup)', category: 'Equipment', unitsSold: Math.round(94 * totalMult), revenue: `₹${(37600 * totalMult).toLocaleString('en-IN', {maximumFractionDigits: 0})}`, growth: '-2.1%', growthDirection: 'down', stockCount: 0, stockStatus: 'out_of_stock'},
|
||||
];
|
||||
}
|
||||
|
||||
export const BUSINESS_ALERTS: BusinessAlert[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user