mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:45:24 +02:00
Fix Weeks of Coverage: use unfiltered velocity map for robust calculation
This commit is contained in:
@@ -5,8 +5,8 @@ import Dashboard from './components/Dashboard';
|
|||||||
import FilterBar from './components/FilterBar';
|
import FilterBar from './components/FilterBar';
|
||||||
import AIChat from './components/AIChat';
|
import AIChat from './components/AIChat';
|
||||||
import CrazeLogo from './components/CrazeLogo';
|
import CrazeLogo from './components/CrazeLogo';
|
||||||
|
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, calculateVelocityMap, getUniqueValues, processForecastExcel } from './services/dataProcessor';
|
||||||
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
|
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
|
||||||
import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData, processForecastExcel, calculateForecastViewData, processStockExcel, processVendorStockExcel } from './services/dataProcessor';
|
|
||||||
import { queryGemini } from './services/geminiService';
|
import { queryGemini } from './services/geminiService';
|
||||||
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
||||||
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
|
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
|
||||||
@@ -198,13 +198,13 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const viewData = calculateForecastViewData(sales, fcRecords, meta, activeFilters);
|
const viewData = calculateForecastViewData(sales, fcRecords, meta, activeFilters, velocityMap);
|
||||||
setForecastData(viewData);
|
setForecastData(viewData);
|
||||||
console.log('[App] Forecast loaded:', viewData.length, 'records');
|
console.log('[App] Forecast loaded:', viewData.length, 'records');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Forecast fetch failed:", error);
|
console.warn("Forecast fetch failed:", error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [velocityMap]); // Added velocityMap to dependencies
|
||||||
|
|
||||||
// Update forecast when filters change
|
// Update forecast when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -450,10 +450,26 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [rawData]);
|
}, [rawData]);
|
||||||
|
|
||||||
|
// Calculate 4-week Sales Velocity Map (Context-Aware)
|
||||||
|
const velocityMap = useMemo(() => {
|
||||||
|
// Determine which dataset to use for velocity calculation based on top50Mode (which reflects region filter)
|
||||||
|
// If 'uk', use UK data. If 'eu', use EU data.
|
||||||
|
// IMPOTANT: We do NOT filter by Week/Month here, so we get the full history for velocity calculation
|
||||||
|
const regionData = rawData.filter(r => {
|
||||||
|
if (top50Mode === 'uk') return r.customer.toLowerCase().includes('uk');
|
||||||
|
// for EU, exclude UK
|
||||||
|
return !r.customer.toLowerCase().includes('uk');
|
||||||
|
});
|
||||||
|
return calculateVelocityMap(regionData);
|
||||||
|
}, [rawData, top50Mode]);
|
||||||
|
|
||||||
// Combine Sales & Ads Data dynamically based on current filters
|
// Combine Sales & Ads Data dynamically based on current filters
|
||||||
const combinedAdsData = useMemo(() => {
|
const combinedAdsData = useMemo(() => {
|
||||||
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData);
|
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap);
|
||||||
}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData]);
|
}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]);
|
||||||
|
|
||||||
|
// Derived Data for Views
|
||||||
|
const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]);
|
||||||
|
|
||||||
// Derive Context Data (Product Line Context when drilling down)
|
// Derive Context Data (Product Line Context when drilling down)
|
||||||
const contextAggregatedData = useMemo(() => {
|
const contextAggregatedData = useMemo(() => {
|
||||||
|
|||||||
+45
-56
@@ -505,7 +505,8 @@ export const mergeSalesAndAdsData = (
|
|||||||
salesData: SalesRecord[],
|
salesData: SalesRecord[],
|
||||||
adsData: AdsRecord[],
|
adsData: AdsRecord[],
|
||||||
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>,
|
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>,
|
||||||
trafficData?: TrafficRecord[]
|
trafficData?: TrafficRecord[],
|
||||||
|
velocityMap?: Map<string, number>
|
||||||
): CombinedKPIs[] => {
|
): CombinedKPIs[] => {
|
||||||
// Key for both sales and ads: ASIN|Customer|Year|Week
|
// Key for both sales and ads: ASIN|Customer|Year|Week
|
||||||
const createKey = (asin: string, customer: string, year: number, week: number) =>
|
const createKey = (asin: string, customer: string, year: number, week: number) =>
|
||||||
@@ -598,35 +599,10 @@ export const mergeSalesAndAdsData = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Calculate 4-Week Average Sales per ASIN
|
|
||||||
const latestYear = Math.max(...salesData.map(r => r.year).filter(y => y > 0));
|
|
||||||
const yearData = salesData.filter(r => r.year === latestYear);
|
|
||||||
const latestWeek = yearData.length > 0 ? Math.max(...yearData.map(r => r.week).filter(w => w !== undefined) as number[]) : 0;
|
|
||||||
|
|
||||||
const last4WeeksKeys = new Set<string>();
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
let w = latestWeek - i;
|
|
||||||
let y = latestYear;
|
|
||||||
if (w <= 0) {
|
|
||||||
w = 52 + w;
|
|
||||||
y = latestYear - 1;
|
|
||||||
}
|
|
||||||
last4WeeksKeys.add(`${y}|${w}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const asin4WeekSales = new Map<string, number>();
|
|
||||||
salesData.forEach(r => {
|
|
||||||
if (r.week === undefined) return;
|
|
||||||
if (last4WeeksKeys.has(`${r.year}|${r.week}`)) {
|
|
||||||
const key = r.asin.trim().toUpperCase();
|
|
||||||
asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + r.units);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergedData: CombinedKPIs[] = [];
|
const mergedData: CombinedKPIs[] = [];
|
||||||
const processedKeys = new Set<string>();
|
const processedKeys = new Set<string>();
|
||||||
|
|
||||||
// 5. Create ONE record per ASIN/Customer/Year/Week from sales
|
// 4. Create ONE record per ASIN/Customer/Year/Week from sales
|
||||||
salesMap.forEach((sale, key) => {
|
salesMap.forEach((sale, key) => {
|
||||||
processedKeys.add(key);
|
processedKeys.add(key);
|
||||||
const ad = adsMap.get(key);
|
const ad = adsMap.get(key);
|
||||||
@@ -648,7 +624,7 @@ export const mergeSalesAndAdsData = (
|
|||||||
const ctr = adImpressions > 0 ? (adClicks / adImpressions) * 100 : 0;
|
const ctr = adImpressions > 0 ? (adClicks / adImpressions) * 100 : 0;
|
||||||
const cpc = adClicks > 0 ? adCost / adClicks : 0;
|
const cpc = adClicks > 0 ? adCost / adClicks : 0;
|
||||||
const cvrUnits = adClicks > 0 ? (adUnits / adClicks) * 100 : 0;
|
const cvrUnits = adClicks > 0 ? (adUnits / adClicks) * 100 : 0;
|
||||||
const avgWeeklySales = (asin4WeekSales.get(sale.asin.trim().toUpperCase()) || 0) / 4;
|
const avgWeeklySales = velocityMap?.get(sale.asin.trim().toUpperCase()) || 0;
|
||||||
|
|
||||||
mergedData.push({
|
mergedData.push({
|
||||||
id: `merged-${key}`,
|
id: `merged-${key}`,
|
||||||
@@ -689,7 +665,7 @@ export const mergeSalesAndAdsData = (
|
|||||||
if (!processedKeys.has(key)) {
|
if (!processedKeys.has(key)) {
|
||||||
const asin = ad.asin.trim().toUpperCase();
|
const asin = ad.asin.trim().toUpperCase();
|
||||||
const meta = asinMetadata.get(asin);
|
const meta = asinMetadata.get(asin);
|
||||||
const avgWeeklySales = (asin4WeekSales.get(asin) || 0) / 4;
|
const avgWeeklySales = velocityMap?.get(asin) || 0;
|
||||||
|
|
||||||
mergedData.push({
|
mergedData.push({
|
||||||
id: `ads-only-${key}`,
|
id: `ads-only-${key}`,
|
||||||
@@ -1642,7 +1618,8 @@ 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
|
filters?: FilterState,
|
||||||
|
velocityMap?: Map<string, number>
|
||||||
): 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);
|
||||||
@@ -1713,35 +1690,10 @@ export const calculateForecastViewData = (
|
|||||||
monthMap.set(m, (monthMap.get(m) || 0) + r.units);
|
monthMap.set(m, (monthMap.get(m) || 0) + r.units);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4-Week Average Sales Calculation
|
|
||||||
const latestYear = Math.max(...rawData.map(r => r.year).filter(y => y > 0));
|
|
||||||
const yearData = rawData.filter(r => r.year === latestYear);
|
|
||||||
const latestWeek = yearData.length > 0 ? Math.max(...yearData.map(r => r.week).filter(w => w !== undefined) as number[]) : 0;
|
|
||||||
|
|
||||||
const last4WeeksKeys = new Set<string>();
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
let w = latestWeek - i;
|
|
||||||
let y = latestYear;
|
|
||||||
if (w <= 0) {
|
|
||||||
w = 52 + w;
|
|
||||||
y = latestYear - 1;
|
|
||||||
}
|
|
||||||
last4WeeksKeys.add(`${y}|${w}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const asin4WeekSales = new Map<string, number>();
|
|
||||||
rawData.forEach(r => {
|
|
||||||
if (r.week === undefined) return;
|
|
||||||
if (last4WeeksKeys.has(`${r.year}|${r.week}`)) {
|
|
||||||
const key = r.asin.trim().toUpperCase();
|
|
||||||
asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + r.units);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return forecastData.map(fc => {
|
return forecastData.map(fc => {
|
||||||
const identifier = fc.asin.toUpperCase();
|
const identifier = fc.asin.toUpperCase();
|
||||||
const meta = asinMetadata.get(identifier);
|
const meta = asinMetadata.get(identifier);
|
||||||
const avgWeeklySales = (asin4WeekSales.get(identifier) || 0) / 4;
|
const avgWeeklySales = velocityMap?.get(identifier) || 0;
|
||||||
|
|
||||||
// 2. Determine weights for this ASIN
|
// 2. Determine weights for this ASIN
|
||||||
const productRecords2025 = dataByAsin2025.get(identifier) || [];
|
const productRecords2025 = dataByAsin2025.get(identifier) || [];
|
||||||
@@ -1858,6 +1810,43 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const calculateVelocityMap = (data: SalesRecord[]): Map<string, number> => {
|
||||||
|
// 4-Week Average Sales Calculation
|
||||||
|
const validYears = data.map(r => r.year).filter(y => y > 0);
|
||||||
|
if (validYears.length === 0) return new Map();
|
||||||
|
|
||||||
|
const latestYear = Math.max(...validYears);
|
||||||
|
const yearData = data.filter(r => r.year === latestYear);
|
||||||
|
const latestWeek = yearData.length > 0 ? Math.max(...yearData.map(r => r.week).filter(w => w !== undefined) as number[]) : 0;
|
||||||
|
|
||||||
|
const last4WeeksKeys = new Set<string>();
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
let w = latestWeek - i;
|
||||||
|
let y = latestYear;
|
||||||
|
if (w <= 0) {
|
||||||
|
w = 52 + w;
|
||||||
|
y = latestYear - 1;
|
||||||
|
}
|
||||||
|
last4WeeksKeys.add(`${y}|${w}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const asin4WeekSales = new Map<string, number>();
|
||||||
|
data.forEach(r => {
|
||||||
|
if (r.week === undefined) return;
|
||||||
|
if (last4WeeksKeys.has(`${r.year}|${r.week}`)) {
|
||||||
|
const key = r.asin.trim().toUpperCase();
|
||||||
|
asin4WeekSales.set(key, (asin4WeekSales.get(key) || 0) + r.units);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const velocityMap = new Map<string, number>();
|
||||||
|
asin4WeekSales.forEach((total, asin) => {
|
||||||
|
velocityMap.set(asin, total / 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
return velocityMap;
|
||||||
|
};
|
||||||
|
|
||||||
export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, number>> => {
|
export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, number>> => {
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = fileOrBuffer instanceof File
|
const arrayBuffer = fileOrBuffer instanceof File
|
||||||
|
|||||||
Reference in New Issue
Block a user