diff --git a/App.tsx b/App.tsx index 7d80d6b..260e3c5 100644 --- a/App.tsx +++ b/App.tsx @@ -125,528 +125,536 @@ const App: React.FC = () => { } }, []); - setTrafficData(data); - console.log('[App] Successfully loaded', data.length, 'traffic records'); -} catch (error) { - console.error("Failed to fetch/parse Traffic", error); -} + 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 /api/fetch-stock...'); - const response = await fetch('/api/fetch-stock'); - if (!response.ok) throw new Error(`Failed to fetch stock: ${response.status}`); + const handleStockFetch = useCallback(async () => { + try { + console.log('[App] Fetching stock from /api/fetch-stock...'); + const response = await fetch('/api/fetch-stock'); + 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 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 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'; + 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'; - console.log(`[App] Fetching forecast from ${filename}...`); - const response = await fetch(filename); - if (!response.ok) throw new Error(`Forecast file ${filename} not found`); + console.log(`[App] Fetching forecast from ${filename}...`); + const response = await fetch(filename); + if (!response.ok) throw new Error(`Forecast file ${filename} not found`); - const buffer = await response.arrayBuffer(); - const fcRecords = await processForecastExcel(buffer); + const buffer = await response.arrayBuffer(); + const fcRecords = await processForecastExcel(buffer); - // 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; + // 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 => { + 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); + setForecastData(viewData); + console.log('[App] Forecast loaded:', viewData.length, 'records'); + } catch (error) { + console.warn("Forecast fetch failed:", error); + } + }, []); + + // 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 + }); + handleForecastFetch(forecastRelevantData, rawData, filters); + } + }, [filters, rawData, handleForecastFetch]); + + 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); + // 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(); + + // 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 = meta.get(asin); + const existing = metaMap.get(asin); + // Keep most complete title if (!existing || (r.title && r.title.length > (existing.title?.length || 0))) { - meta.set(asin, { sku: r.sku, title: r.title, line: r.line }); + metaMap.set(asin, { sku: r.sku, title: r.title, line: r.line }); } }); + return metaMap; + }, [rawData]); - const viewData = calculateForecastViewData(sales, fcRecords, meta, activeFilters); - setForecastData(viewData); - console.log('[App] Forecast loaded:', viewData.length, 'records'); - } catch (error) { - console.warn("Forecast fetch failed:", error); - } -}, []); + const filteredData = useMemo(() => filterData(rawData, filters), [rawData, filters]); + const filteredAdsData = useMemo(() => filterAdsData(adsData, filters, globalAsinMetadata), [adsData, filters, globalAsinMetadata]); + const aggregatedData = useMemo(() => aggregateData(filteredData), [filteredData]); -// 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, { + // 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, - year: [] // Ensure we don't filter out 2025/2026 if a single year is selected in UI - }); - handleForecastFetch(forecastRelevantData, rawData, filters); - } -}, [filters, rawData, handleForecastFetch]); + line: activeLines, // Force these lines + sku: [], // Clear specific item filters + asin: [], + title: [] + }; -const initializeData = (data: SalesRecord[]) => { - setRawData(data); - setFilters({ - customer: [], - year: [], - month: [], - line: [], - asin: [], - sku: [], - title: [], - week: [], - }); -}; + // 3. Process this broader dataset + const broadData = filterData(rawData, contextFilters); + return aggregateData(broadData); -const handleSkuDrillDown = useCallback((sku: string) => { - if (!sku) return; - setFilters(prev => ({ - ...prev, - sku: [sku] - })); - setView('ads'); -}, []); + }, [rawData, filters, filteredData]); -// 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; + // 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]); - // 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(); - - // 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); - }); - } + const handleFilterChange = (key: keyof FilterState, value: string[]) => { + setFilters(prev => ({ ...prev, [key]: value })); }; - // 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 handleAskGemini = async (text: string) => { + return await queryGemini(text, aggregatedData, filteredData.length); }; - 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: [] + 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([]); }; - // 3. Process this broader dataset - const broadData = filterData(rawData, contextFilters); - return aggregateData(broadData); + return ( +
-}, [rawData, filters, filteredData]); + {/* Header */} +
+
+
+ {/* Logo Container (Horizontal Box) - Persistent User Image */} +
+ +
- -// 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

+
+
+ + Live Sync +
+ {lastUpdated && ( + + Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + )} +
+
- {/* 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 */} +
+ + + + + +
+
-
- {/* Force Sync Action */} - - {/* 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} stockMap={stockMap} /> + + )} + {view === 'weekly' && ( + }> + 0 ? mergeSalesAndAdsData(filteredData, filteredAdsData, undefined, trafficData) : filteredData} onDrillDown={handleSkuDrillDown} stockMap={stockMap} /> + + )} + {view === 'movers' && ( + }> + + + )} + {view === 'ads' && ( + }> + + + )} + {view === 'forecast' && ( + }> + + + )} +
+ + )} +
+ + {/* Chat Assistant */} + + + {/* DATA MODAL */} + {isDataModalOpen && ( +
+
+
+

Data Source Settings

+ +
+ +
+ +
-
-
- - {/* Main Content */} -
- {loading ? ( - // Initial loading spinner -
-
-

Loading Dashboard...

-

Syncing with Dropbox...

-
- ) : ( - <> - -
- {view === 'dashboard' && } - {view === 'table' && ( - }> - 0} adsData={filteredAdsData} stockMap={stockMap} /> - - )} - {view === 'weekly' && ( - }> - 0 ? mergeSalesAndAdsData(filteredData, filteredAdsData, undefined, trafficData) : filteredData} onDrillDown={handleSkuDrillDown} stockMap={stockMap} /> - - )} - {view === 'movers' && ( - }> - - - )} - {view === 'ads' && ( - }> - - - )} - {view === 'forecast' && ( - }> - - - )} -
- )} -
- {/* Chat Assistant */} - - - {/* DATA MODAL */} - {isDataModalOpen && ( -
-
-
-

Data Source Settings

- -
- -
- -
-
-
- )} - -
-); + + ); }; export default App;