udpates on the ui changesand api integration

This commit is contained in:
2026-06-09 11:25:29 +05:30
parent 7dbae96b5f
commit 9f25c5f60a
26 changed files with 4324 additions and 2639 deletions

View File

@@ -32,6 +32,7 @@ import {
} 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;
@@ -49,7 +50,6 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
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 [hoveredPoint, setHoveredPoint] = useState<number | null>(null);
const [expandedProductId, setExpandedProductId] = useState<string | null>(null);
const [exportingFormat, setExportingFormat] = useState<'PDF' | 'CSV' | null>(null);
const [exportProgress, setExportProgress] = useState(0);
@@ -78,11 +78,17 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
// ── Live analytics (Fiesta) ───────────────────────────────────────────────
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
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(FIESTA_TENANT_ID, ymd(yearStart), todate);
const prevSummaryQ = useFiestaOrderSummary(FIESTA_TENANT_ID, ymd(prevStart), ymd(prevEnd));
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
const insightQ = useFiestaOrderInsight(FIESTA_TENANT_ID);
const stockQ = useFiestaStockStatement({
@@ -94,94 +100,17 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
});
const s = summaryQ.data;
const prevS = prevSummaryQ.data;
const activeSkus = (stockQ.data ?? []).length;
// Base YTD data pool
const CHART_DATA_YTD = [
{ label: 'Jan', orders: 240, revenue: 78000, cancelled: 15, skus: 120 },
{ label: 'Feb', orders: 310, revenue: 98000, cancelled: 10, skus: 125 },
{ label: 'Mar', orders: 290, revenue: 89000, cancelled: 8, skus: 128 },
{ label: 'Apr', orders: 380, revenue: 120000, cancelled: 12, skus: 135 },
{ label: 'May', orders: 420, revenue: 145000, cancelled: 5, skus: 138 },
{ label: 'Jun', orders: 510, revenue: 175000, cancelled: 9, skus: 140 },
{ label: 'Jul', orders: 480, revenue: 162000, cancelled: 4, skus: 142 },
{ label: 'Aug', orders: 560, revenue: 189000, cancelled: 3, skus: 145 },
{ label: 'Sep', orders: 630, revenue: 215000, cancelled: 6, skus: 150 },
{ label: 'Oct', orders: 710, revenue: 248000, cancelled: 8, skus: 152 },
{ label: 'Nov', orders: 790, revenue: 275000, cancelled: 5, skus: 155 },
{ label: 'Dec', orders: 920, revenue: 320000, cancelled: 2, skus: 158 },
];
// Dynamic coordinates builder based on selected region and timeframe
const getDynamicChartData = () => {
let rawData = [...CHART_DATA_YTD];
if (selectedTimeframe === 'This Month') {
rawData = [
{ label: '02 Jun', orders: 15, revenue: 5200, cancelled: 1, skus: 145 },
{ label: '04 Jun', orders: 18, revenue: 6100, cancelled: 0, skus: 145 },
{ label: '06 Jun', orders: 12, revenue: 4300, cancelled: 2, skus: 145 },
{ label: '08 Jun', orders: 22, revenue: 7800, cancelled: 1, skus: 146 },
{ label: '10 Jun', orders: 25, revenue: 8900, cancelled: 3, skus: 146 },
{ label: '12 Jun', orders: 28, revenue: 9900, cancelled: 1, skus: 147 },
{ label: '14 Jun', orders: 24, revenue: 8400, cancelled: 0, skus: 147 },
{ label: '16 Jun', orders: 30, revenue: 10500, cancelled: 2, skus: 148 },
{ label: '18 Jun', orders: 35, revenue: 12200, cancelled: 1, skus: 148 },
{ label: '20 Jun', orders: 32, revenue: 11100, cancelled: 0, skus: 149 },
{ label: '22 Jun', orders: 38, revenue: 13300, cancelled: 4, skus: 149 },
{ label: '24 Jun', orders: 42, revenue: 14800, cancelled: 2, skus: 150 },
{ label: '26 Jun', orders: 45, revenue: 15800, cancelled: 1, skus: 150 },
{ label: '28 Jun', orders: 40, revenue: 13900, cancelled: 0, skus: 151 },
{ label: '30 Jun', orders: 50, revenue: 17500, cancelled: 1, skus: 151 },
];
} else if (selectedTimeframe === 'Last 12 Months') {
rawData = [
{ label: 'Jul 25', orders: 480, revenue: 162000, cancelled: 4, skus: 142 },
{ label: 'Aug 25', orders: 560, revenue: 189000, cancelled: 3, skus: 145 },
{ label: 'Sep 25', orders: 630, revenue: 215000, cancelled: 6, skus: 150 },
{ label: 'Oct 25', orders: 710, revenue: 248000, cancelled: 8, skus: 152 },
{ label: 'Nov 25', orders: 790, revenue: 275000, cancelled: 5, skus: 155 },
{ label: 'Dec 25', orders: 920, revenue: 320000, cancelled: 2, skus: 158 },
{ label: 'Jan 26', orders: 840, revenue: 290000, cancelled: 12, skus: 160 },
{ label: 'Feb 26', orders: 890, revenue: 310000, cancelled: 8, skus: 162 },
{ label: 'Mar 26', orders: 950, revenue: 330000, cancelled: 14, skus: 165 },
{ label: 'Apr 26', orders: 1020, revenue: 355000, cancelled: 10, skus: 168 },
{ label: 'May 26', orders: 1100, revenue: 385000, cancelled: 7, skus: 170 },
{ label: 'Jun 26', orders: 1250, revenue: 435000, cancelled: 5, skus: 172 },
];
} else if (selectedTimeframe === 'All Time') {
rawData = [
{ label: '2022', orders: 2500, revenue: 850000, cancelled: 85, skus: 90 },
{ label: '2023', orders: 4800, revenue: 1650000, cancelled: 120, skus: 120 },
{ label: '2024', orders: 7200, revenue: 2500000, cancelled: 190, skus: 140 },
{ label: '2025', orders: 9800, revenue: 3400000, cancelled: 210, skus: 160 },
{ label: '2026 (Est)', orders: 12500, revenue: 4350000, cancelled: 150, skus: 172 },
];
}
// Scale values depending on region selected
if (selectedRegion !== 'all') {
const rScale = getRegionScale();
return rawData.map(d => ({
...d,
orders: Math.round(d.orders * rScale),
revenue: Math.round(d.revenue * (rScale * 1.05)),
cancelled: Math.round(d.cancelled * (selectedRegion === 'coimbatore' ? 0.35 : selectedRegion === 'chennai' ? 0.50 : 0.65)),
skus: Math.round(d.skus * (selectedRegion === 'coimbatore' ? 0.85 : selectedRegion === 'chennai' ? 0.90 : 0.95))
}));
}
return rawData;
// 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 getRegionScale = () => {
if (selectedRegion === 'coimbatore') return 0.42;
if (selectedRegion === 'chennai') return 0.60;
if (selectedRegion === 'bangalore') return 0.75;
return 1.0;
};
const currentChartData = getDynamicChartData();
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) => {
@@ -195,46 +124,6 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
}).join(' ');
};
// Simple cubic bezier curve generator for SVG path
const getBezierPath = (pts: Array<{ x: number; y: number }>) => {
if (pts.length === 0) return '';
let d = `M ${pts[0].x} ${pts[0].y}`;
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i];
const p1 = pts[i + 1];
const cpX1 = p0.x + (p1.x - p0.x) / 3;
const cpY1 = p0.y;
const cpX2 = p0.x + 2 * (p1.x - p0.x) / 3;
const cpY2 = p1.y;
d += ` C ${cpX1} ${cpY1}, ${cpX2} ${cpY2}, ${p1.x} ${p1.y}`;
}
return d;
};
// Dynamic SVG path calculations for the primary trend chart
const paddingX = 40;
const paddingY = 20;
const chartWidth = 920;
const chartHeight = 220;
const chartMaxVal = chartMetric === 'orders'
? Math.max(...currentChartData.map(d => d.orders)) * 1.1
: chartMetric === 'revenue'
? Math.max(...currentChartData.map(d => d.revenue)) * 1.1
: chartMetric === 'cancelled'
? Math.max(...currentChartData.map(d => d.cancelled)) * 1.1
: Math.max(...currentChartData.map(d => d.skus)) * 1.1;
const points = currentChartData.map((d, index) => {
const val = d[chartMetric] as number;
const x = paddingX + (index / (currentChartData.length - 1)) * (chartWidth - 2 * paddingX);
const y = chartHeight - paddingY - (val / chartMaxVal) * (chartHeight - 2 * paddingY);
return { x, y, label: d.label, val };
});
const linePath = getBezierPath(points);
const areaPath = points.length ? `${linePath} L ${points[points.length - 1].x} ${chartHeight - paddingY} L ${points[0].x} ${chartHeight - paddingY} Z` : '';
// Tab thematic config
const getChartColors = () => {
switch (chartMetric) {
@@ -274,57 +163,61 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
};
const theme = getChartColors();
// Region specific calculations for KPIs
const scale = getRegionScale();
const scaleCancelled = selectedRegion === 'coimbatore' ? 0.35 : selectedRegion === 'chennai' ? 0.50 : selectedRegion === 'bangalore' ? 0.65 : 1.0;
const scaleSkus = selectedRegion === 'coimbatore' ? 0.85 : selectedRegion === 'chennai' ? 0.90 : selectedRegion === 'bangalore' ? 0.95 : 1.0;
// 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;
const totalOrdersVal = Math.round((s?.total ?? 0) * scale);
const deliveredVal = Math.round((s?.delivered ?? 0) * scale);
const cancelledVal = Math.round((s?.cancelled ?? 0) * scaleCancelled);
const activeSkusVal = Math.round(activeSkus * scaleSkus);
// KPI Row Configuration
// 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: `+12.5%`,
trend: ordersDelta !== null ? fmtDelta(ordersDelta) : null,
status: `${deliveredVal.toLocaleString('en-IN')} filled`,
isPositive: true,
isPositive: ordersDelta === null ? true : ordersDelta >= 0,
spark: [30, 45, 35, 60, 55, 70, 65, 80],
color: 'indigo'
color: 'indigo',
awaiting: false,
},
{
// Revenue: no revenue API ([R1]) — render AwaitingApi instead of a value.
id: 'revenue' as const,
title: 'Revenue',
value: `${(deliveredVal * 355).toLocaleString('en-IN')}`,
trend: `+14.8%`,
status: `Growth steady`,
value: '',
trend: null,
status: '',
isPositive: true,
spark: [20, 30, 25, 45, 40, 55, 50, 68],
color: 'emerald'
color: 'emerald',
awaiting: true,
},
{
id: 'cancelled' as const,
title: 'Cancelled',
value: cancelledVal.toLocaleString('en-IN'),
trend: `-1.2%`,
status: `${Math.round((s?.created ?? 0) * scaleCancelled)} active`,
isPositive: false,
// 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'
color: 'rose',
awaiting: false,
},
{
id: 'skus' as const,
title: 'Active SKUs',
value: activeSkusVal.toLocaleString('en-IN'),
trend: `+8.4%`,
// 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'
color: 'sky',
awaiting: false,
},
];
@@ -337,25 +230,15 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
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') {
return [
{ locationid: 2001, locationname: 'Chennai Adyar Hub', total: 420, delivered: 405, cancelled: 15 },
{ locationid: 2002, locationname: 'Chennai T-Nagar Outlet', total: 310, delivered: 290, cancelled: 20 },
{ locationid: 2003, locationname: 'Chennai Velachery Super', total: 290, delivered: 285, cancelled: 5 },
{ locationid: 2004, locationname: 'Chennai OMR Express', total: 180, delivered: 172, cancelled: 8 },
] as any[];
}
if (selectedRegion === 'bangalore') {
return [
{ locationid: 3001, locationname: 'Bangalore Indiranagar Hub', total: 580, delivered: 560, cancelled: 20 },
{ locationid: 3002, locationname: 'Bangalore Koramangala Store', total: 410, delivered: 395, cancelled: 15 },
{ locationid: 3003, locationname: 'Bangalore HSR Layout Express', total: 320, delivered: 312, cancelled: 8 },
] as any[];
if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
return [];
}
return rawLocations;
@@ -372,32 +255,22 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
rank: String(i + 1).padStart(2, '0'),
name: r.locationname || `Location ${r.locationid}`,
percentage: max > 0 ? Math.round((r.total / max) * 100) : 0,
revenue: `${r.total.toLocaleString('en-IN')} ord`,
// 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
// 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 = (() => {
if (selectedRegion === 'chennai') {
return [
{ name: 'Chennai Adyar Hub', months: { jan: 30, feb: 35, mar: 40, apr: 45, may: 50, jun: 55, jul: 60, Aug: 65, Sep: 70, Oct: 75, Nov: 80, Dece: 85 } },
{ name: 'Chennai T-Nagar Outlet', months: { jan: 25, feb: 28, mar: 30, apr: 35, may: 38, jun: 42, jul: 45, Aug: 48, Sep: 52, Oct: 55, Nov: 60, Dece: 65 } },
{ name: 'Chennai Velachery Super', months: { jan: 20, feb: 22, mar: 25, apr: 28, may: 30, jun: 35, jul: 38, Aug: 42, Sep: 45, Oct: 48, Nov: 52, Dece: 55 } },
{ name: 'Chennai OMR Express', months: { jan: 15, feb: 18, mar: 20, apr: 22, may: 25, jun: 28, jul: 30, Aug: 32, Sep: 35, Oct: 38, Nov: 40, Dece: 45 } },
];
}
if (selectedRegion === 'bangalore') {
return [
{ name: 'Bangalore Indiranagar Hub', months: { jan: 40, feb: 45, mar: 50, apr: 55, may: 60, jun: 65, jul: 70, Aug: 75, Sep: 80, Oct: 85, Nov: 90, Dece: 95 } },
{ name: 'Bangalore Koramangala Store', months: { jan: 30, feb: 32, mar: 35, apr: 38, may: 42, jun: 45, jul: 48, Aug: 52, Sep: 55, Oct: 60, Nov: 65, Dece: 70 } },
{ name: 'Bangalore HSR Layout Express', months: { jan: 20, feb: 24, mar: 26, apr: 28, may: 32, jun: 35, jul: 38, Aug: 40, Sep: 44, Oct: 48, Nov: 52, Dece: 55 } },
];
}
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)}`,
@@ -465,7 +338,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
<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-35">
<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
@@ -481,7 +354,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
{/* Content Row */}
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-lg">
<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
@@ -634,13 +507,13 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
{currentChartData.reduce((acc, curr) => acc + curr.orders, 0).toLocaleString('en-IN')}
{totalOrdersVal.toLocaleString('en-IN')}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Segment Volume</p>
</div>
</div>
{/* Card 4: Total Segment Revenue */}
{/* 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>
@@ -649,10 +522,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
{currentChartData.reduce((acc, curr) => acc + curr.revenue, 0).toLocaleString('en-IN')}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Estimated Value</p>
<AwaitingApi label="Gross Revenue" api="[R1]" compact />
</div>
</div>
</div>
@@ -726,16 +596,24 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
{/* Main Metric Value and Trend Badge */}
<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}
{kpi.awaiting ? (
<div className="mt-3">
<AwaitingApi label="Revenue" api="[R1]" compact />
</div>
<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>
) : (
<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]">
@@ -786,158 +664,11 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
</div>
{/* SVG Custom Graph Area */}
<div className="relative h-64 select-none w-full">
<svg className="w-full h-full overflow-visible" viewBox={`0 0 ${chartWidth} ${chartHeight}`}>
<defs>
{/* Indigo Gradients */}
<linearGradient id="chart-indigo-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#818cf8" />
<stop offset="100%" stopColor="#4f46e5" />
</linearGradient>
<linearGradient id="chart-indigo-area" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#6366f1" stopOpacity="0.15" />
<stop offset="100%" stopColor="#6366f1" stopOpacity="0.00" />
</linearGradient>
{/* Emerald Gradients */}
<linearGradient id="chart-emerald-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#34d399" />
<stop offset="100%" stopColor="#059669" />
</linearGradient>
<linearGradient id="chart-emerald-area" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#10b981" stopOpacity="0.15" />
<stop offset="100%" stopColor="#10b981" stopOpacity="0.00" />
</linearGradient>
{/* Rose Gradients */}
<linearGradient id="chart-rose-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#fb7185" />
<stop offset="100%" stopColor="#e11d48" />
</linearGradient>
<linearGradient id="chart-rose-area" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f43f5e" stopOpacity="0.15" />
<stop offset="100%" stopColor="#f43f5e" stopOpacity="0.00" />
</linearGradient>
{/* Sky Gradients */}
<linearGradient id="chart-sky-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#38bdf8" />
<stop offset="100%" stopColor="#0284c7" />
</linearGradient>
<linearGradient id="chart-sky-area" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#0ea5e9" stopOpacity="0.15" />
<stop offset="100%" stopColor="#0ea5e9" stopOpacity="0.00" />
</linearGradient>
</defs>
{/* Grid Lines */}
{[0, 0.25, 0.5, 0.75, 1].map((ratio, idx) => {
const y = paddingY + ratio * (chartHeight - 2 * paddingY);
return (
<line
key={idx}
x1={paddingX}
y1={y}
x2={chartWidth - paddingX}
y2={y}
stroke="#f1f5f9"
strokeWidth="1"
/>
);
})}
{/* Area Fill */}
{areaPath && (
<path d={areaPath} fill={theme.fill} className="transition-all duration-550 ease-out" />
)}
{/* Line Path */}
{linePath && (
<path
d={linePath}
fill="none"
stroke={theme.stroke}
strokeWidth="3"
strokeLinecap="round"
className="transition-all duration-550 ease-out"
/>
)}
{/* Hover Indicator Vertical Line */}
{hoveredPoint !== null && (
<line
x1={points[hoveredPoint].x}
y1={paddingY}
x2={points[hoveredPoint].x}
y2={chartHeight - paddingY}
stroke={theme.activeLine}
strokeWidth="1.5"
strokeDasharray="3 3"
className="transition-all duration-150"
/>
)}
{/* Chart Points & Interactive Hover Areas */}
{points.map((p, idx) => {
const isHovered = hoveredPoint === idx;
return (
<g key={idx}>
<circle
cx={p.x}
cy={p.y}
r={isHovered ? 6 : 4}
fill={isHovered ? theme.hoverCircle : theme.pointFill}
stroke="#ffffff"
strokeWidth="2"
className="transition-all duration-150"
/>
<circle
cx={p.x}
cy={p.y}
r="20"
fill="transparent"
className="cursor-pointer"
onMouseEnter={() => setHoveredPoint(idx)}
onMouseLeave={() => setHoveredPoint(null)}
/>
</g>
);
})}
</svg>
{/* Hover Tooltip Overlay */}
{hoveredPoint !== null && (
<div
className="absolute bg-zinc-950/95 border border-zinc-800 text-white rounded-2xl p-3 shadow-2xl font-sans text-xs z-10 pointer-events-none transition-all animate-in zoom-in-95 duration-150 flex flex-col gap-1 w-44 backdrop-blur-md"
style={{
left: `${(points[hoveredPoint].x / chartWidth) * 100}%`,
top: `${(points[hoveredPoint].y / chartHeight) * 100 - 36}%`,
transform: 'translateX(-50%)'
}}
>
<div className="flex justify-between items-center border-b border-zinc-800 pb-1 mb-1">
<span className="font-bold text-zinc-400">{currentChartData[hoveredPoint].label}</span>
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: theme.activeLine }} />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Metrics Focus</span>
<span className="font-extrabold font-mono text-base" style={{ color: theme.activeLine }}>
{chartMetric === 'orders' ? `${points[hoveredPoint].val} Orders` :
chartMetric === 'revenue' ? `${points[hoveredPoint].val.toLocaleString('en-IN')}` :
chartMetric === 'cancelled' ? `${points[hoveredPoint].val} Cancelled` :
`${points[hoveredPoint].val} SKUs`}
</span>
</div>
</div>
)}
</div>
{/* X Axis Labels */}
<div className="flex justify-between items-center text-[10px] font-bold text-zinc-400 uppercase font-mono px-xl border-t border-[#f1f5f9] pt-md mt-sm select-none">
{currentChartData.map((d, index) => (
<span key={index}>{d.label}</span>
))}
{/* 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>
@@ -1204,89 +935,11 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
<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">
<div className="grid grid-cols-1 md:grid-cols-3 gap-lg text-xs">
{/* Inventory Level Progress block */}
<div className="space-y-2">
<span className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider block">
Stock Capacity Index
</span>
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm">
<div className="flex justify-between items-center mb-xs font-semibold">
<span className="text-zinc-655">Current Balance</span>
<span className={
prod.stockStatus === 'Healthy' ? 'text-emerald-600' :
prod.stockStatus === 'Low Stock' ? 'text-amber-600' : 'text-rose-600'
}>
{prod.stockStatus === 'Healthy' ? '142 Units (Optimal)' :
prod.stockStatus === 'Low Stock' ? '42 Units (Low)' : '6 Units (Critical)'}
</span>
</div>
<div className="w-full bg-slate-100 h-2.5 rounded-full overflow-hidden mt-1.5">
<div className={`h-full rounded-full transition-all duration-500 ${prod.stockStatus === 'Healthy' ? 'bg-emerald-500 w-[85%]' :
prod.stockStatus === 'Low Stock' ? 'bg-amber-500 w-[35%]' : 'bg-rose-500 w-[8%]'
}`} />
</div>
</div>
</div>
{/* Distribution Locations */}
<div className="space-y-2">
<span className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider block">
Hub Distribution Allocations
</span>
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm space-y-2">
<div className="flex justify-between text-[10px] font-semibold text-zinc-600">
<span>Saravanampatti Hub</span>
<span className="font-mono">{prod.stockStatus === 'Healthy' ? '85 units' : prod.stockStatus === 'Low Stock' ? '25 units' : '4 units'}</span>
</div>
<div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
<div className="bg-purple-500 h-full rounded-full w-[60%]" />
</div>
<div className="flex justify-between text-[10px] font-semibold text-zinc-600 mt-1">
<span>RS Puram Hub</span>
<span className="font-mono">{prod.stockStatus === 'Healthy' ? '57 units' : prod.stockStatus === 'Low Stock' ? '17 units' : '2 units'}</span>
</div>
<div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
<div className="bg-indigo-500 h-full rounded-full w-[40%]" />
</div>
</div>
</div>
{/* System Audit */}
<div className="space-y-2">
<span className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider block">
Metadata & Barcode Identification
</span>
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm flex items-center justify-between gap-md">
<div className="space-y-1">
<div>
<span className="text-[8px] text-zinc-400 font-bold uppercase block leading-none mb-0.5">Warehouse Bin</span>
<span className="font-mono font-bold text-zinc-750">BIN-C{prod.sku.replace(/\D/g, '').slice(-3) || '042'}</span>
</div>
<div className="pt-1">
<span className="text-[8px] text-zinc-400 font-bold uppercase block leading-none mb-0.5">Last Audited</span>
<span className="text-zinc-650 font-medium">{new Date().toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}</span>
</div>
</div>
{/* Monospace barcode simulation */}
<div className="flex flex-col items-center shrink-0 select-none bg-zinc-50 p-2 rounded-lg border border-zinc-100">
<div className="flex items-center gap-[1.5px] h-7 px-1">
{[1, 3, 1, 2, 4, 1, 3, 2, 1, 2, 3, 1, 2, 4, 1, 2].map((w, idx) => (
<div
key={idx}
className="bg-zinc-805 h-full"
style={{ width: `${w * 0.7}px` }}
/>
))}
</div>
<span className="text-[8px] font-mono text-zinc-400 mt-1 uppercase tracking-wider">{prod.sku}</span>
</div>
</div>
</div>
</div>
{/* 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>