feat: relocate orders and deliveries to store console & polish store cover images

This commit is contained in:
Suriya
2026-06-03 18:20:43 +05:30
commit 6eaeb5c4a7
32 changed files with 13430 additions and 0 deletions

View File

@@ -0,0 +1,478 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState } from 'react';
import {
TrendingUp,
TrendingDown,
Download,
Filter,
ArrowUpRight,
ChevronDown,
TrendingUp as TrendUp,
TrendingDown as TrendDown,
Equal as TrendFlat,
ChevronLeft,
ChevronRight,
Info
} 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';
interface ReportsViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
}
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 }: ReportsViewProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState('Month to Date');
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);
// ── 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 fromdate = ymd(monthStart);
const todate = ymd(today);
const summaryQ = useFiestaOrderSummary(FIESTA_TENANT_ID, ymd(yearStart), todate);
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
const insightQ = useFiestaOrderInsight(FIESTA_TENANT_ID);
const stockQ = useFiestaStockStatement({
tenantid: FIESTA_TENANT_ID,
locationid: FIESTA_PRIMARY_LOCATION_ID,
keyword: '',
pageno: 1,
pagesize: 100,
});
const s = summaryQ.data;
const activeSkus = (stockQ.data ?? []).length;
// KPI row — all live.
const reportsKPIs = [
{
title: 'Total Orders (YTD)',
value: (s?.total ?? 0).toLocaleString('en-IN'),
subtext: `${ymd(yearStart)}${todate}`,
trend: `${s?.delivered ?? 0} delivered`,
isPositive: true,
},
{
title: 'Delivered',
value: (s?.delivered ?? 0).toLocaleString('en-IN'),
subtext: `${s ? Math.round(((s.delivered) / Math.max(s.total, 1)) * 100) : 0}% of all orders`,
trend: `${s?.pending ?? 0} pending`,
isPositive: true,
},
{
title: 'Cancelled',
value: (s?.cancelled ?? 0).toLocaleString('en-IN'),
subtext: `${s ? Math.round(((s.cancelled) / Math.max(s.total, 1)) * 100) : 0}% of all orders`,
trend: `${s?.created ?? 0} created`,
isPositive: false,
},
{
title: 'Active SKUs',
value: activeSkus.toLocaleString('en-IN'),
subtext: 'Live stock statement entries',
trend: 'In catalogue',
isPositive: true,
},
];
// Leaderboard — outlets ranked by total live orders.
const leaderboard: LeaderboardNode[] = (() => {
const rows = [...(locSummaryQ.data ?? [])].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,
revenue: `${r.total.toLocaleString('en-IN')} ord`,
}));
})();
const currentLeaderboard = leaderboard;
// Monthly order distribution per outlet (replaces the static hourly heatmap).
const insightRows = (insightQ.data ?? []).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 colour, scaled relative to the busiest month/outlet.
const getHeatmapColorClass = (val: number) => {
const ratio = val / heatmapMax;
if (ratio < 0.15) return 'bg-[#581c87]/10 text-[#0f172a] hover:bg-[#581c87]/20';
if (ratio <= 0.5) return 'bg-[#a78bfa]/40 text-[#581c87] hover:bg-[#a78bfa]/50';
return 'bg-[#581c87] text-white hover:bg-purple-800';
};
// Export alerts
const triggerExport = (format: 'PDF' | 'CSV') => {
alert(`BI Engine initiating automated ${format} bundle export. Generating compiled schema reports...`);
};
return (
<div className="space-y-lg animate-in fade-in duration-500">
{/* Context filter header row */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-md bg-[#f8fafc] border border-[#e2e8f0] p-md rounded-xl shadow-sm">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
Business Intelligence Center
</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
Real-time analytical metrics engine surfacing regional performance deltas and potential logistic constraints.
</p>
</div>
{/* Action picker filters */}
<div className="flex items-center gap-sm flex-wrap text-xs">
<div className="bg-white border border-[#e2e8f0] rounded-lg px-sm py-1.5 flex items-center gap-sm shadow-sm select-none">
<span className="text-zinc-400 font-medium">Timeframe:</span>
<select
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value)}
className="bg-transparent border-none focus:ring-0 font-sans font-semibold text-zinc-700 cursor-pointer outline-none"
>
<option>Month to Date</option>
<option>Year to Date</option>
<option>Last 12 Months</option>
</select>
</div>
<div className="bg-white border border-[#e2e8f0] rounded-lg px-sm py-1.5 flex items-center gap-sm shadow-sm font-semibold text-zinc-700">
<Filter size={14} className="text-zinc-400 font-medium" />
<span>{isCoimbatoreView ? 'Coimbatore Zones (5)' : 'All Regions (12)'}</span>
</div>
<button
onClick={() => triggerExport('PDF')}
className="bg-[#581c87] text-white font-sans font-semibold px-4 py-1.5 rounded-lg flex items-center gap-sm cursor-pointer transition-colors hover:bg-purple-800 active:bg-purple-900 shadow-sm"
>
<Download size={13} />
Export PDF
</button>
</div>
</div>
{/* Primary KPI Row - 4 Key cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-gutter mb-xl text-xs">
{reportsKPIs.map((kpi, idx) => (
<div key={idx} className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm">
<div className="flex justify-between items-start mb-sm">
<span className="text-[11px] font-sans font-bold text-zinc-400 uppercase tracking-widest block font-sans">
{kpi.title}
</span>
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded flex items-center gap-px ${
kpi.isPositive ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'
}`}>
{kpi.isPositive ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
{kpi.trend}
</span>
</div>
<div className="font-sans font-bold text-[#0f172a] text-xl tracking-tight">
{kpi.value}
</div>
<div className="text-[10px] text-zinc-400 font-medium tracking-wide mt-sm uppercase font-sans">
{kpi.subtext}
</div>
</div>
))}
</div>
{/* Bento split maps */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-gutter">
{/* Revenue Heatmap table - 8 Cols */}
<div className="lg:col-span-8 bg-white border border-[#e2e8f0] rounded-xl 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">
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
Monthly Order Distribution by Outlet
</span>
<div className="flex items-center gap-2 text-[10px] font-bold text-zinc-400 uppercase tracking-tight">
<span className="w-2 h-2 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-sm p-sm bg-purple-50 border border-purple-100 rounded-lg text-xs flex justify-between items-center animate-in fade-in">
<span className="font-sans text-blue-900 font-medium">
<strong className="font-bold">{selectedCell.day}</strong> registered <strong className="font-bold font-mono">{selectedCell.val}</strong> order(s) in <strong className="font-bold">{selectedCell.hour}</strong>.
</span>
<button
onClick={() => setSelectedCell(null)}
className="text-xs font-bold text-[#581c87] hover:underline cursor-pointer"
>
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 tenant.'}
</div>
) : (
<table className="w-full text-center border-collapse text-xs font-sans">
<thead>
<tr>
<th className="p-xs text-[10px] font-bold text-zinc-400 w-32 uppercase italic">Outlet</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]">
{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-100 cursor-pointer ${getHeatmapColorClass(val)}`}
>
{val}
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
)}
</div>
<div className="px-md py-sm bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between text-[11px] text-zinc-400 font-sans font-medium">
<span>Colour intensity scales with monthly order volume per outlet</span>
<span>Click cells to inspect metrics</span>
</div>
</div>
{/* Leaderboard nodes bar list - 4 Cols */}
<div className="lg:col-span-4 bg-white border border-[#e2e8f0] rounded-xl flex flex-col shadow-sm mt-0">
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm">
<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.map((node) => (
<div key={node.rank} className="flex items-center gap-md text-xs">
<span className="text-xs font-bold text-zinc-400 font-mono w-4">
{node.rank}
</span>
<div className="flex-1">
<div className="flex justify-between items-center font-medium">
<span className="text-[#0f172a] font-semibold">{node.name}</span>
<span className="text-[#581c87] font-mono font-bold">{node.revenue}</span>
</div>
<div className="w-full bg-[#eceef0] h-1.5 rounded-full mt-1 overflow-hidden">
<div
className="bg-[#581c87] h-full rounded-full transition-all duration-300"
style={{ width: `${node.percentage}%` }}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Detailed Performance Matrix table */}
<div className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden shadow-sm">
{/* 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">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a]">
Product Performance Matrix
</h3>
<p className="text-zinc-500 text-xs font-sans mt-0.5">
Assortment unit sales and physical balance trend indices.
</p>
</div>
{/* Quick interactive filter pills */}
<div className="flex gap-2 text-xs font-semibold">
{(['All', 'Healthy', 'Low Stock', 'Critical'] as const).map((filter) => (
<button
key={filter}
onClick={() => setStockFilter(filter)}
className={`px-3 py-1 rounded-lg cursor-pointer transition-colors ${
stockFilter === filter
? 'bg-[#581c87] text-white shadow-sm'
: 'bg-white border border-[#e2e8f0] text-zinc-600 hover:bg-zinc-50'
}`}
>
{filter}
</button>
))}
<button
onClick={() => triggerExport('CSV')}
className="px-3 py-1 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-800 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-[10px] uppercase font-bold tracking-wider">
<tr>
<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={6} className="text-center py-8 text-zinc-400">
No matching items matching stock filter criteria.
</td>
</tr>
) : (
filteredProducts.map((prod) => (
<tr key={prod.id} className="hover:bg-[#f2f4f6]/40 transition-all">
<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>
<span className="font-semibold text-[#0f172a]">{prod.name}</span>
</td>
<td className="px-md py-md font-mono text-zinc-500 font-medium">
{prod.sku}
</td>
<td className="px-md py-md text-right font-mono text-zinc-600 font-semibold">
{prod.unitsSold.toLocaleString()}
</td>
<td className="px-md py-md text-right font-mono text-zinc-700 font-bold">
{prod.revenue.toLocaleString()}
</td>
<td className="px-md py-md text-right">
<span className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-tight ${
prod.stockStatus === 'Healthy'
? 'bg-emerald-100 text-emerald-750 font-bold font-sans'
: prod.stockStatus === 'Low Stock'
? 'bg-amber-100 text-amber-750 font-bold font-sans'
: 'bg-rose-100 text-rose-750 font-bold font-sans'
}`}>
{prod.stockStatus}
</span>
</td>
<td className="px-md py-md text-center">
<span className="inline-block p-1 bg-[#f8fafc] border border-zinc-100 rounded-full">
{prod.trend === 'up' ? (
<TrendUp size={14} className="text-emerald-500" />
) : prod.trend === 'down' ? (
<TrendDown size={14} className="text-rose-500" />
) : (
<TrendFlat size={14} className="text-zinc-400" />
)}
</span>
</td>
</tr>
))
)}
</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-semibold font-sans">
<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-500 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-500 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>
</div>
);
}