Forecast View: Hybrid UK/Pan-EU seasonality weighting for Jan-Aug

This commit is contained in:
Christian Vidal Wolf
2026-01-27 09:49:35 +01:00
parent d496073e88
commit f8c6ecdf29
2 changed files with 54 additions and 13 deletions
+6 -6
View File
@@ -90,7 +90,7 @@ const App: React.FC = () => {
console.log('[App] Successfully loaded', data.length, 'rows'); console.log('[App] Successfully loaded', data.length, 'rows');
// Refresh forecast too // Refresh forecast too
handleForecastFetch(data, filters.customer); handleForecastFetch(data, filters);
} catch (error) { } catch (error) {
console.error("Failed to fetch/parse CSV", error); console.error("Failed to fetch/parse CSV", error);
alert("Error loading data. Please refresh the page."); alert("Error loading data. Please refresh the page.");
@@ -143,9 +143,9 @@ const App: React.FC = () => {
} }
}, []); }, []);
const handleForecastFetch = useCallback(async (sales: SalesRecord[], customerFilters: string[] = []) => { const handleForecastFetch = useCallback(async (sales: SalesRecord[], activeFilters: FilterState) => {
try { try {
const isUK = customerFilters.includes('Amazon UK'); const isUK = activeFilters.customer.includes('Amazon UK');
const filename = isUK ? '/fc UK 26.xlsx' : '/fc 26.xlsx'; const filename = isUK ? '/fc UK 26.xlsx' : '/fc 26.xlsx';
console.log(`[App] Fetching forecast from ${filename}...`); console.log(`[App] Fetching forecast from ${filename}...`);
@@ -165,7 +165,7 @@ const App: React.FC = () => {
} }
}); });
const viewData = calculateForecastViewData(sales, fcRecords, meta); const viewData = calculateForecastViewData(sales, fcRecords, meta, activeFilters);
setForecastData(viewData); setForecastData(viewData);
console.log('[App] Forecast loaded:', viewData.length, 'records'); console.log('[App] Forecast loaded:', viewData.length, 'records');
} catch (error) { } catch (error) {
@@ -181,7 +181,7 @@ const App: React.FC = () => {
...filters, ...filters,
year: [] // Ensure we don't filter out 2025/2026 if a single year is selected in UI year: [] // Ensure we don't filter out 2025/2026 if a single year is selected in UI
}); });
handleForecastFetch(forecastRelevantData, filters.customer); handleForecastFetch(forecastRelevantData, filters);
} }
}, [filters, rawData, handleForecastFetch]); }, [filters, rawData, handleForecastFetch]);
@@ -260,7 +260,7 @@ const App: React.FC = () => {
handleTrafficFetch(); handleTrafficFetch();
// 1d. Fetch Forecast data // 1d. Fetch Forecast data
handleForecastFetch(cachedData || [], filters.customer); handleForecastFetch(cachedData || [], filters);
}; };
initApp(); initApp();
}, [handleDataFetch]); }, [handleDataFetch]);
+48 -7
View File
@@ -1523,12 +1523,13 @@ export const processForecastExcel = async (fileOrBuffer: File | ArrayBuffer): Pr
export const calculateForecastViewData = ( export const calculateForecastViewData = (
rawData: SalesRecord[], rawData: SalesRecord[],
forecastData: ForecastRecord[], forecastData: ForecastRecord[],
asinMetadata: Map<string, { sku: string; title: string; line: string }> asinMetadata: Map<string, { sku: string; title: string; line: string }>,
filters?: FilterState
): ProductForecastData[] => { ): ProductForecastData[] => {
const data2025 = rawData.filter(r => r.year === 2025); const data2025 = rawData.filter(r => r.year === 2025);
const data2026 = rawData.filter(r => r.year === 2026); const data2026 = rawData.filter(r => r.year === 2026);
// Calculate Global Seasonality weights for 2025 // Calculate Seasonality weights for 2025
const getWeights = (records: SalesRecord[]) => { const getWeights = (records: SalesRecord[]) => {
const weights = new Array(12).fill(0); const weights = new Array(12).fill(0);
let total = 0; let total = 0;
@@ -1544,7 +1545,37 @@ export const calculateForecastViewData = (
return weights.map(w => w / total); return weights.map(w => w / total);
}; };
const globalWeights = getWeights(data2025); // 1. Determine Global/Default Weights
const panEuData2025 = data2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer));
const panEuWeights = getWeights(panEuData2025);
// Check if we are in UK-only mode
const isUkOnly = filters?.customer?.includes('Amazon UK') && filters.customer.length === 1;
const getHybridWeights = (paEuRecords: SalesRecord[], ukRecords: SalesRecord[]) => {
const peWeights = getWeights(paEuRecords);
const ukWeights = getWeights(ukRecords);
// Blend: Jan-Aug from Pan-EU, Sep-Dec from UK
const hybrid = new Array(12).fill(0);
const hasUkHistory = ukRecords.length > 0;
for (let i = 0; i < 12; i++) {
if (i < 8) { // Jan-Aug
hybrid[i] = peWeights[i];
} else { // Sep-Dec
hybrid[i] = hasUkHistory ? ukWeights[i] : peWeights[i];
}
}
// Normalize
const sum = hybrid.reduce((a, b) => a + b, 0);
return sum > 0 ? hybrid.map(w => w / sum) : peWeights;
};
const globalWeights = isUkOnly
? getHybridWeights(panEuData2025, data2025.filter(r => r.customer === 'Amazon UK'))
: panEuWeights;
// Map 2025 data by ASIN for quick access // Map 2025 data by ASIN for quick access
const dataByAsin2025 = new Map<string, SalesRecord[]>(); const dataByAsin2025 = new Map<string, SalesRecord[]>();
@@ -1568,13 +1599,23 @@ export const calculateForecastViewData = (
const identifier = fc.asin.toUpperCase(); const identifier = fc.asin.toUpperCase();
const meta = asinMetadata.get(identifier); const meta = asinMetadata.get(identifier);
// 1. Determine weights (Product specific or global backup) // 2. Determine weights for this ASIN
const productRecords2025 = dataByAsin2025.get(identifier) || []; const productRecords2025 = dataByAsin2025.get(identifier) || [];
const weights = productRecords2025.length > 0 ? getWeights(productRecords2025) : globalWeights; let productWeights = globalWeights;
// 2. Build monthly points if (productRecords2025.length > 0) {
if (isUkOnly) {
const peProd = productRecords2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer));
const ukProd = productRecords2025.filter(r => r.customer === 'Amazon UK');
productWeights = getHybridWeights(peProd.length > 0 ? peProd : panEuData2025, ukProd);
} else {
productWeights = getWeights(productRecords2025);
}
}
// 3. Build monthly points
const monthlyData: MonthlyForecastPoint[] = MONTH_ORDER.map((m, idx) => { const monthlyData: MonthlyForecastPoint[] = MONTH_ORDER.map((m, idx) => {
const forecastUnits = Math.round(fc.annualForecast * weights[idx]); const forecastUnits = Math.round(fc.annualForecast * productWeights[idx]);
const actualUnits = actuals2026.get(identifier)?.get(m) || 0; const actualUnits = actuals2026.get(identifier)?.get(m) || 0;
return { return {
month: m, month: m,