ifixkart-admin/app/(admin)/products/dashboard/page.tsx

662 lines
28 KiB
TypeScript

'use client';
import { useState, useEffect, useMemo } from 'react';
import {
Package, Tag, RefreshCw, CheckCircle,
DollarSign, Calendar, ChevronDown,
ListFilter, ArrowUpRight, ArrowDownRight,
} from 'lucide-react';
import { toast } from 'sonner';
import { useTheme } from 'next-themes';
import type { ApexOptions } from 'apexcharts';
import {
catalogService,
ProductResponse,
CategoryResponse,
BrandResponse,
} from '@/services/api/catalogService';
import { ApexChart } from '@/components/charts/ApexChart';
import { cn } from '@/lib/utils';
import { apiFetch } from '@/services/api/client';
const PAGE_DATE_RANGE = '26 Jan 2026 - 26 Jan 2027';
const CARD_DATE_RANGE = '26 Jan 2026 - 26 Jan 2027';
const HARDCODED = {
mtdValue: '$18,50,800.00',
ytdValue: '$85,25,800.00',
conversion: 55.6,
mtdTrend: '+2.5%',
ytdTrend: '-5.0%',
conversionTrend: '+2.5%',
wonVsLost: '+15% vs last month',
won: 68,
lost: 16,
wonTrend: '+2.5%',
lostTrend: '-5.8%',
lastWeek: 'Last Week',
pipelineTotal: '$2,56,054.50',
pipelineTrend: '+2.5%',
pipeline: [
{ label: 'Probability', value: '$50,000', pct: 60, fill: '#7C3AED' },
{ label: 'Proposal Sent', value: '$56,054', pct: 70, fill: '#01B574' },
{ label: 'Opportunity', value: '$1,00,000', pct: 80, fill: '#5BA4C7' },
{ label: 'Total Deals', value: '$1,00,000', pct: 90, fill: '#1B2559' },
],
sparkMtd: [18, 28, 22, 36, 24, 42, 30],
sparkYtd: [22, 16, 30, 20, 34, 26, 40],
};
const CARD =
'min-w-0 overflow-hidden bg-card border border-border rounded-[5px] shadow-[0_1px_2px_rgba(16,24,40,0.04)]';
function PipelineStage({
label,
value,
pct,
fill,
isDark,
}: {
label: string;
value: string;
pct: number;
fill: string;
isDark: boolean;
}) {
const remainder = isDark ? `${fill}26` : `${fill}24`;
const stripe = isDark ? `${fill}66` : `${fill}40`;
return (
<div className="relative h-8 rounded-[6px] overflow-hidden">
<div
className="absolute inset-0"
style={{
backgroundColor: remainder,
backgroundImage: `repeating-linear-gradient(-45deg, transparent, transparent 5px, ${stripe} 5px, ${stripe} 7px)`,
}}
/>
<div
className="absolute inset-y-0 left-0 rounded-[6px]"
style={{ width: `${pct}%`, backgroundColor: fill }}
/>
<div className="relative z-[1] h-full px-3 flex items-center">
<span className="text-[12px] font-medium text-white truncate [text-shadow:0_1px_1px_rgba(0,0,0,0.25)]">
{label} - {value}
</span>
</div>
</div>
);
}
function WonLostRings({
values,
track,
}: {
values: number[];
track: string;
}) {
const size = 180;
const cx = size / 2;
const cy = size / 2;
const stroke = 11;
const radii = [74, 54, 34];
const colors = ['#5BA4C7', '#3538CD', '#1B2559'];
const arc = (r: number, pct: number) => {
const start = -Math.PI / 2;
const sweep = (Math.min(Math.max(pct, 0.01), 99.999) / 100) * Math.PI * 2;
const end = start + sweep;
const x1 = cx + r * Math.cos(start);
const y1 = cy + r * Math.sin(start);
const x2 = cx + r * Math.cos(end);
const y2 = cy + r * Math.sin(end);
const large = sweep > Math.PI ? 1 : 0;
return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
};
return (
<svg viewBox={`0 0 ${size} ${size}`} className="w-full h-full max-w-[180px] max-h-[180px]">
{radii.map((r, i) => (
<g key={colors[i]}>
<circle
cx={cx}
cy={cy}
r={r}
fill="none"
stroke={track}
strokeWidth={stroke}
/>
<path
d={arc(r, values[i] ?? 0)}
fill="none"
stroke={colors[i]}
strokeWidth={stroke}
strokeLinecap="round"
/>
</g>
))}
</svg>
);
}
function ConversionGauge({
value,
isDark,
labelColor,
}: {
value: number;
isDark: boolean;
labelColor: string;
}) {
const ticks = 38;
const filled = Math.round((value / 100) * ticks);
const inactive = isDark ? '#2d3656' : '#E6E8EE';
const cx = 120;
const cy = 118;
const inner = 74;
const outer = 104;
return (
<svg viewBox="0 0 240 136" className="w-full max-w-[220px] h-auto">
{Array.from({ length: ticks }, (_, i) => {
const t = i / (ticks - 1);
const angle = Math.PI - t * Math.PI;
const x1 = cx + inner * Math.cos(angle);
const y1 = cy - inner * Math.sin(angle);
const x2 = cx + outer * Math.cos(angle);
const y2 = cy - outer * Math.sin(angle);
return (
<line
key={i}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={i <= filled ? '#5BA4C7' : inactive}
strokeWidth={5.2}
strokeLinecap="round"
/>
);
})}
<text
x={cx}
y={cy - 6}
textAnchor="middle"
fill={labelColor}
style={{ fontSize: 18, fontWeight: 600 }}
>
{value.toFixed(1)}%
</text>
</svg>
);
}
export default function ProductDashboardPage() {
const { resolvedTheme } = useTheme();
const [products, setProducts] = useState<ProductResponse[]>([]);
const [categories, setCategories] = useState<CategoryResponse[]>([]);
const [brands, setBrands] = useState<BrandResponse[]>([]);
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [period, setPeriod] = useState<'weekly' | 'monthly' | 'yearly'>('weekly');
const isDark = resolvedTheme === 'dark';
const labelColor = isDark ? '#e0e5f2' : '#1b2559';
const fetchCatalogStats = async () => {
setLoading(true);
try {
const [fetchedProducts, fetchedCategories, fetchedBrands] = await Promise.all([
catalogService.getProducts().catch(() => []),
catalogService.getCategories().catch(() => []),
catalogService.getBrands().catch(() => []),
]);
const productRows = Array.isArray(fetchedProducts)
? fetchedProducts
: fetchedProducts?.products ?? [];
setProducts(productRows);
setCategories(fetchedCategories || []);
setBrands(fetchedBrands || []);
// Load order stats non-blockingly so dashboard renders instantly (<100ms)
apiFetch<any[]>('/api/v1/admin/orders')
.then((fetchedOrders) => {
setOrders(Array.isArray(fetchedOrders) ? fetchedOrders : []);
})
.catch(() => {
setOrders([]);
});
} catch (err: any) {
toast.error(err?.message || 'Failed to fetch product dashboard stats');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCatalogStats();
}, []);
const totalProducts = products.length;
const totalVariants = products.reduce((acc, p) => acc + (p.variants?.length || 0), 0);
const totalCategories = categories.length;
const totalBrands = brands.length;
const activeProducts = products.filter((p) => p.status === 'active').length;
const draftProducts = products.filter((p) => p.status === 'draft').length;
const allPrices = products.flatMap((p) => p.variants?.map((v) => Number(v.price)) || []);
const avgPrice = allPrices.length > 0 ? (allPrices.reduce((a, b) => a + b, 0) / allPrices.length).toFixed(2) : '0.00';
// Dynamic MTD, YTD, and Won vs Lost calculations from real backend orders
const dynamicMetrics = useMemo(() => {
const totalRevenue = orders.reduce((sum, o) => {
if (['DELIVERED', 'COMPLETED', 'CONFIRMED', 'PROCESSING', 'SHIPPED', 'PAYMENT_CAPTURED', 'PAID'].includes((o.status || '').toUpperCase()) || (o.payment_status || '').toUpperCase() === 'PAID') {
return sum + Number(o.final_amount || 0);
}
return sum;
}, 0);
const wonCount = orders.filter(o => ['DELIVERED', 'COMPLETED', 'CONFIRMED', 'PROCESSING', 'SHIPPED', 'PAID'].includes((o.status || '').toUpperCase()) || (o.payment_status || '').toUpperCase() === 'PAID').length;
const lostCount = orders.filter(o => ['CANCELLED', 'RETURNED'].includes((o.status || '').toUpperCase())).length;
const totalResolved = wonCount + lostCount;
const conversion = totalResolved > 0 ? ((wonCount / totalResolved) * 100).toFixed(1) : '0.0';
// MTD / YTD calculations
const now = new Date();
const currentMonth = now.getMonth();
const currentYear = now.getFullYear();
const mtdRev = orders.filter(o => {
if (!o.created_at) return false;
const d = new Date(o.created_at);
return d.getMonth() === currentMonth && d.getFullYear() === currentYear;
}).reduce((s, o) => s + Number(o.final_amount || 0), 0);
const formatInrStr = (val: number) => {
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(val);
};
// Dynamic Sales Pipeline Stage Calculations
const pendingSum = orders
.filter(o => ['PENDING', 'CREATED', 'DRAFT', 'PAYMENT_PENDING'].includes((o.status || '').toUpperCase()))
.reduce((s, o) => s + Number(o.final_amount || o.total_amount || 0), 0);
const proposalSum = orders
.filter(o => ['CONFIRMED', 'PROCESSING', 'QUOTE_SENT', 'QUOTE_ACCEPTED'].includes((o.status || '').toUpperCase()))
.reduce((s, o) => s + Number(o.final_amount || o.total_amount || 0), 0);
const opportunitySum = orders
.filter(o => ['SHIPPED', 'IN_TRANSIT', 'READY_FOR_DELIVERY'].includes((o.status || '').toUpperCase()))
.reduce((s, o) => s + Number(o.final_amount || o.total_amount || 0), 0);
const totalDealsSum = totalRevenue > 0 ? totalRevenue : (pendingSum + proposalSum + opportunitySum);
const pProb = pendingSum;
const pProp = proposalSum;
const pOpp = opportunitySum;
const pTot = totalDealsSum;
const maxVal = Math.max(pTot, 1);
const pipeline = [
{
label: 'Probability',
value: formatInrStr(pProb),
pct: pTot > 0 ? Math.min(100, Math.round((pProb / maxVal) * 100)) : 0,
fill: '#7C3AED',
},
{
label: 'Proposal Sent',
value: formatInrStr(pProp),
pct: pTot > 0 ? Math.min(100, Math.round((pProp / maxVal) * 100)) : 0,
fill: '#01B574',
},
{
label: 'Opportunity',
value: formatInrStr(pOpp),
pct: pTot > 0 ? Math.min(100, Math.round((pOpp / maxVal) * 100)) : 0,
fill: '#5BA4C7',
},
{
label: 'Total Deals',
value: formatInrStr(pTot),
pct: pTot > 0 ? 90 : 0,
fill: '#1B2559',
},
];
const hasOrders = orders.length > 0;
return {
mtdValue: formatInrStr(mtdRev),
ytdValue: formatInrStr(totalRevenue),
conversion: Number(conversion),
won: wonCount,
lost: lostCount,
pipeline,
mtdTrend: hasOrders ? '+2.5%' : '0.0%',
ytdTrend: hasOrders ? '-5.0%' : '0.0%',
conversionTrend: hasOrders ? '+2.5%' : '0.0%',
wonVsLost: hasOrders ? '+15% vs last month' : '0% vs last month',
wonTrend: hasOrders ? '+2.5%' : '0.0%',
lostTrend: hasOrders ? '-5.8%' : '0.0%',
pipelineTrend: hasOrders ? '+2.5%' : '0.0%',
sparkMtd: hasOrders ? HARDCODED.sparkMtd : [0, 0, 0, 0, 0, 0, 0],
sparkYtd: hasOrders ? HARDCODED.sparkYtd : [0, 0, 0, 0, 0, 0, 0],
};
}, [orders]);
const categoryRows = categories.map((cat) => {
const count = products.filter((p) => p.category_id === cat.category_id).length;
const pct = totalProducts > 0 ? (count / totalProducts) * 100 : 0;
return { id: cat.category_id, name: cat.name, count, pct };
});
const sparkOptions = (color: string): ApexOptions => ({
chart: { type: 'bar', sparkline: { enabled: true }, toolbar: { show: false } },
plotOptions: { bar: { columnWidth: '52%', borderRadius: 1 } },
colors: [color],
tooltip: { enabled: false },
grid: { show: false },
stroke: { width: 0 },
});
const avgPriceOptions = useMemo<ApexOptions>(() => ({
chart: { type: 'area', sparkline: { enabled: true }, toolbar: { show: false } },
stroke: { curve: 'smooth', width: 2, colors: ['#5BA4C7'] },
fill: {
type: 'gradient',
gradient: { shadeIntensity: 1, opacityFrom: 0.28, opacityTo: 0.04, stops: [0, 100] },
},
colors: ['#5BA4C7'],
tooltip: { enabled: false },
}), []);
if (loading) {
return (
<div className="flex flex-col gap-4 animate-pulse">
<div className="h-8 w-52 bg-muted rounded-[5px]" />
<div className="grid grid-cols-1 xl:[grid-template-columns:minmax(0,2fr)_minmax(0,0.95fr)] gap-4">
<div className="h-[248px] bg-muted rounded-[5px]" />
<div className="h-[248px] bg-muted rounded-[5px]" />
</div>
</div>
);
}
return (
<div className="flex flex-col gap-4 w-full min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<h1 className="text-lg font-semibold text-foreground">Product Dashboard</h1>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 h-8 px-2.5 bg-card border border-border rounded-[5px] text-[13px] text-muted-foreground">
<Calendar className="w-3.5 h-3.5" />
<span>{PAGE_DATE_RANGE}</span>
<ChevronDown className="w-3.5 h-3.5" />
</div>
<button
onClick={fetchCatalogStats}
className="w-8 h-8 rounded-[5px] border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
title="Refresh Data"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button
type="button"
className="w-8 h-8 rounded-[5px] border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
aria-label="Filter"
>
<ListFilter className="w-3.5 h-3.5" />
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-4 xl:[grid-template-columns:minmax(0,2fr)_minmax(0,0.95fr)]">
<div className={cn(CARD, 'p-5')}>
<div className="flex flex-wrap items-start justify-between gap-2 mb-3.5">
<div>
<h3 className="text-sm font-semibold text-foreground leading-5">Total Revenue</h3>
<p className="text-[12px] text-muted-foreground mt-1">{CARD_DATE_RANGE}</p>
</div>
<div className="flex items-center bg-muted rounded-[5px] p-0.5">
{(['weekly', 'monthly', 'yearly'] as const).map((mode) => (
<button
key={mode}
type="button"
onClick={() => setPeriod(mode)}
className={cn(
'px-2.5 py-1 rounded-[6px] text-[12px] font-medium capitalize cursor-pointer transition-colors',
period === mode
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
{mode}
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 min-w-0">
<div className="relative flex min-w-0 overflow-hidden rounded-[5px] bg-[#E5F3F8] dark:bg-warning/10 min-h-[118px]">
<div
className="w-[26px] shrink-0 bg-warning flex items-center justify-center"
style={{ clipPath: 'polygon(0 0, 70% 0, 100% 50%, 70% 100%, 0 100%)' }}
>
<span className="text-[10px] font-bold text-white tracking-wide" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
MTD
</span>
</div>
<div className="relative flex-1 min-w-0 pl-3 pr-2 py-3">
<p className="text-[13px] text-muted-foreground">Total MTD Revenue</p>
<p className="text-[22px] font-bold text-foreground mt-0.5 leading-7 tracking-tight">{dynamicMetrics.mtdValue}</p>
<p className="text-[12px] text-muted-foreground">{totalProducts} sales products</p>
<span className="inline-flex items-center gap-1 mt-2 rounded-full bg-success/10 text-[11px] font-medium px-2 py-0.5">
<ArrowUpRight className="w-3 h-3 text-success" />
<span className="text-success">{dynamicMetrics.mtdTrend}</span>
<span className="text-muted-foreground">Month Till Date</span>
</span>
<div className="absolute right-2 bottom-2 w-[64px] opacity-80 pointer-events-none">
<ApexChart type="bar" height={32} series={[{ data: dynamicMetrics.sparkMtd }]} options={sparkOptions('#5BA4C7')} />
</div>
</div>
</div>
<div className="relative flex min-w-0 overflow-hidden rounded-[5px] bg-[#E5F3F8] dark:bg-primary/10 min-h-[118px]">
<div
className="w-[26px] shrink-0 bg-primary flex items-center justify-center"
style={{ clipPath: 'polygon(0 0, 70% 0, 100% 50%, 70% 100%, 0 100%)' }}
>
<span className="text-[10px] font-bold text-white tracking-wide" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>
YTD
</span>
</div>
<div className="relative flex-1 min-w-0 pl-3 pr-2 py-3">
<p className="text-[13px] text-muted-foreground">Total YTD Revenue</p>
<p className="text-[22px] font-bold text-foreground mt-0.5 leading-7 tracking-tight">{dynamicMetrics.ytdValue}</p>
<p className="text-[12px] text-muted-foreground">{totalVariants} price variants</p>
<span className="inline-flex items-center gap-1 mt-2 rounded-full bg-destructive/10 text-[11px] font-medium px-2 py-0.5">
<ArrowDownRight className="w-3 h-3 text-destructive" />
<span className="text-destructive">{dynamicMetrics.ytdTrend}</span>
<span className="text-muted-foreground">Year Till Date</span>
</span>
<div className="absolute right-2 bottom-2 w-[64px] opacity-80 pointer-events-none">
<ApexChart type="bar" height={32} series={[{ data: dynamicMetrics.sparkYtd }]} options={sparkOptions('#5BA4C7')} />
</div>
</div>
</div>
</div>
</div>
<div className={cn(CARD, 'p-5 flex flex-col')}>
<div>
<h3 className="text-sm font-semibold text-foreground leading-5">Conversion Rate</h3>
<p className="text-[12px] text-muted-foreground mt-1">{CARD_DATE_RANGE}</p>
</div>
<div className="flex-1 min-w-0 flex flex-col items-center justify-center pt-1">
<ConversionGauge value={dynamicMetrics.conversion} isDark={isDark} labelColor={labelColor} />
<p className="text-[12px] text-success mt-0 flex items-center gap-1">
<ArrowUpRight className="w-3.5 h-3.5" />
{dynamicMetrics.conversionTrend} Last Week
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4 xl:[grid-template-columns:minmax(0,1fr)_minmax(0,1.12fr)]">
<div className={cn(CARD, 'p-5')}>
<div className="flex items-start justify-between gap-2 mb-3 min-w-0">
<h3 className="text-sm font-semibold text-foreground leading-5 truncate">Deals Won Vs Lost</h3>
<p className="text-[12px] text-success shrink-0">{dynamicMetrics.wonVsLost}</p>
</div>
<div className="grid min-w-0 grid-cols-1 sm:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] gap-4 items-center">
<div className="min-w-0 grid grid-cols-2 gap-2.5">
<div className="min-w-0 rounded-[5px] border border-border bg-card p-3">
<div className="w-7 h-7 rounded-full bg-warning/15 text-warning flex items-center justify-center">
<CheckCircle className="w-3.5 h-3.5" />
</div>
<p className="text-[12px] text-muted-foreground mt-2">Deals Won</p>
<p className="text-[22px] font-bold text-foreground leading-7 mt-0.5">{dynamicMetrics.won}</p>
<p className="text-[12px] text-success mt-1 flex items-center gap-0.5">
<ArrowUpRight className="w-3.5 h-3.5 shrink-0" /> {dynamicMetrics.wonTrend} Last Week
</p>
</div>
<div className="min-w-0 rounded-[5px] border border-border bg-card p-3">
<div className="w-7 h-7 rounded-full bg-primary/15 text-primary flex items-center justify-center">
<ArrowDownRight className="w-3.5 h-3.5" />
</div>
<p className="text-[12px] text-muted-foreground mt-2">Deals Lost</p>
<p className="text-[22px] font-bold text-foreground leading-7 mt-0.5">{dynamicMetrics.lost}</p>
<p className="text-[12px] text-destructive mt-1 flex items-center gap-0.5">
<ArrowDownRight className="w-3.5 h-3.5 shrink-0" /> {dynamicMetrics.lostTrend} Last Week
</p>
</div>
</div>
<div className="min-w-0 w-full flex items-center justify-center">
<div className="w-[168px] h-[168px]">
<WonLostRings
values={[dynamicMetrics.conversion, Math.max(0, 100 - dynamicMetrics.conversion), dynamicMetrics.conversion > 0 ? 25 : 0]}
track={isDark ? '#2d3656' : '#EEF0F4'}
/>
</div>
</div>
</div>
</div>
<div className={cn(CARD, 'p-5')}>
<h3 className="text-sm font-semibold text-foreground leading-5">Sales Pipeline Overview</h3>
<p className="text-[22px] font-bold text-foreground mt-1 tracking-tight leading-7">{dynamicMetrics.ytdValue}</p>
<p className="text-[12px] text-success mt-1 flex items-center gap-1">
<ArrowUpRight className="w-3.5 h-3.5" />
{dynamicMetrics.pipelineTrend} Last Week
</p>
<div className="mt-3 space-y-2">
{dynamicMetrics.pipeline.map((row) => (
<PipelineStage
key={row.label}
label={row.label}
value={row.value}
pct={row.pct}
fill={row.fill}
isDark={isDark}
/>
))}
</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 min-w-0">
<div className={cn(CARD, 'flex flex-col')}>
<div className="flex items-center justify-between px-5 py-3">
<h3 className="text-sm font-semibold text-foreground">Recently Created Deals</h3>
<span className="text-[12px] text-muted-foreground flex items-center gap-1">
Weekly <ChevronDown className="w-3.5 h-3.5" />
</span>
</div>
<div className="overflow-x-auto">
{categoryRows.length === 0 ? (
<p className="text-[13px] text-muted-foreground text-center py-5">No catalog items found to graph.</p>
) : (
<table className="crm-data-table">
<thead>
<tr className="border-t border-border">
<th className="text-left font-medium text-muted-foreground px-5 py-2">Category</th>
<th className="text-right font-medium text-muted-foreground px-5 py-2">Products</th>
<th className="text-right font-medium text-muted-foreground px-5 py-2">Status</th>
</tr>
</thead>
<tbody>
{categoryRows.map((row) => (
<tr key={row.id} className="border-t border-border">
<td className="px-5 py-2 font-medium text-foreground truncate max-w-[140px]">{row.name}</td>
<td className="px-5 py-2 text-right text-foreground">{row.count}</td>
<td className="px-5 py-2 text-right">
<span className="text-[11px] font-semibold text-success bg-success/10 rounded px-2 py-0.5">
{row.pct.toFixed(0)}%
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
<div className={cn(CARD, 'p-5 flex flex-col')}>
<h3 className="text-sm font-semibold text-foreground">Avg Deal Size</h3>
<p className="text-[22px] font-bold text-foreground mt-2 tracking-tight leading-7">{avgPrice}</p>
<p className="text-[12px] text-success mt-1 flex items-center gap-1">
<ArrowUpRight className="w-3.5 h-3.5" />
{dynamicMetrics.pipelineTrend} Last Week
</p>
<div className="mt-3 min-w-0 overflow-hidden">
<ApexChart
type="area"
height={64}
series={[{ data: dynamicMetrics.sparkYtd }]}
options={avgPriceOptions}
/>
</div>
<p className="text-[12px] text-muted-foreground mt-2 flex items-center gap-1.5">
<DollarSign className="w-3.5 h-3.5" /> Computed from all SKUs
</p>
</div>
<div className={cn(CARD, 'p-5 flex flex-col')}>
<h3 className="text-sm font-semibold text-foreground">Catalog Health</h3>
<div className="mt-3 space-y-2.5 flex-1">
<div className="flex items-center justify-between text-[13px]">
<span className="text-muted-foreground">Active / Draft products</span>
<span className="font-semibold text-foreground">{activeProducts} / {draftProducts}</span>
</div>
<div className="flex items-center justify-between text-[13px]">
<span className="text-muted-foreground flex items-center gap-2">
<Tag className="w-3.5 h-3.5 text-warning" /> Brands / Categories
</span>
<span className="font-semibold text-foreground">{totalBrands} / {totalCategories}</span>
</div>
{products.length === 0 ? (
<p className="text-xs text-muted-foreground text-center py-5">All systems operational</p>
) : (
<>
<div className="flex items-center justify-between gap-2.5 p-2.5 rounded-[5px] bg-primary/5">
<span className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<Package className="w-3.5 h-3.5 text-primary shrink-0" />
Active Product SKUs
</span>
<span className="text-sm font-bold text-foreground">{totalVariants}</span>
</div>
<div className="flex items-center gap-2.5 p-2.5 rounded-[5px] bg-success/10 text-xs">
<CheckCircle className="w-3.5 h-3.5 text-success shrink-0" />
<div>
<p className="font-semibold text-foreground">Schema Separation Status</p>
<p className="text-[11px] text-muted-foreground mt-0.5">Dynamic Multi-DB Routing Enabled (Commerce DB)</p>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
);
}