1021 lines
48 KiB
TypeScript
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>
|
|
);
|
|
}
|