Files
daily_merchant_web/src/components/ReportsView.tsx
2026-06-12 14:45:06 +05:30

1021 lines
48 KiB
TypeScript

/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect } from 'react';
import {
TrendingUp,
TrendingDown,
Download,
Filter,
ChevronDown,
TrendingUp as TrendUp,
TrendingDown as TrendDown,
Equal as TrendFlat,
ChevronLeft,
ChevronRight,
Info,
Activity,
Package,
ShoppingBag,
AlertTriangle,
MapPin,
Calendar
} from 'lucide-react';
import { LeaderboardNode } from '../types';
import {
useFiestaOrderSummary,
useFiestaLocationSummary,
useFiestaOrderInsight,
useFiestaStockStatement,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
import { stockRowToProduct } from '../services/fiestaMappers';
import AwaitingApi from './AwaitingApi';
interface ReportsViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
setIsCoimbatoreView: (val: boolean) => void;
tenantId?: number;
}
const MONTH_KEYS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dece'];
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimbatoreView, tenantId = FIESTA_TENANT_ID }: ReportsViewProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState('This Year (YTD)');
const [selectedRegion, setSelectedRegion] = useState<'all' | 'coimbatore' | 'chennai' | 'bangalore'>('all');
const [stockFilter, setStockFilter] = useState<'All' | 'Healthy' | 'Low Stock' | 'Critical'>('All');
const [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [chartMetric, setChartMetric] = useState<'orders' | 'revenue' | 'cancelled' | 'skus'>('orders');
const [expandedProductId, setExpandedProductId] = useState<string | null>(null);
const [exportingFormat, setExportingFormat] = useState<'PDF' | 'CSV' | null>(null);
const [exportProgress, setExportProgress] = useState(0);
// Dropdown open states
const [showTimeframeDropdown, setShowTimeframeDropdown] = useState(false);
const [showRegionDropdown, setShowRegionDropdown] = useState(false);
// Sync state with parent component's CoimbatoreView flag
useEffect(() => {
if (isCoimbatoreView && selectedRegion !== 'coimbatore') {
setSelectedRegion('coimbatore');
} else if (!isCoimbatoreView && selectedRegion === 'coimbatore') {
setSelectedRegion('all');
}
}, [isCoimbatoreView]);
const handleRegionChange = (region: 'all' | 'coimbatore' | 'chennai' | 'bangalore') => {
setSelectedRegion(region);
if (region === 'coimbatore') {
setIsCoimbatoreView(true);
} else {
setIsCoimbatoreView(false);
}
};
// ── Live analytics (Fiesta) ───────────────────────────────────────────────
const today = new Date();
const yearStart = new Date(today.getFullYear(), 0, 1);
const todate = ymd(today);
// Previous equal-length window (same number of days immediately before the
// current YTD window) so we can derive a REAL orders/cancelled delta.
const periodDays = Math.round((today.getTime() - yearStart.getTime()) / 86400000);
const prevEnd = new Date(yearStart.getTime() - 86400000);
const prevStart = new Date(prevEnd.getTime() - periodDays * 86400000);
const summaryQ = useFiestaOrderSummary(tenantId, ymd(yearStart), todate);
const prevSummaryQ = useFiestaOrderSummary(tenantId, ymd(prevStart), ymd(prevEnd));
const locSummaryQ = useFiestaLocationSummary(tenantId);
const insightQ = useFiestaOrderInsight(tenantId);
const stockQ = useFiestaStockStatement({
tenantid: tenantId,
locationid: FIESTA_PRIMARY_LOCATION_ID,
keyword: '',
pageno: 1,
pagesize: 100,
});
const s = summaryQ.data;
const prevS = prevSummaryQ.data;
const activeSkus = (stockQ.data ?? []).length;
// Real period-over-period % change (null when we can't compute it yet).
const pctChange = (current: number, previous: number): number | null => {
if (previous <= 0) return null;
return ((current - previous) / previous) * 100;
};
const ordersDelta = s && prevS ? pctChange(s.total, prevS.total) : null;
const cancelledDelta = s && prevS ? pctChange(s.cancelled, prevS.cancelled) : null;
const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`;
// Dynamic sparkline generator helper
const getSparkPath = (values: number[], width: number, height: number) => {
const max = Math.max(...values);
const min = Math.min(...values);
const range = max - min || 1;
return values.map((val, idx) => {
const x = (idx / (values.length - 1)) * width;
const y = height - ((val - min) / range) * height;
return `${idx === 0 ? 'M' : 'L'} ${x} ${y}`;
}).join(' ');
};
// Tab thematic config
const getChartColors = () => {
switch (chartMetric) {
case 'orders':
return {
stroke: 'url(#chart-indigo-grad)',
fill: 'url(#chart-indigo-area)',
hoverCircle: '#4f46e5',
activeLine: '#6366f1',
pointFill: '#818cf8'
};
case 'revenue':
return {
stroke: 'url(#chart-emerald-grad)',
fill: 'url(#chart-emerald-area)',
hoverCircle: '#059669',
activeLine: '#10b981',
pointFill: '#34d399'
};
case 'cancelled':
return {
stroke: 'url(#chart-rose-grad)',
fill: 'url(#chart-rose-area)',
hoverCircle: '#e11d48',
activeLine: '#f43f5e',
pointFill: '#fb7185'
};
case 'skus':
return {
stroke: 'url(#chart-sky-grad)',
fill: 'url(#chart-sky-area)',
hoverCircle: '#0284c7',
activeLine: '#0ea5e9',
pointFill: '#38bdf8'
};
}
};
const theme = getChartColors();
// Live KPI values (tenant-wide; region scaling removed — no per-region API).
const totalOrdersVal = s?.total ?? 0;
const deliveredVal = s?.delivered ?? 0;
const cancelledVal = s?.cancelled ?? 0;
const activeSkusVal = activeSkus;
// KPI Row Configuration. `awaiting` cards have no live value (rendered via
// AwaitingApi). `trend` is only set where a REAL delta could be derived.
const reportsKPIs = [
{
id: 'orders' as const,
title: 'Orders',
value: totalOrdersVal.toLocaleString('en-IN'),
trend: ordersDelta !== null ? fmtDelta(ordersDelta) : null,
status: `${deliveredVal.toLocaleString('en-IN')} filled`,
isPositive: ordersDelta === null ? true : ordersDelta >= 0,
spark: [30, 45, 35, 60, 55, 70, 65, 80],
color: 'indigo',
awaiting: false,
},
{
// Revenue: no revenue API ([R1]) — render AwaitingApi instead of a value.
id: 'revenue' as const,
title: 'Revenue',
value: '',
trend: null,
status: '',
isPositive: true,
spark: [20, 30, 25, 45, 40, 55, 50, 68],
color: 'emerald',
awaiting: true,
},
{
id: 'cancelled' as const,
title: 'Cancelled',
value: cancelledVal.toLocaleString('en-IN'),
// Lower cancellations is good, so a negative delta is "positive".
trend: cancelledDelta !== null ? fmtDelta(cancelledDelta) : null,
status: `${(s?.created ?? 0).toLocaleString('en-IN')} active`,
isPositive: cancelledDelta === null ? false : cancelledDelta <= 0,
spark: [15, 10, 8, 12, 5, 9, 4, 3],
color: 'rose',
awaiting: false,
},
{
id: 'skus' as const,
title: 'Active SKUs',
value: activeSkusVal.toLocaleString('en-IN'),
// SKU delta value itself was fabricated — show no trend chip.
trend: null,
status: `All verified`,
isPositive: true,
spark: [50, 50, 55, 60, 60, 68, 70, 72],
color: 'sky',
awaiting: false,
},
];
// Helper matching local hubs belonging to Coimbatore
const isCoimbatoreNode = (name: string) => {
const coimbatoreZones = ['gandhipuram', 'rs puram', 'peelamedu', 'saravanampatti', 'coimbatore'];
return coimbatoreZones.some(zone => name.toLowerCase().includes(zone));
};
const getFilteredLocations = () => {
const rawLocations = [...(locSummaryQ.data ?? [])];
// Only Coimbatore can be filtered from live data; Chennai/Bangalore have no
// live tenant locations (their stub data was removed). Selecting them yields
// an empty list rather than fabricated hubs.
if (selectedRegion === 'coimbatore') {
return rawLocations.filter(r => isCoimbatoreNode(r.locationname || ''));
}
if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
return [];
}
return rawLocations;
};
const filteredLocations = getFilteredLocations();
// Leaderboard — outlets ranked by total live orders.
const leaderboard: LeaderboardNode[] = (() => {
let rows = [...filteredLocations];
rows = rows.sort((a, b) => b.total - a.total).slice(0, 4);
const max = rows.length ? rows[0].total : 0;
return rows.map((r, i) => ({
rank: String(i + 1).padStart(2, '0'),
name: r.locationname || `Location ${r.locationid}`,
percentage: max > 0 ? Math.round((r.total / max) * 100) : 0,
// Live order count drives the ranking/bar. No per-node revenue API yet, so the
// label shows the real order count (not fabricated rupees) — revenue lands with [R1].
revenue: `${r.total.toLocaleString()} ord`,
}));
})();
const currentLeaderboard = leaderboard;
// Monthly order distribution per outlet — live only (useFiestaOrderInsight
// already covers all the tenant's locations). The Chennai/Bangalore stub rows
// were removed; selecting those regions filters the live rows to none.
const insightRows = (() => {
let rows = (insightQ.data ?? []);
if (selectedRegion === 'coimbatore') {
rows = rows.filter(r => isCoimbatoreNode(fstr(r.locationname)));
} else if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
rows = [];
}
return rows.map((r) => ({
name: fstr(r.locationname) || `Location ${fstr(r.locationid)}`,
months: (r.ordermonths ?? {}) as Record<string, unknown>,
}));
})();
const heatmapMax = Math.max(
1,
...insightRows.flatMap((row) => MONTH_KEYS.map((k) => fnum(row.months[k]))),
);
// Live product performance matrix.
const liveProducts = (stockQ.data ?? []).map(stockRowToProduct);
const filteredProducts = liveProducts.filter((prod) => {
const matchesSearch =
prod.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
prod.sku.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStock = stockFilter === 'All' ? true : prod.stockStatus === stockFilter;
return matchesSearch && matchesStock;
});
// Heatmap cell color gradient scale (multi-stop violet theme)
const getHeatmapColorClass = (val: number) => {
const ratio = val / heatmapMax;
if (ratio < 0.15) {
return 'bg-slate-50 text-zinc-400 border border-slate-100/50 hover:bg-slate-100 hover:text-zinc-650 hover:scale-105';
}
if (ratio <= 0.4) {
return 'bg-purple-50 text-purple-700 border border-purple-100/50 hover:bg-purple-100 hover:scale-105';
}
if (ratio <= 0.7) {
return 'bg-purple-200/50 text-purple-900 border border-purple-200/40 hover:bg-purple-200 hover:scale-105';
}
if (ratio <= 0.9) {
return 'bg-[#7c3aed]/20 text-[#7c3aed] border border-[#7c3aed]/20 hover:bg-[#7c3aed]/30 hover:scale-105';
}
return 'bg-gradient-to-br from-[#7c3aed] to-[#581c87] text-white border-none shadow-sm hover:scale-108 hover:shadow-md hover:brightness-110';
};
// Triggers progress bar simulated exporting
const startExportSim = (format: 'PDF' | 'CSV') => {
setExportingFormat(format);
setExportProgress(0);
const interval = setInterval(() => {
setExportProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setTimeout(() => {
setExportingFormat(null);
alert(`${format} ledger exported successfully.`);
}, 450);
return 100;
}
return prev + 10;
});
}, 120);
};
return (
<div className="space-y-lg animate-in fade-in duration-500 font-sans relative">
{/* Immersive Background Blur Blobs */}
<div className="absolute top-10 left-1/4 w-96 h-96 bg-purple-400/5 rounded-full blur-[120px] pointer-events-none -z-10 animate-pulse" />
<div className="absolute top-40 right-1/4 w-[28rem] h-[28rem] bg-indigo-400/5 rounded-full blur-[140px] pointer-events-none -z-10 animate-pulse" style={{ animationDuration: '8s' }} />
{/* ── Immersive Analytics Banner (With Data Cover Image & Slate Gradient Overlay) ── */}
<div className="relative rounded-2xl p-6 md:p-8 text-white shadow-xl border border-purple-500/20 mb-8 animate-in fade-in duration-300 z-40">
{/* Cover Image Background & Decor (wrapped in overflow-hidden to keep rounded corner clip, while allowing dropdown overflow) */}
<div className="absolute inset-0 z-0 overflow-hidden rounded-2xl">
<img
src="https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1200&q=80"
alt="Reports Dashboard Banner"
className="w-full h-full object-cover object-center opacity-40"
/>
<div className="absolute inset-0 bg-gradient-to-r from-slate-950 via-slate-900/90 to-purple-950/80" />
{/* Background decorative glowing circles */}
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none z-0" />
<div className="absolute bottom-0 left-0 w-56 h-56 bg-slate-500/5 rounded-full blur-2xl -ml-20 -mb-20 pointer-events-none z-0" />
</div>
{/* Content Row */}
<div className="relative z-20 flex flex-col md:flex-row md:items-center justify-between gap-lg">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-white flex items-center gap-2">
Business Intelligence Center
<span className="text-[10px] text-purple-200 font-bold bg-purple-900/60 border border-purple-500/30 px-2 py-0.5 rounded-full uppercase tracking-wider animate-pulse">
Live Core
</span>
</h1>
</div>
{/* Action picker filters inside the banner */}
<div className="flex items-center gap-sm flex-wrap text-xs font-sans text-zinc-800">
{/* Custom Timeframe Dropdown */}
<div className="relative text-xs">
<button
onClick={() => {
setShowTimeframeDropdown(!showTimeframeDropdown);
setShowRegionDropdown(false);
}}
className="bg-slate-900/40 backdrop-blur-md border border-white/10 rounded-xl px-sm py-2 flex items-center justify-between gap-md shadow-sm font-bold text-white min-w-[135px] cursor-pointer hover:bg-slate-900/60 transition-colors"
>
<span>{selectedTimeframe}</span>
<ChevronDown size={12} className={`text-purple-300 transition-transform duration-200 ${showTimeframeDropdown ? 'rotate-180' : ''}`} />
</button>
{showTimeframeDropdown && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowTimeframeDropdown(false)} />
<div className="absolute right-0 mt-2 w-44 bg-zinc-950 border border-zinc-800 rounded-2xl shadow-xl py-2 z-50 animate-in zoom-in-95 duration-150 backdrop-blur-md text-white">
{['This Month', 'This Year (YTD)', 'Last 12 Months', 'All Time'].map((opt) => (
<button
key={opt}
onClick={() => {
setSelectedTimeframe(opt);
setShowTimeframeDropdown(false);
}}
className={`w-full text-left px-md py-2 text-xs font-semibold hover:bg-zinc-900 transition-colors ${selectedTimeframe === opt ? 'text-purple-400 bg-purple-950/40' : 'text-zinc-300'}`}
>
{opt}
</button>
))}
</div>
</>
)}
</div>
{/* Custom Region Dropdown */}
<div className="relative text-xs">
<button
onClick={() => {
setShowRegionDropdown(!showRegionDropdown);
setShowTimeframeDropdown(false);
}}
className="bg-slate-900/40 backdrop-blur-md border border-white/10 rounded-xl px-sm py-2 flex items-center justify-between gap-md shadow-sm font-bold text-white min-w-[155px] cursor-pointer hover:bg-slate-900/60 transition-colors"
>
<div className="flex items-center gap-sm">
<Filter size={12} className="text-purple-300" />
<span>
{selectedRegion === 'all' ? 'All Regions (12)' :
selectedRegion === 'coimbatore' ? 'Coimbatore (5)' :
selectedRegion === 'chennai' ? 'Chennai (4)' : 'Bangalore (3)'}
</span>
</div>
<ChevronDown size={12} className={`text-purple-300 transition-transform duration-200 ${showRegionDropdown ? 'rotate-180' : ''}`} />
</button>
{showRegionDropdown && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowRegionDropdown(false)} />
<div className="absolute right-0 mt-2 w-48 bg-zinc-950 border border-zinc-800 rounded-2xl shadow-xl py-2 z-50 animate-in zoom-in-95 duration-150 backdrop-blur-md text-white">
{[
{ id: 'all' as const, label: 'All Regions (12 Hubs)' },
{ id: 'coimbatore' as const, label: 'Coimbatore (5 Hubs)' },
{ id: 'chennai' as const, label: 'Chennai (4 Hubs)' },
{ id: 'bangalore' as const, label: 'Bangalore (3 Hubs)' }
].map((opt) => (
<button
key={opt.id}
onClick={() => {
handleRegionChange(opt.id);
setShowRegionDropdown(false);
}}
className={`w-full text-left px-md py-2 text-xs font-semibold hover:bg-zinc-900 transition-colors ${selectedRegion === opt.id ? 'text-purple-400 bg-purple-950/40' : 'text-zinc-300'}`}
>
{opt.label}
</button>
))}
</div>
</>
)}
</div>
{/* Export PDF action */}
<button
onClick={() => startExportSim('PDF')}
className="bg-[#581c87] border border-purple-500/30 text-white font-sans font-bold px-4 py-2 rounded-xl flex items-center gap-sm cursor-pointer transition-all hover:bg-purple-800 hover:shadow-lg active:bg-purple-900 shadow-sm text-xs hover:-translate-y-0.5 animate-in"
>
<Download size={13} />
Export PDF
</button>
</div>
</div>
{/* Small cards metrics grid relative to reports (similar to StoreDetailView) */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-8 pt-6 border-t border-slate-800/80 relative z-10">
{/* Card 1: Active Region */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Active Region</span>
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
<MapPin className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-sans truncate">
{selectedRegion === 'all' ? 'All Regions' :
selectedRegion === 'coimbatore' ? 'Coimbatore' :
selectedRegion === 'chennai' ? 'Chennai' : 'Bangalore'}
</h3>
<p className="text-[10px] text-purple-400 font-semibold mt-1">
{filteredLocations.length} hubs active
</p>
</div>
</div>
{/* Card 2: Selected Horizon */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Time Horizon</span>
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
<Calendar className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-sans truncate">
{selectedTimeframe}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Historical Period</p>
</div>
</div>
{/* Card 3: Total Segment Orders */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Total Orders</span>
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 group-hover:scale-110 transition-transform">
<ShoppingBag className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
{totalOrdersVal.toLocaleString('en-IN')}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Segment Volume</p>
</div>
</div>
{/* Card 4: Gross Revenue — no revenue API ([R1]) */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Gross Revenue</span>
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 group-hover:scale-110 transition-transform">
<Activity className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<AwaitingApi label="Gross Revenue" api="[R1]" compact />
</div>
</div>
</div>
</div>
{/* Primary KPI Row - 4 Key Tab buttons with Sparklines */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-gutter text-xs font-sans relative z-10">
{reportsKPIs.map((kpi) => {
const isActive = chartMetric === kpi.id;
// Theme-based conditional classes
let activeStyles = '';
let strokeColor = '';
let IconComponent = ShoppingBag;
let iconBg = '';
let iconTextColor = '';
if (kpi.color === 'indigo') {
activeStyles = isActive
? 'border-indigo-500 bg-indigo-50/10 shadow-[0_12px_30px_rgba(99,102,241,0.15)] ring-1 ring-indigo-500 scale-102 z-10'
: 'hover:border-indigo-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(99,102,241,0.05)]';
strokeColor = '#6366f1';
IconComponent = ShoppingBag;
iconBg = 'bg-indigo-50 border border-indigo-100 shadow-[0_0_12px_rgba(99,102,241,0.2)]';
iconTextColor = 'text-indigo-600';
} else if (kpi.color === 'emerald') {
activeStyles = isActive
? 'border-emerald-500 bg-emerald-50/10 shadow-[0_12px_30px_rgba(16,185,129,0.15)] ring-1 ring-emerald-500 scale-102 z-10'
: 'hover:border-emerald-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(16,185,129,0.05)]';
strokeColor = '#10b981';
IconComponent = Activity;
iconBg = 'bg-emerald-50 border border-emerald-100 shadow-[0_0_12px_rgba(16,185,129,0.2)]';
iconTextColor = 'text-emerald-600';
} else if (kpi.color === 'rose') {
activeStyles = isActive
? 'border-rose-500 bg-rose-50/10 shadow-[0_12px_30px_rgba(244,63,94,0.15)] ring-1 ring-rose-500 scale-102 z-10'
: 'hover:border-rose-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(244,63,94,0.05)]';
strokeColor = '#f43f5e';
IconComponent = AlertTriangle;
iconBg = 'bg-rose-50 border border-rose-100 shadow-[0_0_12px_rgba(244,63,94,0.2)]';
iconTextColor = 'text-rose-600';
} else {
activeStyles = isActive
? 'border-sky-500 bg-sky-50/10 shadow-[0_12px_30px_rgba(14,165,233,0.15)] ring-1 ring-sky-500 scale-102 z-10'
: 'hover:border-sky-200 border-[#e2e8f0] bg-white/70 hover:bg-white hover:shadow-[0_8px_20px_rgba(14,165,233,0.05)]';
strokeColor = '#0ea5e9';
IconComponent = Package;
iconBg = 'bg-sky-50 border border-sky-100 shadow-[0_0_12px_rgba(14,165,233,0.2)]';
iconTextColor = 'text-sky-600';
}
return (
<button
key={kpi.id}
onClick={() => setChartMetric(kpi.id)}
className={`border backdrop-blur-md rounded-3xl p-lg flex flex-col justify-between hover:shadow-lg transition-all duration-300 hover:-translate-y-1.5 text-left cursor-pointer outline-none relative overflow-hidden min-h-[160px] group ${activeStyles}`}
>
{/* Subtle top glowing line when active */}
{isActive && (
<div className="absolute top-0 left-0 right-0 h-1" style={{ backgroundColor: strokeColor }} />
)}
{/* Header Row */}
<div className="flex justify-between items-center w-full">
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block">
{kpi.title}
</span>
<div className={`w-8 h-8 rounded-full flex items-center justify-center transition-transform duration-300 group-hover:scale-110 ${iconBg} ${iconTextColor}`}>
<IconComponent size={14} />
</div>
</div>
{/* Main Metric Value and Trend Badge */}
{kpi.awaiting ? (
<div className="mt-3">
<AwaitingApi label="Revenue" api="[R1]" compact />
</div>
) : (
<div className="mt-3 flex items-baseline gap-2">
<div className="font-sans font-black text-slate-900 text-3xl tracking-tight leading-none">
{kpi.value}
</div>
{kpi.trend && (
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full flex items-center gap-0.5 leading-none h-4 ${kpi.isPositive ? 'bg-emerald-50 text-emerald-600 border border-emerald-100/50' : 'bg-rose-50 text-rose-600 border border-rose-100/50'
}`}>
{kpi.isPositive ? '▲' : '▼'}
{kpi.trend}
</span>
)}
</div>
)}
{/* Bottom Sparkline & Subtext segment */}
<div className="flex items-center justify-between mt-auto pt-3 w-full border-t border-[#f1f5f9]">
<span className="text-[10px] text-zinc-550 font-bold uppercase tracking-wider">{kpi.status}</span>
{/* KPI Mini Sparkline Chart */}
<div className="w-16 h-7 select-none shrink-0">
<svg className="w-full h-full" viewBox="0 0 64 32">
<path
d={getSparkPath(kpi.spark, 64, 32)}
fill="none"
stroke={strokeColor}
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
</div>
</button>
);
})}
</div>
{/* Main Interactive Charts & Insights Bento Segment */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-gutter text-xs font-sans relative z-10">
{/* Interactive Main Graph Card — Full width (12 cols) */}
<div className="lg:col-span-12 bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-3xl p-lg shadow-sm flex flex-col justify-between relative overflow-hidden group">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-lg border-b border-[#f1f5f9] pb-md">
<div>
<span className="text-[10px] text-purple-650 font-bold bg-purple-50 px-2 py-0.5 rounded-full uppercase tracking-wider">
Interactive Graph
</span>
<h3 className="font-sans font-bold text-base text-[#0f172a] mt-1.5">
{chartMetric === 'orders' ? 'Total Orders Velocity Trend' :
chartMetric === 'revenue' ? 'Revenue Expansion Trajectory' :
chartMetric === 'cancelled' ? 'Order Cancellation Frequency' :
'Catalogue Active SKUs Growth'}
</h3>
</div>
<div className="flex gap-2">
<span className="text-[10px] font-mono text-zinc-555 font-bold bg-zinc-50 border border-zinc-200/60 px-3 py-1 rounded-xl flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-current animate-pulse" style={{ color: theme.activeLine }} />
<span>Live Sync</span>
</span>
</div>
</div>
{/* Plotted Area — no time-series API ([R2]) for orders/revenue/skus.
The metric tabs (KPI cards above) still switch the card title; the
chart body itself shows the awaiting-backend placeholder. */}
<div className="relative h-64 select-none w-full flex items-center justify-center">
<AwaitingApi label="Orders & revenue time-series" api="[R2]" className="w-full h-full justify-center" />
</div>
</div>
</div>
{/* Bento split maps (Heatmap grid & Leaderboard) */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-gutter text-xs font-sans relative z-10">
{/* Revenue Heatmap table - 8 Cols */}
<div className="lg:col-span-8 bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl overflow-hidden flex flex-col justify-between shadow-sm">
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm flex justify-between items-center select-none">
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
Monthly Order Distribution
</span>
<div className="flex items-center gap-2 text-[9px] font-bold text-zinc-400 uppercase tracking-tight">
<span className="w-1.5 h-1.5 rounded-full bg-[#581c87] animate-pulse"></span>
<span>Busiest Month</span>
</div>
</div>
<div className="p-md flex-1 overflow-x-auto">
{selectedCell && (
<div className="mb-4 p-md bg-purple-500/10 border border-purple-500/20 backdrop-blur-md rounded-2xl text-xs flex justify-between items-center animate-in slide-in-from-top-2 duration-300">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-full bg-purple-500/20 flex items-center justify-center text-purple-650 shrink-0">
<Info size={14} />
</div>
<span className="font-sans text-purple-955 font-semibold">
{selectedCell.day} · {selectedCell.hour} : {selectedCell.val} orders
</span>
</div>
<button
onClick={() => setSelectedCell(null)}
className="text-xs font-bold text-purple-700 hover:text-purple-900 cursor-pointer border-none bg-transparent hover:underline px-2"
>
Dismiss
</button>
</div>
)}
{insightRows.length === 0 ? (
<div className="py-xl text-center text-zinc-400 text-xs">
{insightQ.isLoading ? 'Loading monthly order distribution…' : 'No order insight available for this region.'}
</div>
) : (
<table className="w-full text-center border-collapse text-xs font-sans">
<thead>
<tr>
<th className="p-xs text-[9px] font-bold text-zinc-400 w-32 uppercase italic text-left pl-md">Outlet Name</th>
{MONTH_LABELS.map((m) => (
<th key={m} className="p-sm text-[10px] font-bold text-[#0f172a] border-b border-[#f1f5f9]">
{m}
</th>
))}
</tr>
</thead>
<tbody>
{insightRows.map((row, idx) => (
<tr key={idx}>
<td className="p-sm text-left font-bold text-[#0f172a] tracking-wide truncate max-w-[8rem] border-r border-[#f1f5f9] pl-md">
{row.name}
</td>
{MONTH_KEYS.map((key, mIdx) => {
const val = fnum(row.months[key]);
return (
<td key={key} className="p-1 border border-white">
<button
onClick={() => setSelectedCell({ day: row.name, hour: MONTH_LABELS[mIdx], val })}
className={`w-full py-2.5 rounded-lg font-semibold transition-all duration-200 cursor-pointer border-none ${getHeatmapColorClass(val)}`}
>
{val}
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{/* Leaderboard nodes bar list - 4 Cols */}
<div className="lg:col-span-4 bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm">
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm select-none">
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
Top Performing Nodes
</span>
</div>
<div className="p-md flex-1 space-y-md flex flex-col justify-center">
{currentLeaderboard.length === 0 ? (
<div className="text-center py-8 text-zinc-450 font-medium">
No active nodes in this region.
</div>
) : (
currentLeaderboard.map((node, index) => {
let badgeStyle = '';
let badgeContent: React.ReactNode = node.rank;
if (index === 0) {
badgeStyle = 'bg-gradient-to-tr from-amber-300 via-yellow-400 to-amber-500 text-amber-955 border-amber-200 shadow-md shadow-amber-500/20';
badgeContent = '🥇';
} else if (index === 1) {
badgeStyle = 'bg-gradient-to-tr from-slate-200 via-zinc-300 to-slate-400 text-zinc-800 border-zinc-100 shadow-md shadow-zinc-400/10';
badgeContent = '🥈';
} else if (index === 2) {
badgeStyle = 'bg-gradient-to-tr from-amber-600 via-amber-700 to-orange-850 text-amber-50 border-amber-500 shadow-md shadow-amber-800/20';
badgeContent = '🥉';
} else {
badgeStyle = 'bg-slate-100 text-zinc-505 border-slate-200';
}
return (
<div key={node.rank} className="flex items-center gap-md text-xs hover:bg-slate-50/50 p-2.5 rounded-xl transition-all duration-200 group">
{/* Ranking circle avatar */}
<div className={`w-9 h-9 rounded-full border font-black flex items-center justify-center font-mono shrink-0 select-none text-sm transition-all duration-300 group-hover:scale-105 ${badgeStyle}`}>
{badgeContent}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center font-medium mb-1">
<span className="text-[#0f172a] font-bold truncate max-w-[12rem]">{node.name}</span>
<span className="text-[#581c87] font-mono font-bold">{node.revenue}</span>
</div>
<div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
<div
className="bg-gradient-to-r from-purple-500 via-indigo-600 to-[#581c87] h-full rounded-full transition-all duration-500 ease-out origin-left group-hover:brightness-110"
style={{ width: `${node.percentage}%` }}
/>
</div>
</div>
</div>
);
})
)}
</div>
</div>
</div>
{/* Detailed Performance Matrix table */}
<div className="bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl overflow-hidden shadow-sm relative z-10">
{/* Table header with filters control */}
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] p-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-sm select-none">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-1.5">
<Activity size={15} className="text-[#581c87]" /> Product Performance Matrix
</h3>
</div>
{/* Quick interactive filter pills */}
<div className="flex gap-2 text-[10px] font-bold uppercase tracking-wider">
{(['All', 'Healthy', 'Low Stock', 'Critical'] as const).map((filter) => (
<button
key={filter}
onClick={() => setStockFilter(filter)}
className={`px-3 py-1.5 rounded-lg cursor-pointer transition-colors border ${stockFilter === filter
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
: 'bg-white border-[#e2e8f0] text-zinc-655 hover:bg-zinc-55'
}`}
>
{filter}
</button>
))}
<button
onClick={() => startExportSim('CSV')}
className="px-3 py-1.5 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-805 border border-[#0f172a] transition-colors shadow-sm"
>
Export CSV
</button>
</div>
</div>
{/* Matrix Data table */}
<div className="overflow-x-auto text-xs font-sans">
<table className="w-full text-left">
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[9px] uppercase font-bold tracking-wider">
<tr>
<th className="w-10 px-md py-sm"></th>
<th className="px-md py-sm">Product Name</th>
<th className="px-md py-sm">SKU ID</th>
<th className="px-md py-sm text-right">Units Sold</th>
<th className="px-md py-sm text-right">Revenue</th>
<th className="px-md py-sm text-right">Stock Status</th>
<th className="px-md py-sm text-center">Trend index</th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{filteredProducts.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-8 text-zinc-400 font-medium">
No matching items matching stock filter criteria.
</td>
</tr>
) : (
filteredProducts.map((prod) => {
const isExpanded = expandedProductId === prod.id;
return (
<React.Fragment key={prod.id}>
<tr
onClick={() => setExpandedProductId(isExpanded ? null : prod.id)}
className={`hover:bg-[#f2f4f6]/40 transition-all font-medium text-zinc-700 cursor-pointer ${isExpanded ? 'bg-slate-50/50' : ''}`}
>
<td className="px-md py-md text-center text-zinc-400">
<ChevronDown
size={14}
className={`transition-transform duration-200 ${isExpanded ? 'rotate-180 text-purple-650' : 'text-zinc-400'}`}
/>
</td>
<td className="px-md py-md flex items-center gap-sm">
<div className="w-8 h-8 rounded-lg shrink-0 border border-[#e2e8f0] overflow-hidden bg-zinc-50">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover"
/>
</div>
<div>
<span className="font-bold text-[#0f172a] block">{prod.name}</span>
<span className="text-[9px] text-zinc-455 block font-semibold uppercase mt-0.5">{prod.category}</span>
</div>
</td>
<td className="px-md py-md font-mono text-zinc-500 font-semibold">
{prod.sku}
</td>
<td className="px-md py-md text-right font-mono text-zinc-655 font-bold">
{prod.unitsSold.toLocaleString()}
</td>
<td className="px-md py-md text-right font-mono text-zinc-800 font-extrabold">
{prod.revenue.toLocaleString()}
</td>
<td className="px-md py-md text-right">
<span className={`px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wide inline-block ${prod.stockStatus === 'Healthy'
? 'bg-emerald-50 text-emerald-600 border border-emerald-100'
: prod.stockStatus === 'Low Stock'
? 'bg-amber-50 text-amber-600 border border-amber-100'
: 'bg-rose-50 text-rose-600 border border-rose-100'
}`}>
{prod.stockStatus}
</span>
</td>
<td className="px-md py-md text-center">
<span className="inline-block p-1 bg-[#f8fafc] border border-zinc-100/85 rounded-full select-none">
{prod.trend === 'up' ? (
<TrendUp size={13} className="text-emerald-505" />
) : prod.trend === 'down' ? (
<TrendDown size={13} className="text-rose-505" />
) : (
<TrendFlat size={13} className="text-zinc-400" />
)}
</span>
</td>
</tr>
{/* Expanded details row */}
{isExpanded && (
<tr className="bg-slate-50/20">
<td colSpan={7} className="p-0 border-b border-[#e2e8f0]">
<div className="px-lg py-md bg-gradient-to-r from-slate-50/50 to-purple-50/10 border-t border-[#e2e8f0] animate-in slide-in-from-top-2 duration-300">
{/* Per-product stock & location breakdown has no live
API ([R3]); the previously fabricated unit counts,
hub split, bin code, audit date and barcode are
replaced with the awaiting-backend placeholder. */}
<AwaitingApi label="Per-product stock & location detail" api="[R3]" compact />
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
)}
</tbody>
</table>
</div>
{/* Matrix table pagination */}
<div className="p-md bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between items-center text-[10px] text-zinc-500 font-bold font-sans select-none">
<span>Showing 1-{filteredProducts.length} of {liveProducts.length} live products</span>
<div className="flex gap-xs">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
>
<ChevronLeft size={12} />
</button>
<button className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center font-bold text-[10px] bg-[#0f172a] text-white">
1
</button>
<button
onClick={() => alert('Proceeding to page 2 details representation')}
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-550 font-bold font-mono text-[10px] cursor-pointer"
>
2
</button>
<button
onClick={() => alert('Proceeding to page 3 details representation')}
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-555 font-bold font-mono text-[10px] cursor-pointer"
>
3
</button>
<button
onClick={() => setCurrentPage(prev => prev + 1)}
className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
>
<ChevronRight size={12} />
</button>
</div>
</div>
</div>
{/* EXPORTING DIALOG MODAL */}
{exportingFormat && (
<div className="fixed inset-0 bg-[#0f172a]/35 backdrop-blur-sm z-[200] flex items-center justify-center p-md">
<div className="bg-white border border-[#e2e8f0] rounded-2xl w-full max-w-[20rem] p-lg flex flex-col items-center justify-center shadow-2xl animate-in zoom-in-95 duration-200 text-center font-sans">
<div className="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center text-[#581c87] mb-md animate-bounce">
<Download size={20} />
</div>
<h4 className="font-bold text-slate-900 text-sm mb-xs">
Generating {exportingFormat} Report
</h4>
<p className="text-[10px] text-zinc-405 font-semibold mb-lg">
Compiling database records and SVG vector curves...
</p>
{/* Progress track */}
<div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden mb-sm relative">
<div
className="bg-gradient-to-r from-purple-500 to-[#581c87] h-full rounded-full transition-all duration-150"
style={{ width: `${exportProgress}%` }}
/>
</div>
<span className="text-xs font-mono font-bold text-purple-750">{exportProgress}%</span>
</div>
</div>
)}
</div>
);
}