udpates on the ui changesand api integration
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user