mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:15:24 +02:00
Forecast View: Hybrid UK/Pan-EU seasonality weighting for Jan-Aug
This commit is contained in:
@@ -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]);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user