Implement Grid-specific YTD filtering and default sorting

This commit is contained in:
christian.vidal
2026-01-29 20:27:19 +01:00
parent 55c91a7bfc
commit 2c6b9c9f5a
2 changed files with 301 additions and 276 deletions
+298 -274
View File
@@ -497,311 +497,335 @@ const App: React.FC = () => {
// Determine which dataset to use for velocity calculation based on top50Mode // Determine which dataset to use for velocity calculation based on top50Mode
// Combine Sales & Ads Data dynamically based on current filters // Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => { return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap);
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap); }, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]);
}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData, velocityMap]);
// Derived Data for Views // Derived Data for Grid (YTD Filtered - Isolated)
const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]); // 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 currentYear = Math.max(...rawData.map(r => r.year));
const contextAggregatedData = useMemo(() => { const currentYearData = rawData.filter(r => r.year === currentYear);
// Check if we are filtering by specific items (SKU, ASIN, Title) const maxWeek = Math.max(...currentYearData.map(r => r.week).filter(Boolean));
const hasItemFilters = filters.sku.length > 0 || filters.asin.length > 0 || filters.title.length > 0;
if (!hasItemFilters || filteredData.length === 0) { return { minWeek: 1, maxWeek: maxWeek || 53, currentYear };
return null; }, [rawData]);
}
// 1. Identify the Product Lines associated with the currently filtered items const ytdGridData = useMemo(() => {
const activeLines = Array.from(new Set(filteredData.map(r => r.line))); // 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 // Derived Data for Views
const contextFilters: FilterState = { const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]);
...filters,
line: activeLines, // Force these lines
sku: [], // Clear specific item filters
asin: [],
title: [],
stock: []
};
// 3. Process this broader dataset // Derive Context Data (Product Line Context when drilling down)
const broadData = filterData(rawData, contextFilters); const contextAggregatedData = useMemo(() => {
return aggregateData(broadData); // 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 // 2. Create a "broad" filter: Keep Year/Customer/Month, but CLEAR Item filters, and restrict to these Lines
const filterOptions = useMemo(() => { const contextFilters: FilterState = {
return { ...filters,
customer: getUniqueValues(rawData, 'customer'), line: activeLines, // Force these lines
year: getUniqueValues(rawData, 'year'), sku: [], // Clear specific item filters
month: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], asin: [],
line: getUniqueValues(rawData, 'line'), title: [],
asin: getUniqueValues(rawData, 'asin'), stock: []
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) => { // 3. Process this broader dataset
return await queryGemini(text, aggregatedData, filteredData.length); 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 () => { const handleFilterChange = (key: keyof FilterState, value: string[]) => {
// Allow disconnecting to clear data, but the app will likely re-connect on next reload due to "Permanent" requirement setFilters(prev => ({ ...prev, [key]: value }));
localStorage.removeItem('craze_csv_url'); };
await clearSalesData();
await clearAdsData();
setActiveUrl(null);
setRawData([]);
setAdsData([]);
};
return ( const handleAskGemini = async (text: string) => {
<div className="min-h-screen flex flex-col bg-background text-slate-200"> return await queryGemini(text, aggregatedData, filteredData.length);
};
{/* Header */} const disconnectUrl = async () => {
<header className="bg-slate-950/90 backdrop-blur border-b border-border py-4 px-6 relative z-40 shadow-2xl"> // Allow disconnecting to clear data, but the app will likely re-connect on next reload due to "Permanent" requirement
<div className="max-w-7xl mx-auto flex justify-between items-center"> localStorage.removeItem('craze_csv_url');
<div className="flex items-center gap-8"> await clearSalesData();
{/* Logo Container (Horizontal Box) - Persistent User Image */} await clearAdsData();
<div className="h-16 w-64 relative flex-shrink-0"> setActiveUrl(null);
<CrazeLogo /> setRawData([]);
</div> setAdsData([]);
};
{/* Title & Status */} return (
<div className="hidden lg:block border-l border-slate-700 pl-6"> <div className="min-h-screen flex flex-col bg-background text-slate-200">
<h1 className="text-xl font-bold tracking-tight text-white text-shadow-sm">Analytics Dashboard</h1>
<div className="flex items-center gap-3 mt-1.5"> {/* Header */}
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-emerald-500/10 border border-emerald-500/20 rounded-full"> <header className="bg-slate-950/90 backdrop-blur border-b border-border py-4 px-6 relative z-40 shadow-2xl">
<span className={`w-1.5 h-1.5 rounded-full bg-emerald-400 ${syncing ? 'animate-ping' : ''}`}></span> <div className="max-w-7xl mx-auto flex justify-between items-center">
<span className="text-[9px] text-emerald-400 font-black uppercase tracking-wider">Live Sync</span> <div className="flex items-center gap-8">
</div> {/* Logo Container (Horizontal Box) - Persistent User Image */}
{lastUpdated && ( <div className="h-16 w-64 relative flex-shrink-0">
<span className="text-[9px] text-slate-500 font-bold uppercase tracking-wider"> <CrazeLogo />
Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
)}
</div>
</div>
</div> </div>
<div className="flex items-center gap-6"> {/* Title & Status */}
{/* Force Sync Action */} <div className="hidden lg:block border-l border-slate-700 pl-6">
<button <h1 className="text-xl font-bold tracking-tight text-white text-shadow-sm">Analytics Dashboard</h1>
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); }} <div className="flex items-center gap-3 mt-1.5">
disabled={syncing} <div className="flex items-center gap-1.5 px-2 py-0.5 bg-emerald-500/10 border border-emerald-500/20 rounded-full">
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all <span className={`w-1.5 h-1.5 rounded-full bg-emerald-400 ${syncing ? 'animate-ping' : ''}`}></span>
${syncing <span className="text-[9px] text-emerald-400 font-black uppercase tracking-wider">Live Sync</span>
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30' </div>
: 'bg-slate-900 text-slate-300 border border-border hover:border-indigo-500/50 hover:text-white shadow-lg active:scale-95'}`} {lastUpdated && (
> <span className="text-[9px] text-slate-500 font-bold uppercase tracking-wider">
<RefreshIcon className={`w-4 h-4 ${syncing ? 'animate-spin' : ''}`} /> Last: {new Date(lastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
<span>{syncing ? 'Syncing...' : 'Sync Now'}</span> </span>
</button> )}
{/* View Switcher is the priority now */}
{/* View Switcher */}
<div className="flex bg-slate-900 rounded-lg p-1 border border-border">
<button
onClick={() => 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'}`}
>
<ChartIcon /> <span className="hidden sm:inline">Dashboard</span>
</button>
<button
onClick={() => 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'}`}
>
<TableIcon /> <span className="hidden sm:inline">Grid</span>
</button>
<button
onClick={() => 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'}`}
>
<TrendingIcon /> <span className="hidden sm:inline">Weekly Sales</span>
</button>
<button
onClick={() => 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'}`}
>
<TrendingIcon /> <span className="hidden sm:inline">Movers</span>
</button>
<button
onClick={() => 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'}`}
>
<MegaphoneIcon /> <span className="hidden sm:inline">Ads</span>
</button>
<button
onClick={() => 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'}`}
>
<ChartIcon /> <span className="hidden sm:inline">Fc 26</span>
</button>
</div> </div>
</div> </div>
</div> </div>
</header>
{/* Main Content */} <div className="flex items-center gap-6">
<main className="flex-1 relative"> {/* Force Sync Action */}
{loading ? ( <button
// Initial loading spinner onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); }}
<div className="flex flex-col items-center justify-center h-[80vh] gap-4"> disabled={syncing}
<div className="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div> className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
<h2 className="text-xl font-bold text-slate-300">Loading Dashboard...</h2> ${syncing
<p className="text-sm text-slate-500">Syncing with Dropbox...</p> ? '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'}`}
>
<RefreshIcon className={`w-4 h-4 ${syncing ? 'animate-spin' : ''}`} />
<span>{syncing ? 'Syncing...' : 'Sync Now'}</span>
</button>
{/* View Switcher is the priority now */}
{/* View Switcher */}
<div className="flex bg-slate-900 rounded-lg p-1 border border-border">
<button
onClick={() => 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'}`}
>
<ChartIcon /> <span className="hidden sm:inline">Dashboard</span>
</button>
<button
onClick={() => 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'}`}
>
<TableIcon /> <span className="hidden sm:inline">Grid</span>
</button>
<button
onClick={() => 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'}`}
>
<TrendingIcon /> <span className="hidden sm:inline">Weekly Sales</span>
</button>
<button
onClick={() => 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'}`}
>
<TrendingIcon /> <span className="hidden sm:inline">Movers</span>
</button>
<button
onClick={() => 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'}`}
>
<MegaphoneIcon /> <span className="hidden sm:inline">Ads</span>
</button>
<button
onClick={() => 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'}`}
>
<ChartIcon /> <span className="hidden sm:inline">Fc 26</span>
</button>
</div> </div>
) : ( </div>
<> </div>
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} /> </header>
<div className="mt-6">
<div className={view === 'dashboard' ? '' : 'hidden'}>
<Dashboard data={aggregatedData} filterState={filters} adsData={filteredAdsData} stockMap={stockMap} />
</div>
<Suspense fallback={<LoadingSpinner />}> {/* Main Content */}
<div className={view === 'table' ? '' : 'hidden'}> <main className="flex-1 relative">
<DataGrid {loading ? (
data={combinedAdsData} // Initial loading spinner
hasCustomerFilter={filters.customer.length > 0} <div className="flex flex-col items-center justify-center h-[80vh] gap-4">
adsData={filteredAdsData} <div className="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
stockMap={stockMap} <h2 className="text-xl font-bold text-slate-300">Loading Dashboard...</h2>
vendorStockMap={vendorStockMap} <p className="text-sm text-slate-500">Syncing with Dropbox...</p>
top50Ranking={top50Ranking2025} </div>
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'} ) : (
velocityMap={velocityMap} <>
buyBoxLostMap={buyBoxLostMap} <FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
/> <div className="mt-6">
</div> <div className={view === 'dashboard' ? '' : 'hidden'}>
</Suspense> <Dashboard data={aggregatedData} filterState={filters} adsData={filteredAdsData} stockMap={stockMap} />
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'weekly' ? '' : 'hidden'}>
<WeeklyGrid
data={combinedAdsData}
onDrillDown={handleSkuDrillDown}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
stockFilter={filters.stock}
onStockFilterChange={(s) => 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}
/>
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'movers' ? '' : 'hidden'}>
<TopMovers data={filteredData} stockMap={stockMap} />
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'ads' ? '' : 'hidden'}>
<AdsPerformance
data={combinedAdsData}
filters={filters}
top50Ranking={top50Ranking2025}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
stockFilter={filters.stock}
onStockFilterChange={(s) => 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}
/>
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'forecast' ? '' : 'hidden'}>
<ForecastView
data={forecastData}
filters={filters}
top50Ranking={top50Ranking2025}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
stockFilter={filters.stock}
onStockFilterChange={(s) => 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}
/>
</div>
</Suspense>
</div> </div>
</>
)}
</main>
{/* Chat Assistant */} <Suspense fallback={<LoadingSpinner />}>
<AIChat onSendMessage={handleAskGemini} isOpen={isChatOpen} setIsOpen={setIsChatOpen} /> <div className={view === 'table' ? '' : 'hidden'}>
<DataGrid
{/* DATA MODAL */} data={ytdGridData}
{ hasCustomerFilter={filters.customer.length > 0}
isDataModalOpen && ( adsData={filteredAdsData}
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm animate-fade-in p-4"> stockMap={stockMap}
<div className="bg-slate-950 border border-border rounded-2xl shadow-2xl w-full max-w-lg relative overflow-hidden"> vendorStockMap={vendorStockMap}
<div className="bg-slate-900 px-6 py-4 border-b border-border flex justify-between items-center"> top50Ranking={top50Ranking2025}
<h2 className="text-lg font-bold text-white">Data Source Settings</h2> top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
<button onClick={() => setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors"> velocityMap={velocityMap}
<CloseIcon /> buyBoxLostMap={buyBoxLostMap}
</button> defaultSort={{ key: `total_sellOut_${gridContext.currentYear}`, direction: 'desc' }}
</div>
<div className="p-6">
<FileUpload
onSalesUpload={handleSalesUpload}
onAdsUpload={handleAdsUpload}
onTrafficUpload={handleTrafficUpload}
onUrlSubmit={handleDataFetch}
isLoading={syncing}
activeUrl={activeUrl}
onDisconnect={disconnectUrl}
lastUpdated={lastUpdated}
/> />
</div> </div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'weekly' ? '' : 'hidden'}>
<WeeklyGrid
data={combinedAdsData}
onDrillDown={handleSkuDrillDown}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
stockFilter={filters.stock}
onStockFilterChange={(s) => 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}
/>
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'movers' ? '' : 'hidden'}>
<TopMovers data={filteredData} stockMap={stockMap} />
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'ads' ? '' : 'hidden'}>
<AdsPerformance
data={combinedAdsData}
filters={filters}
top50Ranking={top50Ranking2025}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
stockFilter={filters.stock}
onStockFilterChange={(s) => 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}
/>
</div>
</Suspense>
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'forecast' ? '' : 'hidden'}>
<ForecastView
data={forecastData}
filters={filters}
top50Ranking={top50Ranking2025}
stockMap={stockMap}
vendorStockMap={vendorStockMap}
stockFilter={filters.stock}
onStockFilterChange={(s) => 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}
/>
</div>
</Suspense>
</div>
</>
)}
</main>
{/* Chat Assistant */}
<AIChat onSendMessage={handleAskGemini} isOpen={isChatOpen} setIsOpen={setIsChatOpen} />
{/* DATA MODAL */}
{
isDataModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm animate-fade-in p-4">
<div className="bg-slate-950 border border-border rounded-2xl shadow-2xl w-full max-w-lg relative overflow-hidden">
<div className="bg-slate-900 px-6 py-4 border-b border-border flex justify-between items-center">
<h2 className="text-lg font-bold text-white">Data Source Settings</h2>
<button onClick={() => setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors">
<CloseIcon />
</button>
</div>
<div className="p-6">
<FileUpload
onSalesUpload={handleSalesUpload}
onAdsUpload={handleAdsUpload}
onTrafficUpload={handleTrafficUpload}
onUrlSubmit={handleDataFetch}
isLoading={syncing}
activeUrl={activeUrl}
onDisconnect={disconnectUrl}
lastUpdated={lastUpdated}
/>
</div> </div>
</div> </div>
) </div>
} )
}
</div > </div >
); );
}; };
export default App; export default App;
+3 -2
View File
@@ -23,6 +23,7 @@ interface DataGridProps {
vendorStockMap?: Map<string, { eu: number; uk: number }>; vendorStockMap?: Map<string, { eu: number; uk: number }>;
velocityMap?: Map<string, number>; velocityMap?: Map<string, number>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>; buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
defaultSort?: SortConfig;
} }
type SortConfig = { type SortConfig = {
@@ -239,9 +240,9 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
}; };
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode, velocityMap, buyBoxLostMap }) => { const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode, velocityMap, buyBoxLostMap, defaultSort }) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: null, direction: 'desc' }); const [sortConfig, setSortConfig] = useState<SortConfig>(defaultSort || { key: null, direction: 'desc' });
const [showChart, setShowChart] = useState(true); const [showChart, setShowChart] = useState(true);
const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']); const [visibleMetrics, setVisibleMetrics] = useState<('sellOut' | 'units')[]>(['sellOut', 'units']);
const [showAdsMetrics, setShowAdsMetrics] = useState(true); const [showAdsMetrics, setShowAdsMetrics] = useState(true);