mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:05:24 +02:00
Fix Weekly Grid WOC and Restore Forecast View Graph/UI
This commit is contained in:
@@ -674,6 +674,7 @@ const App: React.FC = () => {
|
|||||||
customerFilters={filters.customer}
|
customerFilters={filters.customer}
|
||||||
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
||||||
top50Ranking={top50Ranking2025}
|
top50Ranking={top50Ranking2025}
|
||||||
|
velocityMap={velocityMap}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+118
-4
@@ -181,9 +181,26 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
|
|
||||||
const globalSummary = useMemo(() => {
|
const globalSummary = useMemo(() => {
|
||||||
return baseFilteredData.reduce((acc, curr) => ({
|
return baseFilteredData.reduce((acc, curr) => ({
|
||||||
actualUnits: acc.actualUnits + curr.actualUnits,
|
actualUnits: acc.actualUnits + (curr.actualUnits || 0),
|
||||||
forecastUnits: acc.forecastUnits + curr.forecastUnits,
|
forecastUnits: acc.forecastUnits + (curr.forecastUnits || 0),
|
||||||
}), { actualUnits: 0, forecastUnits: 0 });
|
annualForecast: acc.annualForecast + (curr.annualForecast || 0),
|
||||||
|
}), { actualUnits: 0, forecastUnits: 0, annualForecast: 0 });
|
||||||
|
}, [baseFilteredData]);
|
||||||
|
|
||||||
|
const chartData = useMemo(() => {
|
||||||
|
const dataMap = new Map<string, { name: string; actual: number; forecast: number }>();
|
||||||
|
MONTH_ORDER.forEach(m => dataMap.set(m, { name: m, actual: 0, forecast: 0 }));
|
||||||
|
|
||||||
|
baseFilteredData.forEach(item => {
|
||||||
|
item.monthlyData?.forEach(m => {
|
||||||
|
const entry = dataMap.get(m.month);
|
||||||
|
if (entry) {
|
||||||
|
entry.actual += m.actual || 0;
|
||||||
|
entry.forecast += m.forecast || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(dataMap.values());
|
||||||
}, [baseFilteredData]);
|
}, [baseFilteredData]);
|
||||||
|
|
||||||
const finalFilteredData = useMemo(() => {
|
const finalFilteredData = useMemo(() => {
|
||||||
@@ -216,7 +233,103 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
}, [finalFilteredData]);
|
}, [finalFilteredData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-hidden flex flex-col min-h-0 bg-slate-950/50">
|
<div className="flex flex-col gap-6 animate-fade-in p-6 h-full overflow-hidden">
|
||||||
|
{/* Top Section: Metrics & Graph */}
|
||||||
|
<div className="flex flex-col xl:flex-row gap-6 h-[400px] shrink-0">
|
||||||
|
{/* Metrics Cards */}
|
||||||
|
<div className="flex flex-col gap-4 min-w-[300px] xl:w-[25%]">
|
||||||
|
{/* Annual Forecast */}
|
||||||
|
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Forecast Total</span>
|
||||||
|
<div className="text-2xl font-black text-white">{(globalSummary.annualForecast || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-slate-500">Units</span></div>
|
||||||
|
</div>
|
||||||
|
{/* Period Forecast */}
|
||||||
|
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
||||||
|
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">Total Forecast (Period)</span>
|
||||||
|
<div className="text-2xl font-black text-white">{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-slate-500">Units</span></div>
|
||||||
|
</div>
|
||||||
|
{/* Actual Sales */}
|
||||||
|
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
||||||
|
<span className="text-[10px] font-black text-emerald-400 uppercase tracking-widest mb-1">Total Actual Sales (Period)</span>
|
||||||
|
<div className="text-2xl font-black text-emerald-400">{(globalSummary.actualUnits || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-emerald-600/70">Units</span></div>
|
||||||
|
</div>
|
||||||
|
{/* Fulfillment */}
|
||||||
|
<div className="flex gap-4 flex-1">
|
||||||
|
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
||||||
|
<span className="text-[10px] font-black text-blue-400 uppercase tracking-widest mb-1">Fulfillment (Period)</span>
|
||||||
|
<div className="text-xl font-black text-white">{globalSummary.forecastUnits > 0 ? ((globalSummary.actualUnits / globalSummary.forecastUnits) * 100).toFixed(1) : '0.0'}%</div>
|
||||||
|
<div className="mt-2 h-1 bg-slate-800 rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-blue-500 rounded-full" style={{ width: `${Math.min(100, globalSummary.forecastUnits > 0 ? ((globalSummary.actualUnits / globalSummary.forecastUnits) * 100) : 0)}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Fulfillment %</span>
|
||||||
|
<div className="text-xl font-black text-white">{globalSummary.annualForecast > 0 ? ((globalSummary.actualUnits / globalSummary.annualForecast) * 100).toFixed(1) : '0.0'}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Graph */}
|
||||||
|
<div className="flex-1 bg-slate-900 border border-white/5 rounded-xl shadow-lg p-6 flex flex-col">
|
||||||
|
<h3 className="flex items-center gap-2 text-sm font-bold text-white mb-6">
|
||||||
|
<TrendingIcon /> Monthly Evolution: Forecast vs Actual
|
||||||
|
</h3>
|
||||||
|
<div className="flex-1 w-full min-h-0">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<ComposedChart data={chartData}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="colorActual" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
|
||||||
|
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="colorForecast" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
||||||
|
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.3} vertical={false} />
|
||||||
|
<XAxis dataKey="name" stroke="#94a3b8" tick={{ fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||||
|
<YAxis stroke="#94a3b8" tick={{ fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', color: '#f8fafc' }}
|
||||||
|
itemStyle={{ fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="actual" name="Actual Sales" fill="#10b981" radius={[4, 4, 0, 0]} barSize={20} />
|
||||||
|
<Line type="monotone" dataKey="forecast" name="Forecast" stroke="#6366f1" strokeWidth={3} dot={{ r: 4, fill: "#6366f1" }} activeDot={{ r: 6 }} />
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Section: Table */}
|
||||||
|
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0">
|
||||||
|
<div className="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0">
|
||||||
|
<h3 className="text-lg font-bold text-white uppercase tracking-tight">Product Performance Comparison</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
|
||||||
|
<div className="flex bg-slate-950/50 p-1 rounded-xl border border-white/10 shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowOnlyTop50(!showOnlyTop50)}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${showOnlyTop50 ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-lg' : 'text-slate-400 hover:text-amber-400'}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm">🏆</span>
|
||||||
|
Top 50 ({top50Mode.toUpperCase()})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="bg-slate-950 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:outline-none w-64"
|
||||||
|
/>
|
||||||
|
<button onClick={handleExportExcel} className="p-2 bg-slate-800 hover:bg-slate-700 text-white rounded-xl border border-white/5 transition-all"><DownloadIcon /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar relative">
|
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar relative">
|
||||||
<table className="w-full text-left border-collapse relative">
|
<table className="w-full text-left border-collapse relative">
|
||||||
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-xs font-bold text-slate-400 uppercase tracking-wider">
|
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||||
@@ -250,6 +363,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface WeeklyGridProps {
|
|||||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
vendorStockFilter: string[];
|
vendorStockFilter: string[];
|
||||||
onVendorStockFilterChange: (newFilters: string[]) => void;
|
onVendorStockFilterChange: (newFilters: string[]) => void;
|
||||||
|
velocityMap?: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortConfig = {
|
type SortConfig = {
|
||||||
@@ -53,7 +54,8 @@ const WeeklyRow: React.FC<{
|
|||||||
renderGrowth: (current: number, previous: number) => React.ReactNode;
|
renderGrowth: (current: number, previous: number) => React.ReactNode;
|
||||||
customerFilters: string[];
|
customerFilters: string[];
|
||||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters, vendorStockMap }) => {
|
velocityMap?: Map<string, number>;
|
||||||
|
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters, vendorStockMap, velocityMap }) => {
|
||||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||||
const asin = row.asin.trim().toUpperCase();
|
const asin = row.asin.trim().toUpperCase();
|
||||||
|
|
||||||
@@ -90,7 +92,12 @@ const WeeklyRow: React.FC<{
|
|||||||
{stockMap && (
|
{stockMap && (
|
||||||
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||||
)}
|
)}
|
||||||
<VendorStockBadge asin={row.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={row.avgWeeklySales} />
|
<VendorStockBadge
|
||||||
|
asin={asin}
|
||||||
|
vendorStockMap={vendorStockMap}
|
||||||
|
mode={top50Mode}
|
||||||
|
avgWeeklySales={velocityMap?.get(asin)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +153,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
vendorStockFilter,
|
vendorStockFilter,
|
||||||
onVendorStockFilterChange,
|
onVendorStockFilterChange,
|
||||||
customerFilters,
|
customerFilters,
|
||||||
top50Mode
|
top50Mode,
|
||||||
|
velocityMap
|
||||||
}) => {
|
}) => {
|
||||||
// Pivot data - memoized
|
// Pivot data - memoized
|
||||||
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||||
@@ -583,6 +591,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
sortConfig={sortConfig}
|
sortConfig={sortConfig}
|
||||||
renderGrowth={renderGrowth}
|
renderGrowth={renderGrowth}
|
||||||
customerFilters={customerFilters}
|
customerFilters={customerFilters}
|
||||||
|
velocityMap={velocityMap}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{displayCount < sortedRows.length && (
|
{displayCount < sortedRows.length && (
|
||||||
|
|||||||
Reference in New Issue
Block a user