import React, { useState, useMemo, useEffect, useCallback } from 'react'; import FileUpload from './components/FileUpload'; import Dashboard from './components/Dashboard'; import DataGrid from './components/DataGrid'; import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; import TopMoversPage from './components/TopMoversPage'; // New import import { SalesRecord, FilterState, AggregatedData } from './types'; import { processCSV, processExcel, filterData, aggregateData, getUniqueValues } from './services/dataProcessor'; import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon } from './components/Icons'; import { loadSalesData, saveSalesData, clearSalesData } from './services/storage'; // New Refresh Icon const RefreshIcon = ({ className }: { className?: string }) => ( ); // New Movers Icon const MoversIcon = ({ 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&dl=0"; const App: React.FC = () => { const [rawData, setRawData] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [view, setView] = useState<'dashboard' | 'table' | 'topMovers'>('dashboard'); // Added 'topMovers' const [isChatOpen, setIsChatOpen] = useState(false); const [activeUrl, setActiveUrl] = useState(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL); const [lastUpdated, setLastUpdated] = useState(null); // API Key State const [apiKey, setApiKey] = useState(() => localStorage.getItem('gemini_api_key') || ''); // Modal State const [isDataModalOpen, setIsDataModalOpen] = useState(false); // Filters State const [filters, setFilters] = useState({ customer: [], year: [], month: [], line: [], asin: [], sku: [], title: [], }); // Handle API Key Change const handleApiKeyChange = (key: string) => { setApiKey(key); if (key) { localStorage.setItem('gemini_api_key', key); } else { localStorage.removeItem('gemini_api_key'); } }; // Handle URL Fetch (Auto/Manual) const handleUrlFetch = useCallback(async (url: string) => { setSyncing(true); try { let directUrl = url; // Create a direct download link for Dropbox if it's a share link. if (url.includes('dropbox.com/') && !url.includes('dl.dropboxusercontent.com')) { const urlObject = new URL(url); urlObject.searchParams.set('dl', '1'); directUrl = urlObject.toString(); } // Use a CORS proxy to bypass browser's same-origin policy restrictions. const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(directUrl)}`; const response = await fetch(proxyUrl); if (!response.ok) throw new Error(`Failed to fetch CSV from URL: ${response.status} ${response.statusText}`); const csvText = await response.text(); const data = await processCSV(csvText); await saveSalesData(data); initializeData(data); setActiveUrl(url); // Store the original user-facing URL const now = new Date().toISOString(); setLastUpdated(now); localStorage.setItem('craze_last_updated', now); localStorage.setItem('craze_csv_url', url); setIsDataModalOpen(false); // Close modal on success } catch (error) { console.error("Failed to fetch/parse CSV from URL", error); // Don't alert on auto-fetch to avoid spamming the user on startup if offline // alert("Error syncing data. Please check the URL."); throw error; // re-throw to be caught by caller } finally { setSyncing(false); setLoading(false); } }, []); const initializeData = (data: SalesRecord[]) => { setRawData(data); setFilters({ customer: [], year: [], month: [], line: [], asin: [], sku: [], title: [], }); }; // 1. Initial Load from Cache (IndexedDB) or Auto-Fetch Permanent URL useEffect(() => { const initApp = async () => { setLoading(true); const currentStoredUrl = localStorage.getItem('craze_csv_url'); if (currentStoredUrl !== PERMANENT_DROPBOX_URL) { localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL); setActiveUrl(PERMANENT_DROPBOX_URL); } const { data, lastUpdated: date } = await loadSalesData(); if (data && data.length > 0) { console.log("Loaded data from cache:", data.length, "rows"); initializeData(data); setLastUpdated(date); setLoading(false); } else { console.log("No cache found. Auto-fetching from Permanent URL..."); handleUrlFetch(PERMANENT_DROPBOX_URL).catch(e => { console.error("Initial fetch failed."); }); } }; initApp(); }, [handleUrlFetch]); // Handle uploaded file (Manual) const handleFileUpload = async (file: File) => { setSyncing(true); try { let data: SalesRecord[] = []; const lowerName = file.name.toLowerCase(); if (lowerName.endsWith('.csv')) { data = await processCSV(file); } else if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) { data = await processExcel(file); } else { throw new Error("Unsupported file format"); } await saveSalesData(data); initializeData(data); setLastUpdated(new Date().toISOString()); setIsDataModalOpen(false); } catch (error) { console.error("Failed to parse file", error); alert("Error parsing file. Please check format (CSV or Excel)."); } 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..."); handleUrlFetch(PERMANENT_DROPBOX_URL).then(() => { localStorage.setItem('craze_last_refresh_date', today); console.log("Daily refresh successful."); }).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); }, [handleUrlFetch]); // Derive Data const filteredData = useMemo(() => filterData(rawData, filters), [rawData, filters]); const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]); // 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'), }; }, [rawData]); const handleFilterChange = (key: keyof FilterState, value: string[]) => { setFilters(prev => ({ ...prev, [key]: value })); }; const handleAskGemini = async (text: string) => { return await queryGemini(apiKey, 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(); setActiveUrl(null); setRawData([]); }; return (
{/* Header */}
{/* Logo Container (Horizontal Box) - Persistent User Image */}
{/* Title & Status */}

Analytics Dashboard

{activeUrl && lastUpdated && (

Live Sync Active

)}
{/* Main Action: Data Source Button */} {/* NEW REFRESH BUTTON */} {/* View Switcher */}
{/* Main Content */}
{loading ? ( // Initial loading spinner

Loading Dashboard...

Syncing with Dropbox...

) : ( <> {/* FilterBar is now relevant for all views including Top Movers */} {(view === 'dashboard' || view === 'table' || view === 'topMovers') && ( )}
{view === 'dashboard' ? ( ) : view === 'table' ? ( ) : ( // New Top Movers View )}
)}
{/* Chat Assistant - Now with API Key Props */} {/* DATA MODAL */} {isDataModalOpen && (

Data Source Settings

)}
); }; export default App;