+ {/* Hidden file inputs — always in DOM */}
+
e.target.files?.[0] && loadFile(e.target.files[0], 'sellin')}
+ onClick={e => { (e.target as HTMLInputElement).value = ''; }}
+ />
+
e.target.files?.[0] && loadFile(e.target.files[0], 'budget')}
+ onClick={e => { (e.target as HTMLInputElement).value = ''; }}
+ />
- return {
- ...item,
- forecastPeriod,
- ytdAchievement,
- fcAchievement
- };
- });
- }, [baseFilteredData, activeMonths]);
-
- const processedData = useMemo(() => {
- let result = calculatedData.slice();
-
- if (showOnlyTop50 && top50Ranking) {
- const currentRankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk;
- result = result.filter(p => currentRankMap.has(p.asin.toUpperCase()));
- }
-
- if (debouncedSearch) {
- const searchTerms = debouncedSearch.toLowerCase().split(/[\s,]+/).filter(term => term.length > 0);
- if (searchTerms.length > 0) {
- result = result.filter(p => {
- const rowValues = [
- p.sku.toLowerCase(),
- p.asin.toLowerCase(),
- p.title.toLowerCase()
- ];
- return searchTerms.some(term =>
- rowValues.some(val => val.includes(term))
- );
- });
- }
- }
-
- // Column Filters (Excel-style)
- if (Object.keys(columnFilters).length > 0) {
- (Object.entries(columnFilters) as [string, ColumnFilterCondition][]).forEach(([key, condition]) => {
- if (!condition) return;
-
- if (condition.selectedValues && condition.selectedValues.length > 0) {
- result = result.filter(p => {
- const val = String((p as any)[key] || '');
- return condition.selectedValues?.includes(val);
- });
- }
-
- if (condition.textFilter) {
- const { operator, value } = condition.textFilter;
- const lowerValue = value.toLowerCase();
-
- result = result.filter(p => {
- const rowVal = String((p as any)[key] || '').toLowerCase();
- switch (operator) {
- case 'equals': return rowVal === lowerValue;
- case 'notEquals': return rowVal !== lowerValue;
- case 'contains': return rowVal.includes(lowerValue);
- case 'notContains': return !rowVal.includes(lowerValue);
- case 'startsWith': return rowVal.startsWith(lowerValue);
- case 'notStartsWith': return !rowVal.startsWith(lowerValue);
- case 'endsWith': return rowVal.endsWith(lowerValue);
- case 'notEndsWith': return !rowVal.endsWith(lowerValue);
- default: return true;
- }
- });
- }
- });
- }
-
- // Apply WOC Filter
- if (wocFilter && wocFilter.length > 0 && vendorStockMap) {
- result = result.filter(p => {
- const asin = p.asin.trim().toUpperCase();
- const stockData = vendorStockMap.get(asin);
- if (!stockData) return false;
-
- const stock = top50Mode === 'uk' ? stockData.uk : stockData.eu;
- const velocity = p.avgWeeklySales || 0;
-
- let woc: number = 0;
- if (velocity > 0) {
- woc = stock / velocity;
- } else if (stock > 0) {
- woc = 999;
- }
-
- return wocFilter.some(f => {
- if (f === '< 4 Weeks') return woc < 4;
- if (f === '> 4 Weeks') return woc >= 4;
- if (f === 'Out of Stock') return woc === 0;
- if (f === 'Infinite Cover') return woc === 999;
- return true;
- });
- });
- }
-
- // Apply Sorting
- const { key, direction } = sortConfig;
- result.sort((a: any, b: any) => {
- let valA = a[key];
- let valB = b[key];
-
- if (typeof valA === 'string') valA = valA.toLowerCase();
- if (typeof valB === 'string') valB = valB.toLowerCase();
-
- if (valA < valB) return direction === 'asc' ? -1 : 1;
- if (valA > valB) return direction === 'asc' ? 1 : -1;
- return 0;
- });
-
- return result;
- }, [calculatedData, debouncedSearch, showOnlyTop50, top50Ranking, top50Mode, sortConfig, wocFilter, vendorStockMap, columnFilters]);
-
- // [MOVED HERE] Global Summary - Now respects all filters including Search/WOC
- const globalSummary = useMemo(() => {
- return processedData.reduce((acc, curr) => {
- const itemPeriodForecast = activeMonths.reduce((sum, month) => {
- return sum + (curr.monthlyData?.[month]?.forecastUnits || 0);
- }, 0);
-
- const asin = curr.asin.trim().toUpperCase();
- const stockData = vendorStockMap?.get(asin);
- const stock = stockData ? (top50Mode === 'uk' ? stockData.uk : stockData.eu) : 0;
- const velocity = curr.avgWeeklySales || 0;
-
- return {
- actualUnits: acc.actualUnits + (curr.actualUnits || 0),
- forecastUnits: acc.forecastUnits + itemPeriodForecast,
- annualForecast: acc.annualForecast + (curr.annualForecast || 0),
- totalStock: acc.totalStock + stock,
- totalVelocity: acc.totalVelocity + velocity
- };
- }, { actualUnits: 0, forecastUnits: 0, annualForecast: 0, totalStock: 0, totalVelocity: 0 });
- }, [processedData, activeMonths, vendorStockMap, top50Mode]);
-
- // [MOVED HERE] Chart Data - Now respects all filters including Search/WOC
- const chartData = useMemo(() => {
- const dataMap = new Map
();
- MONTH_ORDER.forEach(m => dataMap.set(m, { name: m, actual: 0, forecast: 0 }));
-
- processedData.forEach(item => {
- if (item.monthlyData) {
- Object.values(item.monthlyData).forEach((m: any) => {
- const entry = dataMap.get(m.month);
- if (entry) {
- entry.actual += m.actualUnits || 0;
- entry.forecast += m.forecastUnits || 0;
- }
- });
- }
- });
- return Array.from(dataMap.values());
- }, [processedData]);
-
- const paginatedData = useMemo(() => processedData.slice(0, displayCount), [processedData, displayCount]);
-
- const handleExportExcel = useCallback(() => {
- const exportData = processedData.map(p => ({
- SKU: p.sku,
- ASIN: p.asin,
- Title: p.title,
- Line: p.line,
- 'Annual Forecast': p.annualForecast,
- 'Forecast (Period)': p.forecastPeriod,
- 'Actual Units (2026)': p.actualUnits,
- 'FC 26 Achievement (%)': p.fcAchievement,
- 'YTD Achievement (%)': p.ytdAchievement
- }));
- const ws = XLSX.utils.json_to_sheet(exportData);
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, 'Forecast');
- XLSX.writeFile(wb, `Forecast_Export_${new Date().toISOString().slice(0, 10)}.xlsx`);
- }, [processedData]);
-
- const SortIndicator = ({ column }: { column: string }) => {
- if (sortConfig.key !== column) return ↕;
- return {sortConfig.direction === 'desc' ? '↓' : '↑'};
- };
-
- return (
-
-
-
-
-
Annual Forecast Total
-
{(globalSummary.annualForecast || 0).toLocaleString('de-DE')} Units
-
-
-
Total Forecast (Period)
-
{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')} Units
-
-
-
Total Actual Sales (Period)
-
{(globalSummary.actualUnits || 0).toLocaleString('de-DE')} Units
-
-
-
-
Fulfillment (Period)
-
{globalSummary.forecastUnits > 0 ? ((globalSummary.actualUnits / globalSummary.forecastUnits) * 100).toFixed(1) : '0.0'}%
-
-
0 ? ((globalSummary.actualUnits / globalSummary.forecastUnits) * 100) : 0)}%` }} />
-
-
-
-
Annual Fulfillment %
-
{globalSummary.annualForecast > 0 ? ((globalSummary.actualUnits / globalSummary.annualForecast) * 100).toFixed(1) : '0.0'}%
-
-
- {/* New Aggregate WOC Card */}
-
-
Average Week Coverage
-
-
0 && (globalSummary.totalStock / globalSummary.totalVelocity) < 4 ? 'text-rose-400' : 'text-emerald-400'}`}>
- {globalSummary.totalVelocity > 0
- ? (globalSummary.totalStock / globalSummary.totalVelocity).toFixed(1)
- : (globalSummary.totalStock > 0 ? '> 52' : '0.0')}
-
-
Weeks
-
-
- Total Stock: {globalSummary.totalStock.toLocaleString('de-DE')} | Velocity: {globalSummary.totalVelocity.toFixed(1)}/wk
-
-
-
-
-
-
- Monthly Evolution: Forecast vs Actual
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Product Performance Comparison (v2.5 optimized)
-
-
-
Showing {paginatedData.length} of {processedData.length}
- {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"
- />
-
-
}
- />
-
}
- options={[
- 'Out of Stock (0)',
- 'In Stock (>0)',
- 'In Stock (>20)',
- 'Low Stock (<10)',
- ]}
- />
-
}
- options={[
- '< 4 Weeks',
- '> 4 Weeks',
- 'Out of Stock',
- 'Infinite Cover'
- ]}
- />
-
-
-
-
-
-
-
-
-
-
- handleSort('sku')}
- >
- Product Info
-
-
- d.sku))).sort()}
- currentFilter={columnFilters['sku']}
- onFilterChange={handleColumnFilterChange}
- icon={SKU}
- />
- d.asin))).sort()}
- currentFilter={columnFilters['asin']}
- onFilterChange={handleColumnFilterChange}
- icon={ASIN}
- />
- d.title))).sort()}
- currentFilter={columnFilters['title']}
- onFilterChange={handleColumnFilterChange}
- icon={Title}
- />
-
-
-
- }
- />
- }
- options={[
- 'Out of Stock (0)',
- 'In Stock (>0)',
- 'In Stock (>20)',
- 'Low Stock (<10)',
- ]}
- />
- }
- options={[
- '< 4 Weeks',
- '> 4 Weeks',
- 'Out of Stock',
- 'Infinite Cover'
- ]}
- />
-
-
- |
- handleSort('annualForecast')}>
- Annual Forecast
- |
- handleSort('forecastPeriod')}>
- Forecast (Period)
- |
- handleSort('actualUnits')}>
- Actual Units 2026
- |
- handleSort('fcAchievement')}>
- FC 26 Achievement
- |
- handleSort('ytdAchievement')}>
- YTD Achievement
- |
-
-
-
- {paginatedData.map(item => (
-
- ))}
-
-
-
- {displayCount < processedData.length && (
-
-
-
- )}
-
- {processedData.length === 0 && (
-
-
No forecast data found for current filters
-
- )}
-
+ {/* ── Upload UI (shown until budget is loaded) ── */}
+ {!isReady && (
+
+
+ Sell-in vs Budget 2026 — Carga los ficheros
+
+
+ loadFile(f, 'sellin')}
+ onClick={() => sellInInputRef.current?.click()}
+ />
+ loadFile(f, 'budget')}
+ onClick={() => budgetInputRef.current?.click()}
+ />
+
+ {error && (
+
+ {error}
+ )}
- );
+ )}
+
+ {/* ── Compact toolbar (shown when budget is loaded) ── */}
+ {isReady && (
+
+
+
+ {error &&
{error}}
+
+
+ Período hasta
+
+
+
+
+ )}
+
+ {/* ── KPI Cards ── */}
+ {isReady && (
+
+
+
Total Real
+
{kpis.totalActual.toLocaleString('de-DE')}
+
Unidades vendidas a Amazon
+
+
+
+ Forecast Jan–{MONTHS_SHORT[cutoffMonth]}
+
+
{kpis.totalPeriodForecast.toLocaleString('de-DE')}
+
+ {kpis.pctOverall.toFixed(1)}% cumplimiento global
+
+
+
+
Budget Total Año
+
{kpis.totalBudgetYear.toLocaleString('de-DE')}
+
12 meses acumulados
+
+
+
SKUs < 80% Forecast
+
{kpis.below80}
+
+ de {kpis.withForecastCount} con forecast período
+
+
+
+ )}
+
+ {/* ── Comparison Table ── */}
+ {isReady && (
+
+
+
+
+
+ | SKU |
+ Descripción |
+ Real Sell-in |
+
+ Forecast Período
+
+ Jan–{MONTHS_SHORT[cutoffMonth]}
+
+ |
+
+ Budget Año
+ 12 meses
+ |
+ setSortAsc(prev => !prev)}
+ >
+ % Cumpl. Período {sortAsc ? '↑' : '↓'}
+ real / forecast período
+ |
+
+ % Budget Consumido
+ real / budget año
+ |
+
+
+
+ {comparisonRows.map(row => (
+
+ ))}
+
+
+ {comparisonRows.length === 0 && (
+
+ Sin datos de comparación
+
+ )}
+
+
+ )}
+
+ );
};
export default ForecastView;