import React, { useState, useMemo, useEffect, useCallback, Suspense, lazy } from 'react'; import FileUpload from './components/FileUpload'; import Dashboard from './components/Dashboard'; import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel } from './services/dataProcessor'; import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types'; 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'; // Lazy load heavy components for better initial performance 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 = () => (
); // New Refresh Icon const RefreshIcon = ({ className }: { className?: string }) => ( ); // Hardcoded Permanent URL for Auto-Loading // Using the original share link to leverage Dropbox's redirect for robust fetching. const PERMANENT_DROPBOX_URL = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=pzn1zkrg&dl=0"; const PERMANENT_ADS_URL = "https://www.dropbox.com/scl/fi/wng8tep7awhvzd65amwad/Ads-Weekly.xlsx?rlkey=kcmoq8dxgibsb2eb8zz47xvyo&st=z02m4w3g&dl=0"; const App: React.FC = () => { const [rawData, setRawData] = useState([]); const [adsData, setAdsData] = useState([]); const [trafficData, setTrafficData] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); 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); const [stockMap, setStockMap] = useState>(new Map()); const [vendorStockMap, setVendorStockMap] = useState>(new Map()); const [cachedForecastRecords, setCachedForecastRecords] = useState([]); const [lastForecastFile, setLastForecastFile] = useState(null); const [buyBoxLostMap, setBuyBoxLostMap] = useState }>>(new Map()); // Modal State const [isDataModalOpen, setIsDataModalOpen] = useState(false); const [filters, setFilters] = useState({ customer: [], year: [], month: [], line: [], asin: [], sku: [], title: [], week: [], stock: [], vendorStock: [], woc: [], }); const top50Mode = useMemo(() => { const hasUK = filters.customer.includes('Amazon UK'); const hasEU = filters.customer.some(c => ['Amazon DE', 'Amazon IT', 'Amazon FR', 'Amazon ES'].includes(c)); if (hasUK) return 'uk'; return 'eu'; }, [filters.customer]); // Calculate 4-week Sales Velocity Map (Context-Aware) // MOVED HERE TO AVOID REFERENCE ERROR in handleForecastFetch const velocityMap = useMemo(() => { // Determine which dataset to use for velocity calculation based on 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]); // Handle Data Fetch (Simplified) const handleDataFetch = useCallback(async () => { setSyncing(true); try { console.log('[App] Fetching data from /api/fetch-data...'); const response = await fetch('/api/fetch-data'); if (!response.ok) { throw new Error(`Failed to fetch CSV: ${response.status} ${response.statusText}`); } const csvText = await response.text(); const data = await processCSV(csvText); await saveSalesData(data); initializeData(data); setActiveUrl(PERMANENT_DROPBOX_URL); const now = new Date().toISOString(); setLastUpdated(now); localStorage.setItem('craze_last_updated', now); localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL); setIsDataModalOpen(false); console.log('[App] Successfully loaded', data.length, 'rows'); // Refresh forecast too handleForecastFetch(data, data, filters); } catch (error) { console.error("Failed to fetch/parse CSV", error); alert("Error loading data. Please refresh the page."); throw error; } finally { setSyncing(false); setLoading(false); } }, []); const handleAdsFetch = useCallback(async () => { setSyncing(true); try { console.log('[App] Fetching ads from /api/fetch-ads...'); const response = await fetch('/api/fetch-ads'); if (!response.ok) { throw new Error(`Failed to fetch Ads: ${response.status} ${response.statusText}`); } const buffer = await response.arrayBuffer(); const data = await processAdsExcel(buffer); await saveAdsData(data); setAdsData(data); console.log('[App] Successfully loaded', data.length, 'ads records'); } catch (error) { console.error("Failed to fetch/parse Ads", error); } finally { setSyncing(false); } }, []); const handleTrafficFetch = useCallback(async () => { try { console.log('[App] Fetching traffic from /api/fetch-traffic...'); const response = await fetch('/api/fetch-traffic'); if (!response.ok) throw new Error(`Failed to fetch traffic: ${response.status}`); const buffer = await response.arrayBuffer(); const data = await processTrafficExcel(buffer); setTrafficData(data); console.log('[App] Successfully loaded', data.length, 'traffic records'); } catch (error) { console.error("Failed to fetch/parse Traffic", error); } }, []); const handleStockFetch = useCallback(async () => { try { console.log('[App] Fetching stock from /Item Availability.xlsx...'); const response = await fetch('/Item Availability.xlsx'); if (!response.ok) throw new Error(`Failed to fetch stock: ${response.status}`); const buffer = await response.arrayBuffer(); const data = await processStockExcel(buffer); setStockMap(data); console.log('[App] Successfully loaded stock for', data.size, 'normalized SKUs'); } catch (error) { console.error("Failed to fetch/parse Stock", error); } }, []); const handleVendorStockFetch = useCallback(async () => { try { console.log('[App] Fetching vendor stock from /Vendor Stock.xlsx...'); const response = await fetch('/Vendor Stock.xlsx'); if (!response.ok) throw new Error(`Failed to fetch vendor stock: ${response.status}`); const buffer = await response.arrayBuffer(); const data = await processVendorStockExcel(buffer); setVendorStockMap(data); console.log('[App] Successfully loaded vendor stock for', data.size, 'ASINs'); } catch (error) { console.error("Failed to fetch/parse Vendor Stock", error); } }, []); const handleBuyBoxFetch = useCallback(async () => { try { console.log('[App] Fetching Buy Box data from /Buy_Box_tracker.xlsx...'); const response = await fetch('/Buy_Box_tracker.xlsx'); if (!response.ok) throw new Error(`Failed to fetch Buy Box data: ${response.status}`); const buffer = await response.arrayBuffer(); const data = await processBuyBoxExcel(buffer); setBuyBoxLostMap(data); console.log('[App] Successfully loaded Buy Box lost data for', data.size, 'ASINs'); } catch (error) { console.error("Failed to fetch/parse Buy Box data", error); } }, []); const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => { try { const isUK = activeFilters.customer.includes('Amazon UK'); const filename = isUK ? '/fc UK 26.xlsx' : '/fc 26.xlsx'; let fcRecords = cachedForecastRecords; if (lastForecastFile !== filename || cachedForecastRecords.length === 0) { console.log(`[App] Fetching fresh forecast from ${filename}...`); const response = await fetch(filename); if (!response.ok) throw new Error(`Forecast file ${filename} not found`); const buffer = await response.arrayBuffer(); fcRecords = await processForecastExcel(buffer); setCachedForecastRecords(fcRecords); setLastForecastFile(filename); } else { console.log(`[App] Using cached forecast for ${filename}`); } // Build precise metadata map from GLOBAL sales records to ensure info is found even if filtered for market const meta = new Map(); const metaDataSource = globalSales.length > 0 ? globalSales : sales; metaDataSource.forEach(r => { const asin = r.asin.trim().toUpperCase(); const existing = meta.get(asin); if (!existing || (r.title && r.title.length > (existing.title?.length || 0))) { meta.set(asin, { sku: r.sku, title: r.title, line: r.line }); } }); const viewData = calculateForecastViewData(sales, fcRecords, meta, activeFilters, velocityMap); setForecastData(viewData); console.log('[App] Forecast calculation complete:', viewData.length, 'records'); } catch (error) { console.warn("Forecast fetch failed:", error); } }, [velocityMap, cachedForecastRecords, lastForecastFile]); // Update forecast when filters change useEffect(() => { if (rawData.length > 0) { // Filter rawData to respect customer selections but keep all years (2025 for seasonality, 2026 for actuals) const forecastRelevantData = filterData(rawData, { ...filters, year: [], // Ensure we don't filter out 2025/2026 if a single year is selected in UI month: [] // EXCLUDE month filtering to preserve seasonality calculation in calculateForecastViewData }); handleForecastFetch(forecastRelevantData, rawData, filters); } }, [filters, rawData, handleForecastFetch]); const initializeData = (data: SalesRecord[]) => { setRawData(data); setFilters({ customer: [], year: [], month: [], line: [], asin: [], sku: [], title: [], week: [], stock: [], vendorStock: [], woc: [], }); }; const handleSkuDrillDown = useCallback((sku: string) => { if (!sku) return; setFilters(prev => ({ ...prev, sku: [sku] })); setView('ads'); }, []); // 1. Initial Load from Cache (IndexedDB) or Auto-Fetch Permanent URL useEffect(() => { const initApp = async () => { setLoading(true); const currentStoredUrl = localStorage.getItem('craze_csv_url'); let shouldUseCache = true; // Force refresh if URL has changed if (currentStoredUrl !== PERMANENT_DROPBOX_URL) { console.log("URL changed, forcing refresh..."); localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL); setActiveUrl(PERMANENT_DROPBOX_URL); shouldUseCache = false; } // Try to load from cache first IF the URL hasn't changed let cachedData = null; let cachedDate = null; if (shouldUseCache) { const result = await loadSalesData(); cachedData = result.data; cachedDate = result.lastUpdated; } if (cachedData && cachedData.length > 0) { console.log("Loaded data from cache:", cachedData.length, "rows"); initializeData(cachedData); setLastUpdated(cachedDate); setLoading(false); // Trigger background sync to ensure data is fresh handleDataFetch().catch(e => console.warn("Background sales sync failed", e)); } else { console.log("Fetching fresh data from Permanent URL..."); handleDataFetch().catch(e => { console.error("Initial fetch failed."); }); } // 1b. Load Ads from cache or auto-fetch const { data: cachedAds, lastUpdated: adsLastUpdated } = await loadAdsData(); if (cachedAds && cachedAds.length > 0) { console.log("Loaded ads from cache:", cachedAds.length, "records"); setAdsData(cachedAds); // Trigger background sync handleAdsFetch(); } else { console.log("Fetching fresh Ads from URL..."); handleAdsFetch(); } // 1c. Always fetch Traffic data (no caching for now) console.log("Fetching Traffic data..."); handleTrafficFetch(); // 1d. Always fetch Stock data console.log("Fetching Stock data..."); handleStockFetch(); handleVendorStockFetch(); handleBuyBoxFetch(); // 1e. Fetch Forecast data handleForecastFetch(cachedData || [], cachedData || [], filters); }; initApp(); }, [handleDataFetch]); // Handle uploaded Sales file (Manual) const handleSalesUpload = async (file: File) => { setSyncing(true); try { const data = await processCSV(file); await saveSalesData(data); initializeData(data); setLastUpdated(new Date().toISOString()); setIsDataModalOpen(false); // Also trigger ads fetch if not loaded to maintain sync if (adsData.length === 0) handleAdsFetch(); } catch (error) { console.error("Failed to parse CSV", error); alert("Error parsing CSV. Please check the format."); } finally { setSyncing(false); } }; // Handle uploaded Ads file (Manual) - Supports both CSV and Excel const handleAdsUpload = async (file: File) => { setSyncing(true); try { const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls'); const data = isExcel ? await processAdsExcel(file) : await processAdsCSV(file); setAdsData(data); await saveAdsData(data); console.log("Ads loaded:", data.length, "records from", file.name); setIsDataModalOpen(false); } catch (error: any) { console.error("Failed to parse Ads file", error); alert(`Error parsing Ads file: ${error.message || 'Unknown error'}. Please check the format.`); } finally { setSyncing(false); } }; // Handle uploaded Traffic file (Manual) const handleTrafficUpload = async (file: File) => { setSyncing(true); try { const data = await processTrafficExcel(file); setTrafficData(data); console.log("Traffic loaded:", data.length, "records from", file.name); setIsDataModalOpen(false); } catch (error: any) { console.error("Failed to parse Traffic file", error); alert(`Error parsing Traffic file: ${error.message || 'Unknown error'}. Please check the format.`); } finally { setSyncing(false); } }; // 2. Schedule Auto-Refresh (Background) useEffect(() => { const checkAndRefresh = () => { const now = new Date(); const today = now.toISOString().split('T')[0]; // YYYY-MM-DD const lastRefreshDate = localStorage.getItem('craze_last_refresh_date'); // Refresh if it's after 7 AM and we haven't refreshed today if (now.getHours() >= 7 && lastRefreshDate !== today) { console.log("Triggering daily data refresh..."); handleDataFetch().then(() => { localStorage.setItem('craze_last_refresh_date', today); console.log("Daily refresh successful."); // Also refresh ads handleAdsFetch(); }).catch(err => { console.error("Daily refresh failed, will retry later.", err); }); } }; // Check immediately on load in case the user opens the app after 7 AM checkAndRefresh(); // And then check periodically (e.g., every 15 minutes) in case app is left open across midnight const interval = setInterval(checkAndRefresh, 15 * 60 * 1000); return () => clearInterval(interval); }, [handleDataFetch]); // Derive Data const globalAsinMetadata = useMemo(() => { const metaMap = new Map(); rawData.forEach(r => { const asin = r.asin.trim().toUpperCase(); const existing = metaMap.get(asin); // Keep most complete title if (!existing || (r.title && r.title.length > (existing.title?.length || 0))) { metaMap.set(asin, { sku: r.sku, title: r.title, line: r.line }); } }); return metaMap; }, [rawData]); const filteredData = useMemo(() => filterData(rawData, filters, stockMap, vendorStockMap, top50Mode), [rawData, filters, stockMap, vendorStockMap, top50Mode]); const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode), [adsData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]); const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]); // Calculate country-aware Top 50 Best Sellers for 2025 const top50Ranking2025 = useMemo(() => { // Filter only 2025 data const data2025 = rawData.filter(r => r.year === 2025); const calculateTop50 = (entries: SalesRecord[]) => { const asinTotals = new Map(); entries.forEach(r => { const asin = r.asin.trim().toUpperCase(); asinTotals.set(asin, (asinTotals.get(asin) || 0) + r.sellOut); }); const sorted = Array.from(asinTotals.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 50); const rankMap = new Map(); sorted.forEach(([asin], index) => { rankMap.set(asin, index + 1); }); return rankMap; }; const euData = data2025.filter(r => !r.customer.toLowerCase().includes('uk')); const ukData = data2025.filter(r => r.customer.toLowerCase().includes('uk')); return { eu: calculateTop50(euData), uk: calculateTop50(ukData) }; }, [rawData]); // Determine which dataset to use for velocity calculation based on top50Mode // Combine Sales & Ads Data dynamically based on current filters const combinedAdsData = useMemo(() => { return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap); }, [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) const contextAggregatedData = useMemo(() => { // Check if we are filtering by specific items (SKU, ASIN, Title) const hasItemFilters = filters.sku.length > 0 || filters.asin.length > 0 || filters.title.length > 0; if (!hasItemFilters || filteredData.length === 0) { return null; } // 1. Identify the Product Lines associated with the currently filtered items const activeLines = Array.from(new Set(filteredData.map(r => r.line))); // 2. Create a "broad" filter: Keep Year/Customer/Month, but CLEAR Item filters, and restrict to these Lines const contextFilters: FilterState = { ...filters, line: activeLines, // Force these lines sku: [], // Clear specific item filters asin: [], title: [], stock: [] }; // 3. Process this broader dataset const broadData = filterData(rawData, contextFilters); return aggregateData(broadData); }, [rawData, filters, filteredData]); // Derive Options for Filter Dropdowns const filterOptions = useMemo(() => { return { customer: getUniqueValues(rawData, 'customer'), year: getUniqueValues(rawData, 'year'), month: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], line: getUniqueValues(rawData, 'line'), asin: getUniqueValues(rawData, 'asin'), sku: getUniqueValues(rawData, 'sku'), title: getUniqueValues(rawData, 'title'), week: Array.from(new Set(rawData.map(r => r.week).filter(w => w !== undefined))).sort((a, b) => (a as number) - (b as number)).map(w => `W${w}`), stock: [ 'Out of Stock (0)', 'In Stock (>0)', 'Low Stock (<10)', ...Array.from(new Set(Array.from(stockMap.values()).map(String))).sort((a, b) => parseFloat(a) - parseFloat(b)) ] }; }, [rawData, stockMap]); const handleFilterChange = (key: keyof FilterState, value: string[]) => { setFilters(prev => ({ ...prev, [key]: value })); }; const handleAskGemini = async (text: string) => { return await queryGemini(text, aggregatedData, filteredData.length); }; const disconnectUrl = async () => { // Allow disconnecting to clear data, but the app will likely re-connect on next reload due to "Permanent" requirement localStorage.removeItem('craze_csv_url'); await clearSalesData(); await clearAdsData(); setActiveUrl(null); setRawData([]); setAdsData([]); }; return (
{/* Header */}
{/* Logo Container (Horizontal Box) - Persistent User Image */}
{/* Title & Status */}

Analytics Dashboard

Live Sync
{lastUpdated && ( Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} )}
{/* Force Sync Action */} {/* View Switcher is the priority now */} {/* View Switcher */}
{/* Main Content */}
{loading ? ( // Initial loading spinner

Loading Dashboard...

Syncing with Dropbox...

) : ( <>
}>
0} adsData={filteredAdsData} stockMap={stockMap} vendorStockMap={vendorStockMap} top50Ranking={top50Ranking2025} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} />
}>
setFilters(prev => ({ ...prev, stock: s }))} vendorStockFilter={filters.vendorStock} onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} wocFilter={filters.woc} onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))} customerFilters={filters.customer} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} top50Ranking={top50Ranking2025} velocityMap={velocityMap} buyBoxLostMap={buyBoxLostMap} />
}>
}>
setFilters(prev => ({ ...prev, stock: s }))} vendorStockFilter={filters.vendorStock} onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} wocFilter={filters.woc} onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} buyBoxLostMap={buyBoxLostMap} />
}>
setFilters(prev => ({ ...prev, stock: s }))} vendorStockFilter={filters.vendorStock} onVendorStockFilterChange={(s) => setFilters(prev => ({ ...prev, vendorStock: s }))} wocFilter={filters.woc} onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))} top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} buyBoxLostMap={buyBoxLostMap} />
)}
{/* Chat Assistant */} {/* DATA MODAL */} { isDataModalOpen && (

Data Source Settings

) }
); }; export default App;