import React, { useState, useMemo, useEffect, useCallback } from 'react'; import FileUpload from './components/FileUpload'; import Dashboard from './components/Dashboard'; import DataGrid from './components/DataGrid'; import TopMovers from './components/TopMovers'; // import AdvertisingDashboard from './components/AdvertisingDashboard'; // Removed import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; import { SalesRecord, FilterState, AggregatedData, AdsRecord } from './types'; // Imported AdsRecord import { processCSV, filterData, aggregateData, getUniqueValues, processAdsCSV, mergeSalesAndAdsData } from './services/dataProcessor'; // Imported new processors import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon } from './components/Icons'; import { loadSalesData, saveSalesData, clearSalesData } from './services/storage'; // 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 App: React.FC = () => { const [rawData, setRawData] = useState([]); const [adsData, setAdsData] = useState([]); // New Ads State const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [view, setView] = useState<'dashboard' | 'table' | 'movers' | 'ads'>('dashboard'); // Added 'ads' view 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 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(); } // Unified Fetch Logic for Local (Vite) and Production (Vercel Function) // Both environments now support the /api/dropbox/... path. // - Local: Vite proxies /api/dropbox -> https://www.dropbox.com // - Vercel: api/dropbox.js handles the request -> https://www.dropbox.com const urlObj = new URL(directUrl); const searchParams = urlObj.search; // Construct path relative to root: /api/dropbox/scl/fi/... const fetchUrl = `/api/dropbox${urlObj.pathname}${searchParams}`; const response = await fetch(fetchUrl); 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: [], week: [], }); }; // 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..."); handleUrlFetch(PERMANENT_DROPBOX_URL).catch(e => { console.error("Initial fetch failed."); }); } }; initApp(); }, [handleUrlFetch]); // 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); } 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) const handleAdsUpload = async (file: File) => { setSyncing(true); try { const data = await processAdsCSV(file); setAdsData(data); console.log("Ads loaded:", data.length); setIsDataModalOpen(false); // setView('ads'); // Removed switching to ads view } catch (error) { console.error("Failed to parse Ads CSV", error); alert("Error parsing Ads CSV. 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..."); 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]); // Combine Sales & Ads Data dynamically based on current filters const combinedAdsData = useMemo(() => { return mergeSalesAndAdsData(filteredData, adsData); }, [filteredData, adsData]); // 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(); 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 */} setIsDataModalOpen(true)} className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-bold shadow-lg transition-all border ${activeUrl ? 'bg-slate-800 text-emerald-400 border-emerald-500/50 hover:bg-slate-700' : 'bg-indigo-600 text-white border-transparent hover:bg-indigo-500'}`} > {syncing ? : } {activeUrl ? 'Data Settings' : 'Connect Data'} {/* NEW REFRESH BUTTON */} activeUrl && handleUrlFetch(activeUrl)} disabled={syncing} title="Refresh Data" className="p-3 rounded-lg bg-slate-800 border border-border text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > {/* View Switcher */} setView('dashboard')} className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all ${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} > Dashboard setView('table')} className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all ${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} > Grid setView('movers')} className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all ${view === 'movers' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} > Movers {/* Main Content */} {loading ? ( // Initial loading spinner Loading Dashboard... Syncing with Dropbox... ) : ( <> {view === 'dashboard' && ( )} {view === 'table' && } {view === 'movers' && } > )} {/* Chat Assistant */} {/* DATA MODAL */} {isDataModalOpen && ( Data Source Settings setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors"> )} ); }; export default App;
Live Sync Active
Syncing with Dropbox...