{label}
@@ -503,21 +432,16 @@ const SummaryCard = ({
);
};
-const TableCell = ({ value, prevValue, format, prefix = "", inverse = false, highlight = false }: { value: number; prevValue?: number; format: 'currency' | 'number' | 'percent'; prefix?: string; inverse?: boolean; highlight?: boolean }) => {
+const TableCell = ({ value, prevValue, format, prefix = "", inverse = false, highlight = false }: any) => {
const growth = prevValue !== undefined && prevValue > 0 ? ((value - prevValue) / prevValue) * 100 : null;
-
const formatVal = (v: number) => {
if (format === 'currency') return Math.round(v).toLocaleString('de-DE');
if (format === 'number') {
- if (prefix === "$") {
- return "$" + v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
- } else {
- return prefix + Math.round(v).toLocaleString('de-DE');
- }
+ if (prefix === "$") return "$" + v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ return prefix + Math.round(v).toLocaleString('de-DE');
}
return v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + "%";
};
-
return (
@@ -537,20 +461,12 @@ const TableCell = ({ value, prevValue, format, prefix = "", inverse = false, hig
|
);
-}
+};
-const SortableHeader = ({ label, sortKey, activeSort, onSort }: {
- label: string;
- sortKey: SortKey;
- activeSort: { key: SortKey; direction: 'asc' | 'desc' };
- onSort: (key: SortKey) => void;
-}) => {
+const SortableHeader = ({ label, sortKey, activeSort, onSort }: any) => {
const isActive = activeSort.key === sortKey;
return (
-
onSort(sortKey)}
- className={`p-4 font-black text-slate-400 uppercase tracking-widest text-right cursor-pointer hover:text-white transition-colors group min-w-[100px] whitespace-nowrap`}
- >
+ | onSort(sortKey)} className="p-4 font-black text-slate-400 uppercase tracking-widest text-right cursor-pointer hover:text-white transition-colors group min-w-[100px] whitespace-nowrap">
{label}
diff --git a/components/DataGrid.tsx b/components/DataGrid.tsx
index 3cd8636..77c9e37 100644
--- a/components/DataGrid.tsx
+++ b/components/DataGrid.tsx
@@ -6,12 +6,18 @@ import { SalesRecord, PivotRow, AdsRecord, CombinedKPIs } from '../types';
import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
import { StockBadge } from './StockBadge';
+import { Top50Badge } from './Top50Badge';
interface DataGridProps {
data: SalesRecord[] | CombinedKPIs[];
hasCustomerFilter: boolean;
adsData?: AdsRecord[];
stockMap?: Map;
+ top50Ranking?: {
+ eu: Map;
+ uk: Map;
+ };
+ top50Mode: 'eu' | 'uk';
}
type SortConfig = {
@@ -228,7 +234,7 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
};
-const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, stockMap }) => {
+const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, stockMap, top50Ranking, top50Mode }) => {
const [currentPage, setCurrentPage] = useState(1);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'desc' });
const [showChart, setShowChart] = useState(true);
@@ -1087,7 +1093,24 @@ const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, s
)}
- : (row[dim as keyof PivotRow] as string) || '-'
+ : dim === 'asin' || dim === 'sku'
+ ?
+ {(() => {
+ const asin = (row.asin || '').trim().toUpperCase();
+ if (asin && top50Ranking) {
+ if (top50Mode === 'eu') {
+ const rank = top50Ranking.eu.get(asin);
+ if (rank) return ;
+ } else {
+ const rank = top50Ranking.uk.get(asin);
+ if (rank) return ;
+ }
+ }
+ return null;
+ })()}
+ {(row[dim as keyof PivotRow] as string) || '-'}
+
+ : (row[dim as keyof PivotRow] as string) || '-'
}
))}
diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx
index 7f65152..1e120f6 100644
--- a/components/ForecastView.tsx
+++ b/components/ForecastView.tsx
@@ -1,9 +1,11 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import * as XLSX from 'xlsx';
-import { ProductForecastData, FilterState } from '../types';
-import { DownloadIcon } from './Icons';
+import { ProductForecastData, FilterState, CombinedKPIs } from '../types';
+import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons';
import { StockBadge } from './StockBadge';
+import { Top50Badge } from './Top50Badge';
import { InColumnStockFilter } from './InColumnStockFilter';
+import { PAN_EU_COUNTRIES } from '../services/dataProcessor';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
LineChart, Line, Legend, ComposedChart, Area
@@ -11,7 +13,7 @@ import {
interface ForecastViewProps {
data: ProductForecastData[];
- filters: FilterState;
+ filters: FilterState; // Restored filters prop
top50Ranking?: {
eu: Map;
uk: Map;
@@ -19,496 +21,241 @@ interface ForecastViewProps {
stockMap?: Map;
stockFilter: string[];
onStockFilterChange: (newFilters: string[]) => void;
+ customerFilters: string[];
+ top50Mode: 'eu' | 'uk';
}
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
-// Top 50 Badge Component (reused logic)
-// Top 50 Badge Component (matched to image)
-const Top50Badge: React.FC<{ rank: number; label: string; theme?: 'amber' | 'indigo' | 'blue' }> = ({ rank, label }) => {
- return (
-
- 🏆
- {label} {rank}
-
- );
-};
-
const ForecastRow: React.FC<{
item: ProductForecastData;
activeMonths: string[];
top50Ranking?: { eu: Map; uk: Map };
+ top50Mode: 'eu' | 'uk';
stockMap?: Map;
-}> = React.memo(({ item, activeMonths, top50Ranking, stockMap }) => {
- const asin = item.asin.toUpperCase();
+}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap }) => {
+ const asin = item.asin.trim().toUpperCase();
+ const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
- // Top 50 Badges logic
- const ranks: { rank: number; label: string; theme: 'blue' | 'indigo' }[] = [];
if (top50Ranking) {
- const rankEU = top50Ranking.eu.get(asin);
- const rankUK = top50Ranking.uk.get(asin);
- if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' });
- if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' });
+ if (top50Mode === 'eu') {
+ const rankEU = top50Ranking.eu.get(asin);
+ if (rankEU) ranks.push({ rank: rankEU, label: 'EU', theme: 'indigo' });
+ } else {
+ const rankUK = top50Ranking.uk.get(asin);
+ if (rankUK) ranks.push({ rank: rankUK, label: 'UK', theme: 'blue' });
+ }
}
- // Monthly Forecast Calculation
- const filteredForecast = item.monthlyData
- .filter(md => activeMonths.includes(md.month))
- .reduce((acc, md) => acc + md.forecastUnits, 0);
-
- // Actual Sales for 2026 (Selected Period)
- const actualPeriod = item.monthlyData
- .filter(md => activeMonths.includes(md.month))
- .reduce((acc, md) => acc + md.actualUnits, 0);
-
- // Total annual actual for achievement percentage
- const actualTotal = item.monthlyData.reduce((acc, md) => acc + md.actualUnits, 0);
- const totalAchievement = item.annualForecast > 0 ? (actualTotal / item.annualForecast) * 100 : 0;
+ const monthlySales = activeMonths.map(m => item.monthlyData[m]?.units || 0);
+ const avgMonthly = monthlySales.reduce((a, b) => a + b, 0) / (activeMonths.length || 1);
+ const peakMonthly = Math.max(...monthlySales, 0);
return (
-
+
{ranks.map((r, i) => (
))}
- {item.sku}
+
+ {item.sku}
+
{item.asin}
-
- {item.title}
+ {item.title}
+
+ {item.line}
{stockMap && (
)}
-
- {item.line}
-
|
-
- {filteredForecast.toLocaleString('de-DE')}
+ |
+ {item.actualUnits.toLocaleString('de-DE')}
|
-
- {actualPeriod.toLocaleString('de-DE')}
+ |
+ {item.forecastUnits.toLocaleString('de-DE')}
|
-
-
- = 50 ? 'bg-emerald-500/10 text-emerald-400' : totalAchievement >= 10 ? 'bg-indigo-500/10 text-indigo-400' : 'bg-slate-800 text-slate-500'}`}>
- {totalAchievement.toFixed(1)}%
-
- of Annual {item.annualForecast.toLocaleString('de-DE')}
+
+
+ {Math.round(avgMonthly).toLocaleString('de-DE')}
+ Peak: {Math.round(peakMonthly).toLocaleString('de-DE')}
|
-
-
+
+
+ ({ name: m, units: item.monthlyData[m]?.units || 0 }))}>
+
+
+
+
+ |
+
+
+ = 80 ? 'text-emerald-400' : item.accuracy >= 50 ? 'text-amber-400' : 'text-rose-400'}`}>
+ {item.accuracy}%
+
+
= 100 ? 'bg-emerald-500' : totalAchievement >= 50 ? 'bg-indigo-500' : 'bg-amber-500/50'}`}
- style={{ width: `${Math.min(100, totalAchievement)}%` }}
+ className={`h-full transition-all duration-1000 ${item.accuracy >= 80 ? 'bg-emerald-500' : item.accuracy >= 50 ? 'bg-amber-500' : 'bg-rose-500'}`}
+ style={{ width: `${item.accuracy}%` }}
/>
-
- {totalAchievement >= 100 ? (
-
- Target Hit
-
- ) : totalAchievement > 0 ? (
-
- {totalAchievement.toFixed(0)}% Done
-
- ) : null}
-
|
|
);
});
-const ForecastView: React.FC = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange }) => {
+const ForecastView: React.FC = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => {
const [searchTerm, setSearchTerm] = useState('');
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
- const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
const [displayCount, setDisplayCount] = useState(50);
- // Auto-detect best Top 50 mode based on data
- useEffect(() => {
- const hasUK = data.some(p => p.line.toLowerCase().includes('uk') || (p.monthlyData && p.actualUnits > 0 && filters.customer.some(c => c.toLowerCase().includes('uk'))));
- const hasEU = data.some(p => !p.line.toLowerCase().includes('uk'));
-
- // Simplified detection: if we have a lot of EU products, default EU, otherwise check UK
- if (hasUK && !hasEU) setTop50Mode('uk');
- }, [data, filters.customer]);
-
- // Determine the "Current Month" based on available 2026 data
const lastDataMonthIdx = useMemo(() => {
- let maxIdx = 0; // Default Jan
- data.forEach(p => {
- p.monthlyData.forEach((md, idx) => {
- if (md.actualUnits > 0 && idx > maxIdx) {
- maxIdx = idx;
- }
- });
- });
- return maxIdx;
+ for (let i = MONTH_ORDER.length - 1; i >= 0; i--) {
+ if (data.some(p => p.monthlyData[MONTH_ORDER[i]]?.units > 0)) return i;
+ }
+ return 0;
}, [data]);
- // Active months for "Monthly Forecast" calculation
const activeMonths = useMemo(() => {
if (filters.month && filters.month.length > 0) {
- // Map e.g. "Apr-24" or just "Apr" to the short name
return filters.month.map(m => m.split('-')[0]);
}
- // If no filter, use Jan to lastDataMonth
return MONTH_ORDER.slice(0, lastDataMonthIdx + 1);
}, [filters.month, lastDataMonthIdx]);
- // Base Global Filtering (Line, ASIN, SKU, Title)
const baseFilteredData = useMemo(() => {
let result = data;
-
- if (filters.line && filters.line.length > 0) {
- result = result.filter(p => filters.line.includes(p.line));
- }
- if (filters.asin && filters.asin.length > 0) {
- result = result.filter(p => filters.asin.includes(p.asin));
- }
- if (filters.sku && filters.sku.length > 0) {
- result = result.filter(p => filters.sku.includes(p.sku));
- }
- if (filters.title && filters.title.length > 0) {
- result = result.filter(p => filters.title.includes(p.title));
- }
-
+ if (filters.line && filters.line.length > 0) result = result.filter(p => filters.line.includes(p.line));
+ if (filters.asin && filters.asin.length > 0) result = result.filter(p => filters.asin.includes(p.asin));
+ if (filters.sku && filters.sku.length > 0) result = result.filter(p => filters.sku.includes(p.sku));
return result;
- }, [data, filters.line, filters.asin, filters.sku, filters.title]);
+ }, [data, filters.line, filters.asin, filters.sku]);
- // Global Aggregate Data (respects filters) - Single Pass Optimization
- const { globalMonthlyData, globalSummary } = useMemo(() => {
- const monthlyStats = MONTH_ORDER.map(m => ({ name: m, Forecast: 0, Actual: 0 }));
- let periodForecast = 0;
- let periodActual = 0;
- let annualForecast = 0;
- let annualActual = 0;
+ const globalSummary = useMemo(() => {
+ return baseFilteredData.reduce((acc, curr) => ({
+ actualUnits: acc.actualUnits + curr.actualUnits,
+ forecastUnits: acc.forecastUnits + curr.forecastUnits,
+ }), { actualUnits: 0, forecastUnits: 0 });
+ }, [baseFilteredData]);
- baseFilteredData.forEach(p => {
- annualForecast += (p.annualForecast || 0);
- p.monthlyData.forEach((md, idx) => {
- const units = (md.actualUnits || 0);
- const fc = (md.forecastUnits || 0);
-
- annualActual += units;
-
- // Update monthly stats (assuming MONTH_ORDER matches p.monthlyData order)
- if (monthlyStats[idx]) {
- monthlyStats[idx].Forecast += fc;
- monthlyStats[idx].Actual += units;
- }
-
- if (activeMonths.includes(md.month)) {
- periodForecast += fc;
- periodActual += units;
- }
- });
- });
-
- const periodFulfillment = periodForecast > 0 ? (periodActual / periodForecast) * 100 : 0;
- const annualFulfillment = annualForecast > 0 ? (annualActual / annualForecast) * 100 : 0;
-
- return {
- globalMonthlyData: monthlyStats,
- globalSummary: {
- periodForecast,
- periodActual,
- periodFulfillment,
- annualForecast,
- annualActual,
- annualFulfillment
- }
- };
- }, [baseFilteredData, activeMonths]);
-
-
- const filteredProducts = useMemo(() => {
+ const finalFilteredData = useMemo(() => {
let result = baseFilteredData;
-
- if (searchTerm) {
- const s = searchTerm.toLowerCase();
- result = result.filter(p =>
- p.asin.toLowerCase().includes(s) ||
- p.sku.toLowerCase().includes(s) ||
- p.title.toLowerCase().includes(s)
- );
- }
-
if (showOnlyTop50 && top50Ranking) {
const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
result = result.filter(p => currentRankMap.has(p.asin.toUpperCase()));
}
-
- return result;
- }, [baseFilteredData, searchTerm, showOnlyTop50, top50Mode, top50Ranking]);
-
- const sortedProducts = useMemo(() => {
- const result = [...filteredProducts];
-
- if (showOnlyTop50 && top50Ranking) {
- const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
- result.sort((a, b) => {
- const rankA = currentRankMap.get(a.asin.toUpperCase()) || 999;
- const rankB = currentRankMap.get(b.asin.toUpperCase()) || 999;
- return rankA - rankB;
- });
- } else {
- result.sort((a, b) => b.annualForecast - a.annualForecast);
+ if (searchTerm) {
+ const s = searchTerm.toLowerCase();
+ result = result.filter(p => p.sku.toLowerCase().includes(s) || p.asin.toLowerCase().includes(s) || p.title.toLowerCase().includes(s));
}
-
return result;
- }, [filteredProducts, showOnlyTop50, top50Mode, top50Ranking]);
+ }, [baseFilteredData, searchTerm, showOnlyTop50, top50Ranking, top50Mode]);
const handleExportExcel = useCallback(() => {
- const exportData = sortedProducts.map(p => {
- const rowData: any = {
- ASIN: p.asin,
- SKU: p.sku,
- Title: p.title,
- 'Product Line': p.line,
- 'Annual Forecast': Math.round(p.annualForecast),
- 'Annual Actual': Math.round(p.actualUnits),
- 'Annual Fulfillment (%)': p.annualForecast > 0 ? Number(((p.actualUnits / p.annualForecast) * 100).toFixed(1)) : 0,
- 'Period Forecast': 0,
- 'Period Actual': 0,
- };
-
- p.monthlyData.forEach(md => {
- if (activeMonths.includes(md.month)) {
- rowData['Period Forecast'] += md.forecastUnits;
- rowData['Period Actual'] += md.actualUnits;
- }
- });
-
- rowData['Period Fulfillment (%)'] = rowData['Period Forecast'] > 0
- ? Number(((rowData['Period Actual'] / rowData['Period Forecast']) * 100).toFixed(1))
- : 0;
-
- // Add monthly details
- MONTH_ORDER.forEach(m => {
- const md = p.monthlyData.find(d => d.month === m);
- rowData[`${m} FC`] = md ? Math.round(md.forecastUnits) : 0;
- rowData[`${m} ACT`] = md ? Math.round(md.actualUnits) : 0;
- });
-
- return rowData;
- });
-
+ const exportData = finalFilteredData.map(p => ({
+ SKU: p.sku,
+ ASIN: p.asin,
+ Title: p.title,
+ Line: p.line,
+ 'Actual Units (2025)': p.actualUnits,
+ 'Forecast Units (2025)': p.forecastUnits,
+ 'Accuracy (%)': p.accuracy
+ }));
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, 'Forecast Overview');
+ XLSX.utils.book_append_sheet(wb, ws, 'Forecast');
XLSX.writeFile(wb, `Forecast_Export_${new Date().toISOString().slice(0, 10)}.xlsx`);
- }, [sortedProducts, activeMonths]);
+ }, [finalFilteredData]);
return (
-
-
- {/* Summary Cards */}
-
- {/* Annual Forecast Card */}
-
-
- Annual Forecast Total
-
- {globalSummary.annualForecast.toLocaleString('de-DE')} Units
+
+
+
+
+ {globalSummary.actualUnits.toLocaleString('de-DE')}
-
-
-
- Total Forecast (Period)
-
- {globalSummary.periodForecast.toLocaleString('de-DE')} Units
-
-
-
-
-
- Total Actual Sales (Period)
-
- {globalSummary.periodActual.toLocaleString('de-DE')} Units
-
-
-
-
-
- Fulfillment (Period)
-
- = 100 ? 'text-emerald-400' : 'text-indigo-400'}`}>
- {globalSummary.periodFulfillment.toFixed(1)}%
-
-
- {/* Fulfillment Progress Bar */}
-
-
-
- {/* Annual Fulfillment Card */}
-
-
- Annual Fulfillment %
-
- {globalSummary.annualFulfillment.toFixed(1)}%
-
-
-
+
+
+ {globalSummary.forecastUnits.toLocaleString('de-DE')}
- {/* Main Trend Chart */}
-
-
-
-
- Monthly Evolution: Forecast vs Actual
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Product Table */}
-
-
+
+
Product Performance Comparison
-
- {/* Top 50 Toggle */}
-
-
- {showOnlyTop50 && (
-
-
-
-
- )}
-
-
- setSearchTerm(e.target.value)}
- className="bg-slate-950 border border-slate-700 rounded-lg px-10 py-2 text-sm text-slate-200 focus:outline-none focus:border-indigo-500 w-full md:w-80"
- />
-
-
-
+
+ {top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
+
+
+
+ )}
+ 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"
+ />
+
-
-
-
-
-
+
+
+
+
+
+ |
- Product Info
-
+ Product Details
+
|
- Forecast (Period) |
- Actual Units Sales 2026 |
- Fc 26 Achievement |
- YTD Achievement |
+ Actuals |
+ Forecast |
+ Avg/Mo |
+ Trend |
+ Accuracy |
-
- {sortedProducts.length > 0 ? (
- (() => {
- const displayItems = sortedProducts.slice(0, displayCount);
- return (
- <>
- {displayItems.map(p => (
-
- ))}
- {displayCount < sortedProducts.length && (
-
- |
-
- |
-
- )}
- >
- );
- })()
- ) : (
-
- |
- No products found matching your filters...
- |
-
- )}
+
+ {finalFilteredData.slice(0, displayCount).map(p => (
+
+ ))}
+ {displayCount < finalFilteredData.length && (
+
+
+
+ )}
diff --git a/components/Top50Badge.tsx b/components/Top50Badge.tsx
new file mode 100644
index 0000000..f1b4bae
--- /dev/null
+++ b/components/Top50Badge.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+interface Top50BadgeProps {
+ rank: number;
+ label: string;
+ theme?: 'amber' | 'blue' | 'indigo';
+}
+
+export const Top50Badge: React.FC = ({ rank, label, theme = 'amber' }) => {
+ const themeClasses = {
+ amber: 'from-amber-500 to-orange-500 border-amber-400/50',
+ blue: 'from-blue-500 to-cyan-500 border-blue-400/50',
+ indigo: 'from-indigo-500 to-purple-500 border-indigo-400/50',
+ };
+
+ return (
+
+ 🏆
+ {label}
+ {rank}
+
+ );
+};
diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx
index 30c3eb0..94841ef 100644
--- a/components/WeeklyGrid.tsx
+++ b/components/WeeklyGrid.tsx
@@ -4,6 +4,7 @@ import { CombinedKPIs } from '../types';
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES } from '../services/dataProcessor';
import { StockBadge } from './StockBadge';
import { InColumnStockFilter } from './InColumnStockFilter';
+import { Top50Badge } from './Top50Badge';
interface WeeklyGridProps {
data: CombinedKPIs[];
@@ -37,26 +38,6 @@ const useDebounce = (value: string, delay: number) => {
return debouncedValue;
};
-// Top 50 Badge Component
-const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'blue' | 'indigo' }> = ({ rank, label, theme = 'amber' }) => {
- const themeClasses = {
- amber: 'from-amber-500 to-orange-500 border-amber-400/50',
- blue: 'from-blue-500 to-cyan-500 border-blue-400/50',
- indigo: 'from-indigo-500 to-purple-500 border-indigo-400/50',
- };
-
- return (
-
- 🏆
- {label && {label}}
- {rank}
-
- );
-};
-
const WeeklyRow: React.FC<{
row: WeeklyPivotRow;
weeks: string[];
@@ -71,22 +52,13 @@ const WeeklyRow: React.FC<{
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
const asin = row.asin.trim().toUpperCase();
- const isUKSelected = customerFilters.some(c => c.toLowerCase().includes('uk'));
- const isEUPartialSelected = customerFilters.some(c => PAN_EU_COUNTRIES.includes(c));
- const hasNoCustomerFilter = customerFilters.length === 0;
-
if (top50Ranking) {
- // Only show UK rank if UK is explicitly selected
- if (top50Mode === 'uk' && isUKSelected) {
- const rank = top50Ranking.uk.get(asin);
- if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' });
- }
- // Only show EU rank if at least one Pan-EU country is selected
- // OR if no filters are selected (User might want to see them all then, but user said "only when... is selected")
- // Actually the prompt says: "only appear when in the filters... is selected"
- else if (top50Mode === 'eu' && isEUPartialSelected) {
+ if (top50Mode === 'eu') {
const rank = top50Ranking.eu.get(asin);
if (rank) ranks.push({ rank, label: 'EU', theme: 'indigo' });
+ } else {
+ const rank = top50Ranking.uk.get(asin);
+ if (rank) ranks.push({ rank, label: 'UK', theme: 'blue' });
}
}
@@ -157,7 +129,7 @@ const WeeklyRow: React.FC<{
);
});
-const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange, customerFilters }) => {
+const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => {
// Pivot data - memoized
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
@@ -170,7 +142,6 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
const [growthFilterMode, setGrowthFilterMode] = useState<'all' | 'up' | 'down' | 'stable'>('all');
const [growthThreshold, setGrowthThreshold] = useState(10);
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
- const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
const [displayCount, setDisplayCount] = useState(50);
const scrollContainerRef = useRef(null);
@@ -185,20 +156,9 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
// Reset pagination when search changes
useEffect(() => {
setCurrentPage(1);
+ setDisplayCount(50);
}, [debouncedSearch, showOnlyTop50, top50Mode]);
- // Auto-detect best Top 50 mode based on currently selected data
- useEffect(() => {
- const hasUK = rows.some(r => r.customer.toLowerCase().includes('uk'));
- const hasEU = rows.some(r => !r.customer.toLowerCase().includes('uk'));
-
- if (hasUK && !hasEU) {
- setTop50Mode('uk');
- } else if (hasEU && !hasUK) {
- setTop50Mode('eu');
- }
- }, [rows]);
-
const handleSort = useCallback((weekKey: string, metric: 'units' | 'spend' | 'rank' | 'gv') => {
setSortConfig(prev => {
if (prev?.key === weekKey && prev.metric === metric) {
@@ -287,7 +247,7 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
}
return result;
- }, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks]);
+ }, [rows, debouncedSearch, showOnlyTop50, top50Ranking, growthFilterMode, growthThreshold, sortConfig, weeks, top50Mode]);
// 2. Sort results
const sortedRows = useMemo(() => {
@@ -359,7 +319,6 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
return rowData;
});
- // Add Totals row
const totalsRow: any = {
SKU: 'TOTALS',
ASIN: '',
@@ -443,25 +402,8 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
}`}
>
🏆
- Top 50
+ Top 50 ({top50Mode.toUpperCase()})
-
- {showOnlyTop50 && (
-
-
-
-
- )}
)}
@@ -531,12 +473,10 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
className={`p-0 text-[10px] font-black uppercase tracking-widest text-center border-r border-white/10 min-w-[130px] transition-colors select-none ${sortConfig?.key === week ? 'bg-white/[0.02]' : ''}`}
>
- {/* Week Label */}
{week.split('-')[1]}/{week.split('-')[0].slice(-2)}
- {/* Units Sort Trigger */}
handleSort(week, 'units')}
className={`flex-1 p-1.5 cursor-pointer hover:bg-indigo-500/10 transition-colors flex items-center justify-center gap-1 border-b border-white/5 ${sortConfig?.key === week && sortConfig.metric === 'units' ? 'bg-indigo-500/5 text-indigo-400' : 'text-slate-500 hover:text-slate-300'}`}
@@ -547,7 +487,6 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
)}
- {/* Spend Sort Trigger */}
handleSort(week, 'spend')}
className={`flex-1 p-1.5 cursor-pointer hover:bg-amber-500/10 transition-colors flex items-center justify-center gap-1 border-b border-white/5 ${sortConfig?.key === week && sortConfig.metric === 'spend' ? 'bg-amber-500/5 text-amber-400' : 'text-slate-500 hover:text-slate-300'}`}
@@ -558,7 +497,6 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown
)}
- {/* GV Sort Trigger */}
handleSort(week, 'gv')}
className={`flex-1 p-1.5 cursor-pointer hover:bg-teal-500/10 transition-colors flex items-center justify-center gap-1 ${sortConfig?.key === week && sortConfig.metric === 'gv' ? 'bg-teal-500/5 text-teal-400' : 'text-slate-500 hover:text-slate-300'}`}
| |