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 { 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';
// 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);
// Modal State
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
// Filters State
const [filters, setFilters] = useState({
customer: [],
year: [],
month: [],
line: [],
asin: [],
sku: [],
title: [],
});
// 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, globalAsinMetadata);
} 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} ${response.statusText}`);
}
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 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({
customer: [],
year: [],
month: [],
line: [],
asin: [],
sku: [],
title: [],
week: [],
});
};
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);
} 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);
} 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. Fetch Forecast data
handleForecastFetch(cachedData || [], globalAsinMetadata);
};
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), [rawData, filters]);
const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata), [adsData, filters, globalAsinMetadata]);
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]);
// Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => {
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData);
}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData]);
// 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: []
};
// 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}`),
};
}, [rawData]);
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
{activeUrl && lastUpdated && (
Live Sync Active
)}
{/* View Switcher is the priority now */}
{/* View Switcher */}
{/* Main Content */}
{loading ? (
// Initial loading spinner
Loading Dashboard...
Syncing with Dropbox...
) : (
<>
{view === 'dashboard' && (
)}
}>
{view === 'table' &&
0} adsData={filteredAdsData} />}
{view === 'weekly' && }
{view === 'movers' && }
{view === 'ads' && }
{view === 'forecast' && }
>
)}
{/* Chat Assistant */}
{/* DATA MODAL */}
{isDataModalOpen && (
Data Source Settings
)}
);
};
export default App;