diff --git a/App.tsx b/App.tsx
index 3ba9dfe..473e3d1 100644
--- a/App.tsx
+++ b/App.tsx
@@ -497,311 +497,335 @@ const App: React.FC = () => {
// Determine which dataset to use for velocity calculation based on top50Mode
// Combine Sales & Ads Data dynamically based on current filters
- const combinedAdsData = useMemo(() => {
- return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap);
- }, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]);
+ return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap);
+}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]);
- // Derived Data for Views
- const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]);
+// 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() };
- // 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;
+ 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));
- if (!hasItemFilters || filteredData.length === 0) {
- return null;
- }
+ return { minWeek: 1, maxWeek: maxWeek || 53, currentYear };
+}, [rawData]);
- // 1. Identify the Product Lines associated with the currently filtered items
- const activeLines = Array.from(new Set(filteredData.map(r => r.line)));
+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]);
- // 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: []
- };
+// Derived Data for Views
+const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]);
- // 3. Process this broader dataset
- const broadData = filterData(rawData, contextFilters);
- return aggregateData(broadData);
+// 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;
- }, [rawData, filters, filteredData]);
+ 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)));
- // 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 }));
+ // 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: []
};
- const handleAskGemini = async (text: string) => {
- return await queryGemini(text, aggregatedData, filteredData.length);
+ // 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 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([]);
- };
+const handleFilterChange = (key: keyof FilterState, value: string[]) => {
+ setFilters(prev => ({ ...prev, [key]: value }));
+};
- return (
-
+const handleAskGemini = async (text: string) => {
+ return await queryGemini(text, aggregatedData, filteredData.length);
+};
- {/* Header */}
-
-
-
- {/* Logo Container (Horizontal Box) - Persistent User Image */}
-
-
-
+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([]);
+};
- {/* Title & Status */}
-
-
Analytics Dashboard
-
-
-
- Live Sync
-
- {lastUpdated && (
-
- Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
- )}
-
-
+return (
+
+
+ {/* Header */}
+
+
+
+ {/* Logo Container (Horizontal Box) - Persistent User Image */}
+
+
-
- {/* Force Sync Action */}
-
{ handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); }}
- disabled={syncing}
- className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
- ${syncing
- ? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30'
- : 'bg-slate-900 text-slate-300 border border-border hover:border-indigo-500/50 hover:text-white shadow-lg active:scale-95'}`}
- >
-
- {syncing ? 'Syncing...' : 'Sync Now'}
-
- {/* View Switcher is the priority now */}
- {/* 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('weekly')}
- className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
- ${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
- >
- Weekly Sales
-
-
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
-
-
setView('ads')}
- className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
- ${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
- >
- Ads
-
-
setView('forecast')}
- className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
- ${view === 'forecast' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
- >
- Fc 26
-
+ {/* Title & Status */}
+
+
Analytics Dashboard
+
+
+
+ Live Sync
+
+ {lastUpdated && (
+
+ Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+ )}
-
- {/* Main Content */}
-
- {loading ? (
- // Initial loading spinner
-
-
-
Loading Dashboard...
-
Syncing with Dropbox...
+
+ {/* Force Sync Action */}
+
{ handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); }}
+ disabled={syncing}
+ className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
+ ${syncing
+ ? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30'
+ : 'bg-slate-900 text-slate-300 border border-border hover:border-indigo-500/50 hover:text-white shadow-lg active:scale-95'}`}
+ >
+
+ {syncing ? 'Syncing...' : 'Sync Now'}
+
+ {/* View Switcher is the priority now */}
+ {/* 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('weekly')}
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
+ ${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
+ >
+ Weekly Sales
+
+
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
+
+
setView('ads')}
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
+ ${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
+ >
+ Ads
+
+
setView('forecast')}
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
+ ${view === 'forecast' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
+ >
+ Fc 26
+
- ) : (
- <>
-
-
+
+
-
}>
-
- 0}
- adsData={filteredAdsData}
- stockMap={stockMap}
- vendorStockMap={vendorStockMap}
- top50Ranking={top50Ranking2025}
- top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
- 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 }))}
- 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}
- />
-
-
+ {/* Main Content */}
+
+ {loading ? (
+ // Initial loading spinner
+
+
+
Loading Dashboard...
+
Syncing with Dropbox...
+
+ ) : (
+ <>
+
+
+
+
- >
- )}
-
- {/* Chat Assistant */}
-
-
- {/* DATA MODAL */}
- {
- isDataModalOpen && (
-
-
-
-
Data Source Settings
- setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors">
-
-
-
-
-
-
}>
+
+ 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
+ setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors">
+
+
+
+
+
+
- )
- }
+
+ )
+ }
-
- );
+
+);
};
export default App;
diff --git a/components/DataGrid.tsx b/components/DataGrid.tsx
index 354acbf..1045286 100644
--- a/components/DataGrid.tsx
+++ b/components/DataGrid.tsx
@@ -23,6 +23,7 @@ interface DataGridProps {
vendorStockMap?: Map
;
velocityMap?: Map;
buyBoxLostMap?: Map }>;
+ defaultSort?: SortConfig;
}
type SortConfig = {
@@ -239,9 +240,9 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
};
-const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode, velocityMap, buyBoxLostMap }) => {
+const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode, velocityMap, buyBoxLostMap, defaultSort }) => {
const [currentPage, setCurrentPage] = useState(1);
- const [sortConfig, setSortConfig] = useState({ key: null, direction: 'desc' });
+ const [sortConfig, setSortConfig] = useState(defaultSort || { key: null, direction: 'desc' });
const [showChart, setShowChart] = useState(true);
const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']);
const [showAdsMetrics, setShowAdsMetrics] = useState(true);