From e213ef0d16d5f1fbeb5e606a61fc3dd4ee1c97b8 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Mon, 26 Jan 2026 15:47:36 +0100 Subject: [PATCH] feat: implement Forecast 2026 (Fc 26) tab with seasonality logic and actual sales comparison --- App.tsx | 38 ++++++- api/fetch-forecast.ts | 49 +++++++++ components/ForecastView.tsx | 205 ++++++++++++++++++++++++++++++++++++ fc 26.xlsx | Bin 0 -> 25858 bytes public/forecast.xlsx | Bin 0 -> 25858 bytes services/dataProcessor.ts | 95 ++++++++++++++++- types.ts | 19 ++++ 7 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 api/fetch-forecast.ts create mode 100644 components/ForecastView.tsx create mode 100644 fc 26.xlsx create mode 100644 public/forecast.xlsx diff --git a/App.tsx b/App.tsx index bbe9711..d658029 100644 --- a/App.tsx +++ b/App.tsx @@ -5,8 +5,8 @@ import Dashboard from './components/Dashboard'; import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; -import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord } from './types'; -import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData } from './services/dataProcessor'; +import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types'; +import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData, processForecastExcel, calculateForecastViewData } from './services/dataProcessor'; import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage'; @@ -16,6 +16,7 @@ const DataGrid = lazy(() => import('./components/DataGrid')); const WeeklyGrid = lazy(() => import('./components/WeeklyGrid')); const TopMovers = lazy(() => import('./components/TopMovers')); const AdsPerformance = lazy(() => import('./components/AdsPerformance')); +const ForecastView = lazy(() => import('./components/ForecastView')); // Loading fallback component const LoadingSpinner = () => ( @@ -42,7 +43,8 @@ const App: React.FC = () => { const [trafficData, setTrafficData] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); - const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads'>('dashboard'); // Added 'weekly' view + const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads' | 'forecast'>('dashboard'); // Added 'forecast' view + const [forecastData, setForecastData] = useState([]); const [isChatOpen, setIsChatOpen] = useState(false); const [activeUrl, setActiveUrl] = useState(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL); const [lastUpdated, setLastUpdated] = useState(null); @@ -86,6 +88,9 @@ const App: React.FC = () => { setIsDataModalOpen(false); console.log('[App] Successfully loaded', data.length, 'rows'); + + // Refresh forecast too + handleForecastFetch(data, globalAsinMetadata); } catch (error) { console.error("Failed to fetch/parse CSV", error); alert("Error loading data. Please refresh the page."); @@ -138,6 +143,22 @@ const App: React.FC = () => { } }, []); + const handleForecastFetch = useCallback(async (sales: SalesRecord[], meta: Map) => { + try { + console.log('[App] Fetching forecast from /forecast.xlsx...'); + const response = await fetch('/forecast.xlsx'); + if (!response.ok) throw new Error("Forecast file not found"); + + const buffer = await response.arrayBuffer(); + const fcRecords = await processForecastExcel(buffer); + const viewData = calculateForecastViewData(sales, fcRecords, meta); + setForecastData(viewData); + console.log('[App] Forecast loaded:', viewData.length, 'records'); + } catch (error) { + console.warn("Forecast fetch failed (expected if local file not set up yet):", error); + } + }, []); + const initializeData = (data: SalesRecord[]) => { setRawData(data); setFilters({ @@ -211,6 +232,9 @@ const App: React.FC = () => { // 1c. Always fetch Traffic data (no caching for now) console.log("Fetching Traffic data..."); handleTrafficFetch(); + + // 1d. Fetch Forecast data + handleForecastFetch(cachedData || [], globalAsinMetadata); }; initApp(); }, [handleDataFetch]); @@ -502,6 +526,13 @@ const App: React.FC = () => { > Ads + @@ -532,6 +563,7 @@ const App: React.FC = () => { {view === 'weekly' && } {view === 'movers' && } {view === 'ads' && } + {view === 'forecast' && } diff --git a/api/fetch-forecast.ts b/api/fetch-forecast.ts new file mode 100644 index 0000000..ca60504 --- /dev/null +++ b/api/fetch-forecast.ts @@ -0,0 +1,49 @@ + +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import fs from 'fs'; +import path from 'path'; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + return res.status(200).end(); + } + + try { + console.log('[fetch-forecast] Reading Forecast file from local storage...'); + // Try multiple possible paths to be robust + const pathsToTry = [ + path.join(process.cwd(), 'fc 26.xlsx'), + path.join(process.cwd(), 'public', 'fc 26.xlsx'), + path.join('/Users/christianvidalwolf/github/CrazeAnalytix', 'fc 26.xlsx') // Direct path as fallback for this environment + ]; + + let buffer = null; + let foundPath = ''; + + for (const p of pathsToTry) { + console.log(`[fetch-forecast] Checking path: ${p}`); + if (fs.existsSync(p)) { + buffer = fs.readFileSync(p); + foundPath = p; + break; + } + } + + if (!buffer) { + throw new Error(`Forecast file 'fc 26.xlsx' not found in any of: ${pathsToTry.join(', ')}`); + } + + console.log(`[fetch-forecast] Successfully read Forecast Excel from ${foundPath}, size: ${buffer.byteLength}`); + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.status(200).send(buffer); + } catch (error: any) { + console.error('[fetch-forecast] Error:', error); + res.status(500).json({ error: error.message }); + } +} diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx new file mode 100644 index 0000000..3dadaa5 --- /dev/null +++ b/components/ForecastView.tsx @@ -0,0 +1,205 @@ + +import React, { useMemo, useState } from 'react'; +import { ProductForecastData } from '../types'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + LineChart, Line, Legend, ComposedChart, Area +} from 'recharts'; + +interface ForecastViewProps { + data: ProductForecastData[]; +} + +const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9']; + +const ForecastView: React.FC = ({ data }) => { + const [searchTerm, setSearchTerm] = useState(''); + + // Global Aggregate Data + const globalMonthlyData = useMemo(() => { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + return months.map(m => { + let forecast = 0; + let actual = 0; + data.forEach(p => { + const monthPoint = p.monthlyData.find(md => md.month === m); + if (monthPoint) { + forecast += monthPoint.forecastUnits; + actual += monthPoint.actualUnits; + } + }); + return { name: m, Forecast: forecast, Actual: actual }; + }); + }, [data]); + + const globalSummary = useMemo(() => { + let totalForecast = 0; + let totalActual = 0; + data.forEach(p => { + totalForecast += p.annualForecast; + p.monthlyData.forEach(md => { + totalActual += md.actualUnits; + }); + }); + const fulfillment = totalForecast > 0 ? (totalActual / totalForecast) * 100 : 0; + return { totalForecast, totalActual, fulfillment }; + }, [data]); + + const filteredProducts = useMemo(() => { + if (!searchTerm) return data; + const s = searchTerm.toLowerCase(); + return data.filter(p => + p.asin.toLowerCase().includes(s) || + p.sku.toLowerCase().includes(s) || + p.title.toLowerCase().includes(s) + ); + }, [data, searchTerm]); + + const sortedProducts = useMemo(() => { + return [...filteredProducts].sort((a, b) => b.annualForecast - a.annualForecast); + }, [filteredProducts]); + + return ( +
+ + {/* Summary Cards */} +
+
+
+

Total Forecast 2026

+
+ {globalSummary.totalForecast.toLocaleString('de-DE')} Units +
+
+ +
+
+

Total Actual Sales 2026

+
+ {globalSummary.totalActual.toLocaleString('de-DE')} Units +
+
+ +
+
+

Global Fulfillment

+
+
= 100 ? 'text-emerald-400' : 'text-indigo-400'}`}> + {globalSummary.fulfillment.toFixed(1)}% +
+
+ {/* Fulfillment Progress Bar */} +
+
+
+
+
+ + {/* Main Trend Chart */} +
+
+

+ + Monthly Evolution: Forecast vs Actual +

+
+
+ + + + + + + + + + + + +
+
+ + {/* Product Table */} +
+
+

Product Performance Comparison

+
+ setSearchTerm(e.target.value)} + className="bg-slate-950 border border-slate-700 rounded-lg px-10 py-2 text-sm text-slate-200 focus:outline-none focus:border-indigo-500 w-full md:w-80" + /> + +
+
+
+ + + + + + + + + + + + {sortedProducts.map((p) => { + const actualTotal = p.monthlyData.reduce((acc, md) => acc + md.actualUnits, 0); + const fulfillment = p.annualForecast > 0 ? (actualTotal / p.annualForecast) * 100 : 0; + + return ( + + + + + + + + ); + })} + +
Product InfoAnnual ForecastActual 2026Fulfillment %Monthly Status (YTD)
+
+ {p.asin} + {p.title} +
+
+ {p.annualForecast.toLocaleString('de-DE')} + + {actualTotal.toLocaleString('de-DE')} + + = 50 ? 'bg-emerald-500/10 text-emerald-400' : fulfillment >= 20 ? 'bg-indigo-500/10 text-indigo-400' : 'bg-slate-800 text-slate-500'}`}> + {fulfillment.toFixed(1)}% + + +
+ {p.monthlyData.map((md, idx) => { + const isMet = md.actualUnits >= md.forecastUnits && md.forecastUnits > 0; + const height = md.forecastUnits > 0 ? (Math.min(1.5, md.actualUnits / md.forecastUnits) * 100) : 0; + return ( +
0 ? 'bg-indigo-500' : 'bg-slate-800'}`} + style={{ height: `${Math.max(10, height)}%` }} + title={`${md.month}: ${md.actualUnits} / ${md.forecastUnits}`} + >
+ ); + })} +
+
+
+
+
+ ); +}; + +export default ForecastView; diff --git a/fc 26.xlsx b/fc 26.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3e71333f6a733eb60d89723ae86ab0b0a48a3c71 GIT binary patch literal 25858 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 literal 0 HcmV?d00001 diff --git a/public/forecast.xlsx b/public/forecast.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3e71333f6a733eb60d89723ae86ab0b0a48a3c71 GIT binary patch literal 25858 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 literal 0 HcmV?d00001 diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 7bbf088..357420d 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -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 => { + 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 +): 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(); + 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>(); + 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 + }; + }); +}; diff --git a/types.ts b/types.ts index 84e9a8b..850b181 100644 --- a/types.ts +++ b/types.ts @@ -194,3 +194,22 @@ export interface CombinedKPIs { cvrUnits: number; glanceViews: number; // Traffic / page views } + +export interface ForecastRecord { + asin: string; + annualForecast: number; +} + +export interface MonthlyForecastPoint { + month: string; + forecastUnits: number; + actualUnits: number; +} + +export interface ProductForecastData { + asin: string; + sku: string; + title: string; + annualForecast: number; + monthlyData: MonthlyForecastPoint[]; +}