diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 54eadb5..89c33a8 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1625,7 +1625,7 @@ export const calculateForecastViewData = ( const data2026 = rawData.filter(r => r.year === 2026); // Calculate Seasonality weights for 2025 - const getWeights = (records: SalesRecord[]) => { + const getWeights = (records: SalesRecord[]): number[] | null => { const weights = new Array(12).fill(0); let total = 0; const seenMonths = new Set(); @@ -1640,26 +1640,44 @@ export const calculateForecastViewData = ( } }); - // Safety Fallback: If we have sparse data (e.g., only Jan loaded), - // using it as 100% seasonality skews the forecast entirely to that month. - // We require at least 4 months of history to trust the curve; otherwise, we assume flat seasonality (1/12). - if (total === 0 || seenMonths.size < 4) { - return new Array(12).fill(1 / 12); - } + // 1. No data -> Fallback + if (total === 0) return null; + + // 2. Sparse Data Check (< 4 months) + // If an ASIN has very little history (e.g. only Jan), using its own curve implies 100% seasonality in Jan. + // The user requested to use the "General Catalog Seasonality" in these cases. + if (seenMonths.size < 4) return null; return weights.map(w => w / total); }; // 1. Determine Global/Default Weights const panEuData2025 = data2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); - const panEuWeights = getWeights(panEuData2025); + // For Global Weights, we do NOT return null on sparse data (we accept whatever we have for the whole catalog) + // We recreate a simple version of getWeights that doesn't return null for the global set + const getGlobalWeightsInner = (records: SalesRecord[]) => { + const weights = new Array(12).fill(0); + let total = 0; + records.forEach(r => { + const m = r.month.split('-')[0]; + const idx = MONTH_ORDER.indexOf(m); + if (idx !== -1) { + weights[idx] += r.units; + total += r.units; + } + }); + return total === 0 ? new Array(12).fill(1 / 12) : weights.map(w => w / total); + }; + + const panEuWeights = getGlobalWeightsInner(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); + // Use Inner helper to ensure we always get weights for global subsets + const peWeights = getGlobalWeightsInner(paEuRecords); + const ukWeights = getGlobalWeightsInner(ukRecords); // Blend: Jan-Aug from Pan-EU, Sep-Dec from UK const hybrid = new Array(12).fill(0); @@ -1711,11 +1729,25 @@ export const calculateForecastViewData = ( if (productRecords2025.length > 0) { if (isUkOnly) { - const peProd = productRecords2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); + // For UK Only, we try to be strict, but fallback to global UK weights if needed const ukProd = productRecords2025.filter(r => r.customer === 'Amazon UK'); - productWeights = getHybridWeights(peProd.length > 0 ? peProd : panEuData2025, ukProd); + // We use hybrid approach only if we have PanEU data for this product too + const peProd = productRecords2025.filter(r => PAN_EU_COUNTRIES.includes(r.customer)); + + // If we have solid data for this product, use Hybrid or UK weights + const specificWeights = getHybridWeights(peProd, ukProd); + // Wait, getHybridWeights calls getGlobalWeightsInner which never returns null. + // We need to check if specific product data is sparse. + + // Let's simplify: Check if we have enough UK history + const ukWeights = getWeights(ukProd); + if (ukWeights) { + productWeights = ukWeights; + } } else { - productWeights = getWeights(productRecords2025); + const w = getWeights(productRecords2025); + if (w) productWeights = w; + // If w is null (sparse data), productWeights remains globalWeights (Default) } }