diff --git a/App.tsx b/App.tsx index 473e3d1..bc1cba2 100644 --- a/App.tsx +++ b/App.tsx @@ -497,335 +497,336 @@ const App: React.FC = () => { // Determine which dataset to use for velocity calculation based on top50Mode // Combine Sales & Ads Data dynamically based on current filters - return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap); -}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]); + const combinedAdsData = useMemo(() => { + return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap); + }, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]); -// Derived Data for Grid (YTD Filtered - Isolated) -// 1. Identify "Current Year" max week -const gridContext = useMemo(() => { - if (rawData.length === 0) return { minWeek: 1, maxWeek: 53, currentYear: new Date().getFullYear() }; + // Derived Data for Grid (YTD Filtered - Isolated) + // 1. Identify "Current Year" max week + const gridContext = useMemo(() => { + if (rawData.length === 0) return { minWeek: 1, maxWeek: 53, currentYear: new Date().getFullYear() }; - const currentYear = Math.max(...rawData.map(r => r.year)); - const currentYearData = rawData.filter(r => r.year === currentYear); - const maxWeek = Math.max(...currentYearData.map(r => r.week).filter(Boolean)); + const currentYear = Math.max(...rawData.map(r => r.year)); + const currentYearData = rawData.filter(r => r.year === currentYear); + const maxWeek = Math.max(...currentYearData.map(r => r.week).filter(Boolean)); - return { minWeek: 1, maxWeek: maxWeek || 53, currentYear }; -}, [rawData]); + return { minWeek: 1, maxWeek: maxWeek || 53, currentYear }; + }, [rawData]); -const ytdGridData = useMemo(() => { - // Filter combinedAdsData to only include weeks <= maxWeek - // This ensures we compare "Like for Like" periods across years - // We do strictly LESS THAN OR EQUAL to maxWeek. - if (!combinedAdsData) return []; - return combinedAdsData.filter(item => { - // CombinedKPIs has 'week' property - if (!item.week) return true; // Keep non-weekly items if any (shouldn't be) - return item.week <= gridContext.maxWeek; - }); -}, [combinedAdsData, gridContext.maxWeek]); + const ytdGridData = useMemo(() => { + // Filter combinedAdsData to only include weeks <= maxWeek + // This ensures we compare "Like for Like" periods across years + // We do strictly LESS THAN OR EQUAL to maxWeek. + if (!combinedAdsData) return []; + return combinedAdsData.filter(item => { + // CombinedKPIs has 'week' property + if (!item.week) return true; // Keep non-weekly items if any (shouldn't be) + return item.week <= gridContext.maxWeek; + }); + }, [combinedAdsData, gridContext.maxWeek]); -// Derived Data for Views -const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]); + // 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; + // 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'} - velocityMap={velocityMap} - buyBoxLostMap={buyBoxLostMap} - defaultSort={{ key: `total_sellOut_${gridContext.currentYear}`, direction: 'desc' }} - /> -
-
- - }> -
- 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

- -
- -
- -
-
-
- ) + 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'} + velocityMap={velocityMap} + buyBoxLostMap={buyBoxLostMap} + defaultSort={{ key: `total_sellOut_${gridContext.currentYear}`, direction: 'desc' }} + /> +
+
+ + }> +
+ 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;