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

865 lines
36 KiB
TypeScript

'use client';
import { useEffect, useState, useCallback, type ReactNode } from 'react';
import {
ArrowUpRight, ArrowDownRight, ArrowRight, RefreshCw, Calendar, ChevronDown,
TrendingUp, Filter, Handshake,
} from 'lucide-react';
import {
ComposedChart,
Bar,
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
} from 'recharts';
import {
adminService,
DashboardStats,
SessionInfo,
DashboardRevenuePoint,
DashboardMonthlyPoint,
DashboardRecentOrder,
DashboardTopProduct,
} from '@/services/api/adminService';
import { parseJwt, getAccessToken, apiFetch } from '@/services/api/client';
import { cn } from '@/lib/utils';
import Link from 'next/link';
import { withDashboardMock } from './mockData';
const PAGE_DATE_RANGE = 'Live Store & ERP Operations';
const CARD =
'bg-card crm-radius-section border border-border shadow-[0_1px_3px_rgba(16,24,40,0.06)] min-w-0';
const PIPELINE = [
{ key: 'pending', label: 'Pending', color: '#5BA4C7' },
{ key: 'processing', label: 'Processing', color: '#3B82F6' },
{ key: 'shipped', label: 'Shipped', color: '#7C3AED' },
{ key: 'delivered', label: 'Delivered', color: '#01B574' },
] as const;
const OVERVIEW = [
{ key: 'delivered', label: 'Successful orders', color: '#01B574' },
{ key: 'pending', label: 'Pending orders', color: '#5BA4C7' },
{ key: 'cancelled', label: 'Cancelled', color: '#7C3AED' },
{ key: 'shipped', label: 'In transit', color: '#3B82F6' },
] as const;
const DONUT_COLORS: Record<string, string> = {
delivered: '#01B574',
shipped: '#3B82F6',
processing: '#5BA4C7',
pending: '#7C3AED',
};
const PAYMENT_TAG: Record<string, string> = {
paid: 'border-[#01B574] text-[#01B574] bg-[#01B574]/8',
unpaid: 'border-[#5BA4C7] text-[#5BA4C7] bg-[#5BA4C7]/10',
refunded: 'border-primary text-primary bg-primary/8',
};
const PRODUCT_TONES = ['#5BA4C7', '#3B82F6', '#01B574', '#1B2559', '#7C3AED'];
function formatCompact(val: number) {
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(2)}M`;
if (val >= 1_000) return `${(val / 1_000).toFixed(val >= 10_000 ? 0 : 1)}K`;
return `${val.toFixed(0)}`;
}
function formatInr(val: number) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
maximumFractionDigits: 0,
}).format(val);
}
function initials(name: string) {
const parts = name.trim().split(/\s+/).filter(Boolean);
return ((parts[0]?.[0] ?? '') + (parts[1]?.[0] ?? '')).toUpperCase() || '•';
}
function samplePoints<T>(items: T[], count: number) {
if (items.length <= count) return items;
const step = (items.length - 1) / (count - 1);
return Array.from({ length: count }, (_, i) => items[Math.round(i * step)]);
}
function deltaFromWeek(week: number, month: number) {
if (!month) return 2.5;
const actual = week / month;
const expected = 7 / 30;
return ((actual - expected) / expected) * 100;
}
function CardTitle({ children }: { children: ReactNode }) {
return (
<h3 className="flex items-center gap-2 text-[15px] font-bold text-foreground min-w-0">
<span className="w-[3px] h-4 rounded-full bg-warning shrink-0" />
<span className="truncate">{children}</span>
</h3>
);
}
function PeriodPills({
value,
onChange,
}: {
value: 'weekly' | 'monthly' | 'yearly';
onChange: (next: 'weekly' | 'monthly' | 'yearly') => void;
}) {
return (
<div className="inline-flex items-center gap-1 shrink-0">
{(['weekly', 'monthly', 'yearly'] as const).map((mode) => (
<button
key={mode}
type="button"
onClick={() => onChange(mode)}
className={cn(
'h-7 px-3 crm-radius-control text-[12px] font-medium capitalize cursor-pointer transition-colors',
value === mode
? 'bg-primary text-white'
: 'bg-card text-muted-foreground hover:bg-muted'
)}
>
{mode}
</button>
))}
</div>
);
}
function GhostSelect({ label }: { label: string }) {
return (
<span className="inline-flex items-center gap-1 h-7 px-2.5 crm-radius-control border border-border text-[12px] text-muted-foreground shrink-0">
{label}
<ChevronDown className="w-3.5 h-3.5" />
</span>
);
}
function DeltaPill({ value }: { value: number }) {
const up = value >= 0;
return (
<span
className={cn(
'inline-flex items-center gap-0.5 h-[22px] px-1.5 crm-radius-badge text-[11px] font-semibold',
up ? 'bg-success/10 text-success' : 'bg-primary/10 text-primary'
)}
>
{up ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
{up ? '+' : ''}
{Math.abs(value).toFixed(1)}%
</span>
);
}
function StatusDonut({
slices,
}: {
slices: Array<{ label: string; value: number; color: string }>;
}) {
const total = slices.reduce((sum, slice) => sum + slice.value, 0) || 1;
const cx = 90;
const cy = 90;
const r = 58;
const stroke = 26;
const circ = 2 * Math.PI * r;
let offset = 0;
return (
<svg viewBox="0 0 180 180" className="w-[168px] h-[168px]">
<circle cx={cx} cy={cy} r={r} fill="none" stroke="var(--muted)" strokeWidth={stroke} />
{slices.map((slice) => {
const pct = slice.value / total;
const dash = Math.max(pct * circ, 0);
const currentOffset = offset;
offset += pct;
const mid = (currentOffset + pct / 2) * 2 * Math.PI - Math.PI / 2;
const lx = cx + Math.cos(mid) * r;
const ly = cy + Math.sin(mid) * r;
return (
<g key={slice.label}>
<circle
cx={cx}
cy={cy}
r={r}
fill="none"
stroke={slice.color}
strokeWidth={stroke}
strokeDasharray={`${dash} ${circ - dash}`}
strokeDashoffset={-currentOffset * circ}
transform={`rotate(-90 ${cx} ${cy})`}
/>
{pct >= 0.08 && (
<text
x={lx}
y={ly}
textAnchor="middle"
dominantBaseline="middle"
fill="#fff"
style={{ fontSize: 11, fontWeight: 700 }}
>
{Math.round(pct * 100)}%
</text>
)}
</g>
);
})}
</svg>
);
}
export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [loading, setLoading] = useState(true);
const [userName, setUserName] = useState('Admin');
const [chartMode, setChartMode] = useState<'weekly' | 'monthly' | 'yearly'>('weekly');
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
const token = getAccessToken();
if (token) {
const payload = parseJwt(token);
if (payload) {
setUserName((payload.email as string)?.split('@')[0] || 'Admin');
}
}
}, []);
const fetchData = useCallback(async (silent = false) => {
if (!silent) setLoading(true);
else setRefreshing(true);
try {
const [statsRes, ordersRes, productsRes, categoriesRes, brandsRes, sessionsRes] = await Promise.allSettled([
adminService.getDashboardStats().catch(() => null),
apiFetch<any[]>('/api/v1/admin/orders').catch(() => []),
apiFetch<any>('/api/v1/catalog/products/all?limit=200').catch(() => []),
apiFetch<any[]>('/api/v1/catalog/categories/all').catch(() => []),
apiFetch<any[]>('/api/v1/catalog/brands/all').catch(() => []),
adminService.listSessions(0, 50).catch(() => ({ sessions: [] })),
]);
const liveStats = statsRes.status === 'fulfilled' ? statsRes.value : null;
const liveOrders = ordersRes.status === 'fulfilled' && Array.isArray(ordersRes.value) ? ordersRes.value : [];
const liveProductsRaw = productsRes.status === 'fulfilled' ? productsRes.value : [];
const liveProducts = Array.isArray(liveProductsRaw) ? liveProductsRaw : (liveProductsRaw?.products || []);
const liveCategories = categoriesRes.status === 'fulfilled' && Array.isArray(categoriesRes.value) ? categoriesRes.value : [];
const liveBrands = brandsRes.status === 'fulfilled' && Array.isArray(brandsRes.value) ? brandsRes.value : [];
const liveSessions = sessionsRes.status === 'fulfilled' && sessionsRes.value?.sessions ? sessionsRes.value.sessions : [];
let mergedStats: DashboardStats;
if (liveStats) {
mergedStats = liveStats;
} else if (liveOrders.length > 0 || liveProducts.length > 0) {
const ordersByStatus: Record<string, number> = {
pending: 0,
confirmed: 0,
processing: 0,
shipped: 0,
delivered: 0,
cancelled: 0,
returned: 0,
};
let totalRevenue = 0;
const recentOrders: DashboardRecentOrder[] = [];
liveOrders.forEach((o: any) => {
const statusKey = (o.status || 'pending').toLowerCase();
ordersByStatus[statusKey] = (ordersByStatus[statusKey] || 0) + 1;
if (['DELIVERED', 'COMPLETED', 'CONFIRMED', 'PROCESSING', 'SHIPPED', 'PAYMENT_CAPTURED', 'PAID'].includes((o.status || '').toUpperCase()) || (o.payment_status || '').toUpperCase() === 'PAID') {
totalRevenue += Number(o.final_amount || 0);
}
if (recentOrders.length < 10) {
recentOrders.push({
order_no: o.order_no || 'ORD',
amount: Number(o.final_amount || 0),
status: (o.status || 'pending').toLowerCase(),
payment_status: (o.payment_status || 'paid').toLowerCase(),
created_at: o.created_at || null,
customer_name: o.customer_name || o.customer_email || 'Customer',
customer_email: o.customer_email || null,
});
}
});
// Daily chart points from live orders
const dailyMap = new Map<string, { revenue: number; orders: number }>();
liveOrders.forEach((o: any) => {
if (!o.created_at) return;
const d = new Date(o.created_at);
const dayStr = `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const existing = dailyMap.get(dayStr) || { revenue: 0, orders: 0 };
dailyMap.set(dayStr, {
revenue: existing.revenue + Number(o.final_amount || 0),
orders: existing.orders + 1,
});
});
const dynamicDaily = Array.from(dailyMap.entries()).map(([date, val]) => ({
date,
revenue: val.revenue,
orders: val.orders,
}));
const dynamicTopProducts: DashboardTopProduct[] = liveProducts.slice(0, 5).map((p: any) => ({
name: p.name || 'Product',
total_sold: p.variants?.length || 0,
total_revenue: p.variants?.reduce((s: number, v: any) => s + Number(v.price || 0), 0) || 0,
}));
mergedStats = {
total_users: liveOrders.length,
total_orders: liveOrders.length,
total_products: liveProducts.length,
total_brands: liveBrands.length,
total_device_models: 0,
total_device_series: 0,
total_categories: liveCategories.length,
revenue_all_time: totalRevenue,
revenue_last_30_days: totalRevenue,
revenue_last_7_days: totalRevenue,
orders_by_status: ordersByStatus,
revenue_chart_daily: dynamicDaily,
revenue_chart_monthly: [],
top_products: dynamicTopProducts,
recent_orders: recentOrders,
low_stock_variants: 0,
out_of_stock_variants: 0,
total_stock_units: liveProducts.reduce((acc: number, p: any) => acc + (p.variants?.length || 0), 0),
};
} else {
mergedStats = {
total_users: 0,
total_orders: 0,
total_products: 0,
total_brands: 0,
total_device_models: 0,
total_device_series: 0,
total_categories: 0,
revenue_all_time: 0,
revenue_last_30_days: 0,
revenue_last_7_days: 0,
orders_by_status: {},
revenue_chart_daily: [],
revenue_chart_monthly: [],
top_products: [],
recent_orders: [],
low_stock_variants: 0,
out_of_stock_variants: 0,
total_stock_units: 0,
};
}
setStats(mergedStats);
setSessions(liveSessions);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const { stats: displayStats } = withDashboardMock(stats, sessions);
const byStatus = displayStats.orders_by_status ?? {};
const countOf = (key: string) => byStatus[key] ?? 0;
const totalOrders = displayStats.total_orders ?? 0;
const delivered = countOf('delivered');
const conversion = totalOrders > 0 ? (delivered / totalOrders) * 100 : 0;
const revenueDelta = deltaFromWeek(
displayStats.revenue_last_7_days ?? 0,
displayStats.revenue_last_30_days ?? 0
);
const ordersDelta = conversion >= 40 ? 2.5 : -4.2;
const conversionDelta = 15.5;
const customersDelta = 2.5;
const daily: DashboardRevenuePoint[] = displayStats.revenue_chart_daily ?? [];
const monthly: DashboardMonthlyPoint[] = displayStats.revenue_chart_monthly ?? [];
const sourceChart: Array<DashboardRevenuePoint | DashboardMonthlyPoint> =
chartMode === 'weekly' ? samplePoints(daily, 12) : monthly;
const rawPoints = sourceChart.map((row) => ({
name:
'date' in row
? row.date?.replace('08-', '') ?? ''
: row.label?.slice(0, 3) ?? '',
revenue: row.revenue,
orders: row.orders,
}));
const maxRev = Math.max(...rawPoints.map((row) => row.revenue), 1);
const maxOrd = Math.max(...rawPoints.map((row) => row.orders), 1);
const salesScale = (maxRev * 0.72) / maxOrd;
const chartPoints = rawPoints.map((row) => ({
...row,
sales: row.orders * salesScale,
}));
const pipelineRows = PIPELINE.map((stage) => ({
...stage,
count: countOf(stage.key),
value: countOf(stage.key) * (displayStats.revenue_all_time / Math.max(totalOrders, 1)),
}));
const maxPipeline = Math.max(...pipelineRows.map((row) => row.count), 1);
const overviewRows = OVERVIEW.map((row) => ({
...row,
count: countOf(row.key),
}));
const overviewTotal = overviewRows.reduce((sum, row) => sum + row.count, 0) || 1;
const donutSlices = [
{ label: 'Delivered', value: countOf('delivered'), color: DONUT_COLORS.delivered },
{ label: 'Shipped', value: countOf('shipped'), color: DONUT_COLORS.shipped },
{ label: 'Processing', value: countOf('processing') + countOf('confirmed'), color: DONUT_COLORS.processing },
{ label: 'Pending', value: countOf('pending') + countOf('cancelled') + countOf('returned'), color: DONUT_COLORS.pending },
].filter((slice) => slice.value > 0);
const spark = daily.slice(-14).map((row) => row.orders);
const sparkMax = Math.max(...spark, 1);
const customerAvatars = (displayStats.recent_orders ?? []).slice(0, 4);
if (loading) {
return (
<div className="flex flex-col gap-4 animate-pulse">
<div className="h-8 w-48 bg-muted crm-radius-section" />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2 h-72 bg-muted crm-radius-section" />
<div className="h-72 bg-muted crm-radius-section" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-28 bg-muted crm-radius-section" />
))}
</div>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 className="text-lg font-semibold text-foreground">Dashboard</h1>
<p className="text-[13px] text-muted-foreground mt-0.5">
Welcome back, <span className="capitalize font-medium text-foreground">{userName}</span>
</p>
</div>
<div className="flex items-center gap-2 text-[13px] flex-wrap">
<div className="flex items-center gap-1.5 h-9 px-3 bg-card border border-border crm-radius-control text-muted-foreground">
<Calendar className="w-4 h-4" />
<span>{PAGE_DATE_RANGE}</span>
<ChevronDown className="w-3.5 h-3.5" />
</div>
<button
type="button"
onClick={() => fetchData(true)}
className={cn(
'w-9 h-9 bg-card border border-border crm-radius-control text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center',
refreshing && 'animate-spin'
)}
aria-label="Refresh dashboard"
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className={cn(CARD, 'lg:col-span-2 p-5 flex flex-col')}>
<div className="flex flex-wrap items-start justify-between gap-3">
<CardTitle>Revenue Analytics</CardTitle>
<PeriodPills value={chartMode} onChange={setChartMode} />
</div>
<div className="mt-4 flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-wrap items-baseline gap-2">
<p className="text-[28px] font-bold text-foreground tracking-tight leading-none">
{formatCompact(displayStats.revenue_last_30_days).replace('₹', '')}
</p>
<p className="text-[13px] text-muted-foreground">Revenue with orders (INR)</p>
</div>
<div className="flex items-center gap-4 text-[12px] text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-primary" /> Revenue
</span>
<span className="inline-flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-[#d9dee8]" /> Orders
</span>
</div>
</div>
<div className="h-[240px] w-full mt-3">
{chartPoints.length === 0 ? (
<div className="h-full flex items-center justify-center text-[13px] text-muted-foreground">
No order revenue data yet
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartPoints} barCategoryGap="18%" margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<CartesianGrid vertical stroke="var(--border)" strokeDasharray="4 4" horizontal={false} />
<XAxis
dataKey="name"
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
axisLine={false}
tickLine={false}
interval={0}
/>
<YAxis
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
axisLine={false}
tickLine={false}
width={42}
tickFormatter={(v: number) => (v >= 1000 ? `${(v / 1000).toFixed(0)}k` : `${v}`)}
/>
<Tooltip
content={({ active, payload, label }) => {
if (!active || !payload?.length) return null;
const row = payload[0]?.payload as { revenue: number; orders: number };
return (
<div className="bg-card border border-border crm-radius-control px-2.5 py-1.5 shadow-sm text-xs">
<p className="text-muted-foreground mb-0.5">{label}</p>
<p className="font-semibold text-foreground">{formatInr(row.revenue)}</p>
<p className="text-muted-foreground">{row.orders} orders</p>
</div>
);
}}
/>
<Area type="monotone" dataKey="sales" fill="#EEF0F4" stroke="none" />
<Bar dataKey="revenue" fill="#5BA4C7" radius={[2, 2, 0, 0]} />
</ComposedChart>
</ResponsiveContainer>
)}
</div>
</div>
<div className={cn(CARD, 'p-5 flex flex-col')}>
<div className="flex items-center justify-between gap-2">
<CardTitle>Order sources</CardTitle>
<Link
href="/orders"
className="w-8 h-8 crm-radius-control border border-border text-muted-foreground hover:bg-muted flex items-center justify-center"
aria-label="View orders"
>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
<div className="flex-1 flex flex-col items-center justify-center py-3">
{donutSlices.length === 0 ? (
<p className="text-[13px] text-muted-foreground">No orders yet</p>
) : (
<StatusDonut slices={donutSlices} />
)}
</div>
<div className="divide-y divide-border">
{donutSlices.map((slice) => (
<div key={slice.label} className="flex items-center justify-between py-2.5">
<span className="inline-flex items-center gap-2 text-[13px] text-foreground">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: slice.color }} />
{slice.label}
</span>
<span className="text-[13px] font-bold text-foreground">{slice.value}</span>
</div>
))}
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<div className={cn(CARD, 'p-4')}>
<div className="flex items-start justify-between">
<p className="text-[13px] text-muted-foreground">Revenue</p>
<span className="w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
<TrendingUp className="w-4 h-4" />
</span>
</div>
<p className="text-[22px] font-bold text-foreground mt-2 tracking-tight">
{formatInr(displayStats.revenue_all_time)}
</p>
<div className="mt-2 flex items-center gap-2">
<DeltaPill value={revenueDelta} />
<span className="text-[12px] text-muted-foreground">From last week</span>
</div>
</div>
<div className={cn(CARD, 'p-4')}>
<div className="flex items-start justify-between">
<p className="text-[13px] text-muted-foreground">Active orders</p>
<span className="w-10 h-10 rounded-full bg-[#3B82F6] text-white flex items-center justify-center">
<Handshake className="w-4 h-4" />
</span>
</div>
<p className="text-[22px] font-bold text-foreground mt-2 tracking-tight">{totalOrders}</p>
<div className="mt-2 flex items-center gap-2">
<DeltaPill value={ordersDelta} />
<span className="text-[12px] text-muted-foreground">From last week</span>
</div>
</div>
<div className={cn(CARD, 'p-4')}>
<div className="flex items-start justify-between">
<p className="text-[13px] text-muted-foreground">Fulfillment rate</p>
<span className="w-10 h-10 rounded-full bg-[#C026D3] text-white flex items-center justify-center">
<Filter className="w-4 h-4" />
</span>
</div>
<p className="text-[22px] font-bold text-foreground mt-2 tracking-tight">
{conversion.toFixed(1)}%
</p>
<div className="mt-2 flex items-center gap-2">
<DeltaPill value={conversionDelta} />
<span className="text-[12px] text-muted-foreground">Delivered vs total</span>
</div>
</div>
<div className={cn(CARD, 'p-4')}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-[13px] text-muted-foreground">Total customers</p>
<p className="text-[22px] font-bold text-foreground mt-2 tracking-tight">
{displayStats.total_users}
</p>
<div className="mt-2 flex items-center gap-2">
<DeltaPill value={customersDelta} />
</div>
<div className="mt-3 flex items-center">
{customerAvatars.map((order, index) => (
<span
key={order.order_no}
className="w-7 h-7 rounded-full bg-primary text-white text-[10px] font-bold flex items-center justify-center border-2 border-card"
style={{ marginLeft: index === 0 ? 0 : -8 }}
>
{initials(order.customer_name)}
</span>
))}
<span className="ml-2 text-[11px] font-semibold text-muted-foreground">
+{Math.max(displayStats.total_users - customerAvatars.length, 0)}
</span>
</div>
</div>
<div className="flex items-end gap-[3px] h-12 shrink-0 pt-1">
{spark.map((value, index) => (
<span
key={index}
className="w-[5px] rounded-full bg-primary/70"
style={{ height: `${Math.max(18, (value / sparkMax) * 100)}%` }}
/>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4">
<div className={cn(CARD, 'p-5 flex flex-col')}>
<div className="flex items-center justify-between gap-2 mb-4">
<CardTitle>Top products</CardTitle>
<GhostSelect label="Last 30 Days" />
</div>
<div className="flex-1 space-y-1">
{(displayStats.top_products ?? []).length === 0 ? (
<p className="text-[13px] text-muted-foreground text-center py-8">No sales data yet</p>
) : (
(displayStats.top_products ?? []).slice(0, 5).map((product, index) => (
<div key={product.name} className="flex items-center gap-3 py-2.5">
<span
className="w-9 h-9 rounded-full text-white text-[11px] font-bold flex items-center justify-center shrink-0"
style={{ backgroundColor: PRODUCT_TONES[index % PRODUCT_TONES.length] }}
>
{initials(product.name)}
</span>
<div className="min-w-0 flex-1">
<p className="text-[13px] font-semibold text-foreground truncate">{product.name}</p>
<p className="text-[12px] text-muted-foreground">{product.total_sold} units sold</p>
</div>
<span className="text-[13px] font-bold text-foreground shrink-0">
{formatInr(product.total_revenue)}
</span>
</div>
))
)}
</div>
<Link
href="/products"
className="mt-3 inline-flex items-center gap-1 text-[13px] font-medium text-muted-foreground hover:text-foreground"
>
View All <ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
<div className={cn(CARD, 'p-5 flex flex-col')}>
<div className="flex items-center justify-between gap-2 mb-4">
<CardTitle>Pipeline statistics</CardTitle>
<GhostSelect label="Weekly" />
</div>
<div className="grid grid-cols-4 gap-2 mb-4">
{pipelineRows.map((row) => (
<div key={row.key} className="min-w-0">
<p className="text-[11px] text-muted-foreground truncate">{row.label}</p>
<p className="text-[13px] font-bold text-foreground mt-0.5 truncate">
{formatCompact(row.value)}
</p>
<p className="text-[11px] text-muted-foreground">{row.count} orders</p>
</div>
))}
</div>
<div className="flex items-end gap-3 h-[120px]">
{pipelineRows.map((row) => (
<div key={row.key} className="flex-1 flex flex-col items-center justify-end h-full gap-2">
<div
className="w-full max-w-[42px] rounded-t-[5px]"
style={{
height: `${Math.max(18, (row.count / maxPipeline) * 100)}%`,
backgroundColor: row.color,
}}
/>
</div>
))}
</div>
<div className="mt-4 pt-3 border-t border-border flex items-center justify-between gap-3">
<p className="text-[13px] text-muted-foreground">
Stock value{' '}
<span className="font-bold text-foreground">
{(displayStats.total_stock_units ?? 0).toLocaleString()} units
</span>
</p>
<div className="flex items-end gap-[3px] h-8">
{spark.slice(-10).map((value, index) => (
<span
key={index}
className="w-[6px] rounded-full bg-primary"
style={{ height: `${Math.max(20, (value / sparkMax) * 100)}%`, opacity: 0.45 + (index / 20) }}
/>
))}
</div>
</div>
</div>
<div className={cn(CARD, 'p-5 flex flex-col')}>
<div className="flex items-center justify-between gap-2 mb-4">
<CardTitle>Orders overview</CardTitle>
<Link
href="/orders"
className="w-8 h-8 crm-radius-control border border-border text-muted-foreground hover:bg-muted flex items-center justify-center"
aria-label="View orders"
>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
<div className="h-2.5 rounded-full overflow-hidden flex">
{overviewRows.map((row) => (
<span
key={row.key}
style={{
width: `${(row.count / overviewTotal) * 100}%`,
backgroundColor: row.color,
}}
/>
))}
</div>
<p className="text-[28px] font-bold text-foreground mt-3 leading-none">{totalOrders}</p>
<div className="mt-2 flex items-center gap-2">
<DeltaPill value={12.5} />
<span className="text-[12px] text-muted-foreground">compared to last week</span>
</div>
<div className="mt-4 space-y-3 flex-1">
{overviewRows.map((row) => (
<div key={row.key} className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-2 text-[13px] text-foreground">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: row.color }} />
{row.label}
</span>
<span className="text-[13px] font-bold text-foreground">{row.count} orders</span>
</div>
))}
</div>
<div className="mt-4 bg-muted/60 crm-radius-control px-3 py-2.5 flex items-center justify-between gap-2">
<p className="text-[13px] text-muted-foreground">
Orders delivered <span className="font-bold text-foreground">{delivered}</span>
</p>
<span className="inline-flex items-center">
{customerAvatars.slice(0, 4).map((order, index) => (
<span
key={order.order_no}
className="w-6 h-6 rounded-full bg-card border border-border text-[9px] font-bold flex items-center justify-center text-foreground"
style={{ marginLeft: index === 0 ? 0 : -6 }}
>
{initials(order.customer_name)}
</span>
))}
</span>
</div>
</div>
</div>
<div className={CARD}>
<div className="px-5 py-4 flex items-center justify-between gap-3">
<CardTitle>Recent orders</CardTitle>
<Link
href="/orders"
className="inline-flex items-center gap-1 h-8 px-3 crm-radius-control border border-border text-[13px] font-medium text-foreground hover:bg-muted"
>
View All <ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
<div className="overflow-x-auto">
{(displayStats.recent_orders ?? []).length === 0 ? (
<p className="text-[13px] text-muted-foreground text-center py-10">No orders yet</p>
) : (
<table className="crm-data-table min-w-[680px]">
<thead>
<tr className="border-y border-border">
<th className="text-left font-medium text-muted-foreground px-5 py-2.5">Order</th>
<th className="text-left font-medium text-muted-foreground px-5 py-2.5">Stage</th>
<th className="text-left font-medium text-muted-foreground px-5 py-2.5">Value</th>
<th className="text-left font-medium text-muted-foreground px-5 py-2.5">Payment</th>
<th className="text-left font-medium text-muted-foreground px-5 py-2.5">Customer</th>
</tr>
</thead>
<tbody>
{(displayStats.recent_orders ?? []).map((order) => (
<tr key={order.order_no} className="border-b border-border last:border-0">
<td className="px-5 py-3 font-semibold text-foreground whitespace-nowrap">
{order.order_no}
</td>
<td className="px-5 py-3 text-muted-foreground capitalize">{order.status}</td>
<td className="px-5 py-3 text-muted-foreground whitespace-nowrap">
{formatInr(order.amount)}
</td>
<td className="px-5 py-3">
<span
className={cn(
'inline-flex items-center h-6 px-2 crm-radius-badge text-[11px] font-semibold border capitalize',
PAYMENT_TAG[order.payment_status] ?? 'border-border text-muted-foreground'
)}
>
{order.payment_status || '—'}
</span>
</td>
<td className="px-5 py-3">
<span className="inline-flex items-center gap-2 min-w-0">
<span className="w-7 h-7 rounded-full bg-primary/10 text-primary text-[10px] font-bold flex items-center justify-center shrink-0">
{initials(order.customer_name)}
</span>
<span className="text-[13px] text-foreground truncate">{order.customer_name}</span>
</span>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
</div>
);
}