diff --git a/App.tsx b/App.tsx index 9df7ea8..b7389c7 100644 --- a/App.tsx +++ b/App.tsx @@ -90,7 +90,7 @@ const App: React.FC = () => { console.log('[App] Successfully loaded', data.length, 'rows'); // Refresh forecast too - handleForecastFetch(data, filters.customer); + handleForecastFetch(data, filters); } catch (error) { console.error("Failed to fetch/parse CSV", error); 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 { - const isUK = customerFilters.includes('Amazon UK'); + const isUK = activeFilters.customer.includes('Amazon UK'); const filename = isUK ? '/fc UK 26.xlsx' : '/fc 26.xlsx'; 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); console.log('[App] Forecast loaded:', viewData.length, 'records'); } catch (error) { @@ -181,7 +181,7 @@ const App: React.FC = () => { ...filters, 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]); @@ -260,7 +260,7 @@ const App: React.FC = () => { handleTrafficFetch(); // 1d. Fetch Forecast data - handleForecastFetch(cachedData || [], filters.customer); + handleForecastFetch(cachedData || [], filters); }; initApp(); }, [handleDataFetch]); diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index b692798..3d3230f 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1523,12 +1523,13 @@ export const processForecastExcel = async (fileOrBuffer: File | ArrayBuffer): Pr export const calculateForecastViewData = ( rawData: SalesRecord[], forecastData: ForecastRecord[], - asinMetadata: Map + asinMetadata: Map, + filters?: FilterState ): ProductForecastData[] => { const data2025 = rawData.filter(r => r.year === 2025); const data2026 = rawData.filter(r => r.year === 2026); - // Calculate Global Seasonality weights for 2025 + // Calculate Seasonality weights for 2025 const getWeights = (records: SalesRecord[]) => { const weights = new Array(12).fill(0); let total = 0; @@ -1544,7 +1545,37 @@ export const calculateForecastViewData = ( 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 const dataByAsin2025 = new Map(); @@ -1568,13 +1599,23 @@ export const calculateForecastViewData = ( const identifier = fc.asin.toUpperCase(); 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 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 forecastUnits = Math.round(fc.annualForecast * weights[idx]); + const forecastUnits = Math.round(fc.annualForecast * productWeights[idx]); const actualUnits = actuals2026.get(identifier)?.get(m) || 0; return { month: m,