mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:05:24 +02:00
feat: implement Forecast 2026 (Fc 26) tab with seasonality logic and actual sales comparison
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { SalesRecord, AdsRecord, TrafficRecord, CombinedKPIs, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint } from '../types';
|
||||
import { SalesRecord, AdsRecord, TrafficRecord, CombinedKPIs, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint, ForecastRecord, MonthlyForecastPoint, ProductForecastData } from '../types';
|
||||
import * as XLSX from 'xlsx';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
@@ -1496,3 +1496,96 @@ export const pivotWeeklySalesData = (data: CombinedKPIs[]): {
|
||||
weeks: sortedWeeks
|
||||
};
|
||||
};
|
||||
|
||||
export const processForecastExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<ForecastRecord[]> => {
|
||||
try {
|
||||
const arrayBuffer = fileOrBuffer instanceof File
|
||||
? await fileOrBuffer.arrayBuffer()
|
||||
: fileOrBuffer;
|
||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
|
||||
|
||||
return jsonData.map(row => ({
|
||||
asin: String(row['ASIN'] || row['asin'] || '').trim().toUpperCase(),
|
||||
annualForecast: parseUnits(String(row['Forecast 2026'] || row['forecast 2026'] || '0'))
|
||||
})).filter(r => r.asin && r.annualForecast > 0);
|
||||
} catch (error) {
|
||||
console.error("Error processing Forecast Excel:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateForecastViewData = (
|
||||
rawData: SalesRecord[],
|
||||
forecastData: ForecastRecord[],
|
||||
asinMetadata: Map<string, { sku: string; title: string; line: string }>
|
||||
): ProductForecastData[] => {
|
||||
const data2025 = rawData.filter(r => r.year === 2025);
|
||||
const data2026 = rawData.filter(r => r.year === 2026);
|
||||
|
||||
// Calculate Global Seasonality weights for 2025
|
||||
const getWeights = (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;
|
||||
}
|
||||
});
|
||||
if (total === 0) return new Array(12).fill(1 / 12);
|
||||
return weights.map(w => w / total);
|
||||
};
|
||||
|
||||
const globalWeights = getWeights(data2025);
|
||||
|
||||
// Map 2025 data by ASIN for quick access
|
||||
const dataByAsin2025 = new Map<string, SalesRecord[]>();
|
||||
data2025.forEach(r => {
|
||||
const key = r.asin.trim().toUpperCase();
|
||||
if (!dataByAsin2025.has(key)) dataByAsin2025.set(key, []);
|
||||
dataByAsin2025.get(key)!.push(r);
|
||||
});
|
||||
|
||||
// Map 2026 actual sales by ASIN and Month
|
||||
const actuals2026 = new Map<string, Map<string, number>>();
|
||||
data2026.forEach(r => {
|
||||
const key = r.asin.trim().toUpperCase();
|
||||
const m = r.month.split('-')[0];
|
||||
if (!actuals2026.has(key)) actuals2026.set(key, new Map());
|
||||
const monthMap = actuals2026.get(key)!;
|
||||
monthMap.set(m, (monthMap.get(m) || 0) + r.units);
|
||||
});
|
||||
|
||||
return forecastData.map(fc => {
|
||||
const identifier = fc.asin.toUpperCase();
|
||||
const meta = asinMetadata.get(identifier);
|
||||
|
||||
// 1. Determine weights (Product specific or global backup)
|
||||
const productRecords2025 = dataByAsin2025.get(identifier) || [];
|
||||
const weights = productRecords2025.length > 0 ? getWeights(productRecords2025) : globalWeights;
|
||||
|
||||
// 2. Build monthly points
|
||||
const monthlyData: MonthlyForecastPoint[] = MONTH_ORDER.map((m, idx) => {
|
||||
const forecastUnits = Math.round(fc.annualForecast * weights[idx]);
|
||||
const actualUnits = actuals2026.get(identifier)?.get(m) || 0;
|
||||
return {
|
||||
month: m,
|
||||
forecastUnits,
|
||||
actualUnits
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
asin: identifier,
|
||||
sku: meta?.sku || identifier, // Fallback to ASIN if SKU not found
|
||||
title: meta?.title || identifier,
|
||||
annualForecast: fc.annualForecast,
|
||||
monthlyData
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user