mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:45:23 +02:00
Add Buy Box Lost detection feature with visual warning badges
This commit is contained in:
@@ -5,7 +5,7 @@ import Dashboard from './components/Dashboard';
|
|||||||
import FilterBar from './components/FilterBar';
|
import FilterBar from './components/FilterBar';
|
||||||
import AIChat from './components/AIChat';
|
import AIChat from './components/AIChat';
|
||||||
import CrazeLogo from './components/CrazeLogo';
|
import CrazeLogo from './components/CrazeLogo';
|
||||||
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, calculateVelocityMap, getUniqueValues, processForecastExcel } from './services/dataProcessor';
|
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel } from './services/dataProcessor';
|
||||||
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
|
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
|
||||||
import { queryGemini } from './services/geminiService';
|
import { queryGemini } from './services/geminiService';
|
||||||
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
||||||
@@ -52,6 +52,7 @@ const App: React.FC = () => {
|
|||||||
const [vendorStockMap, setVendorStockMap] = useState<Map<string, { eu: number; uk: number }>>(new Map());
|
const [vendorStockMap, setVendorStockMap] = useState<Map<string, { eu: number; uk: number }>>(new Map());
|
||||||
const [cachedForecastRecords, setCachedForecastRecords] = useState<ForecastRecord[]>([]);
|
const [cachedForecastRecords, setCachedForecastRecords] = useState<ForecastRecord[]>([]);
|
||||||
const [lastForecastFile, setLastForecastFile] = useState<string | null>(null);
|
const [lastForecastFile, setLastForecastFile] = useState<string | null>(null);
|
||||||
|
const [buyBoxLostMap, setBuyBoxLostMap] = useState<Map<string, { countries: string[]; reasons: Record<string, string> }>>(new Map());
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
|
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
|
||||||
@@ -197,6 +198,21 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleBuyBoxFetch = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
console.log('[App] Fetching Buy Box data from /Buy_Box_tracker.xlsx...');
|
||||||
|
const response = await fetch('/Buy_Box_tracker.xlsx');
|
||||||
|
if (!response.ok) throw new Error(`Failed to fetch Buy Box data: ${response.status}`);
|
||||||
|
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const data = await processBuyBoxExcel(buffer);
|
||||||
|
setBuyBoxLostMap(data);
|
||||||
|
console.log('[App] Successfully loaded Buy Box lost data for', data.size, 'ASINs');
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch/parse Buy Box data", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => {
|
const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => {
|
||||||
try {
|
try {
|
||||||
@@ -336,6 +352,7 @@ const App: React.FC = () => {
|
|||||||
console.log("Fetching Stock data...");
|
console.log("Fetching Stock data...");
|
||||||
handleStockFetch();
|
handleStockFetch();
|
||||||
handleVendorStockFetch();
|
handleVendorStockFetch();
|
||||||
|
handleBuyBoxFetch();
|
||||||
|
|
||||||
// 1e. Fetch Forecast data
|
// 1e. Fetch Forecast data
|
||||||
handleForecastFetch(cachedData || [], cachedData || [], filters);
|
handleForecastFetch(cachedData || [], cachedData || [], filters);
|
||||||
@@ -694,6 +711,7 @@ const App: React.FC = () => {
|
|||||||
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
||||||
top50Ranking={top50Ranking2025}
|
top50Ranking={top50Ranking2025}
|
||||||
velocityMap={velocityMap}
|
velocityMap={velocityMap}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -719,6 +737,7 @@ const App: React.FC = () => {
|
|||||||
wocFilter={filters.woc}
|
wocFilter={filters.woc}
|
||||||
onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))}
|
onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))}
|
||||||
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -738,6 +757,7 @@ const App: React.FC = () => {
|
|||||||
wocFilter={filters.woc}
|
wocFilter={filters.woc}
|
||||||
onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))}
|
onWocFilterChange={(s) => setFilters(prev => ({ ...prev, woc: s }))}
|
||||||
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
top50Mode={filters.customer.includes('Amazon UK') ? 'uk' : 'eu'}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons';
|
|||||||
import { StockBadge } from './StockBadge';
|
import { StockBadge } from './StockBadge';
|
||||||
import { Top50Badge } from './Top50Badge';
|
import { Top50Badge } from './Top50Badge';
|
||||||
import { VendorStockBadge } from './VendorStockBadge';
|
import { VendorStockBadge } from './VendorStockBadge';
|
||||||
|
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||||
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ interface AdsPerformanceProps {
|
|||||||
onVendorStockFilterChange: (newFilters: string[]) => void;
|
onVendorStockFilterChange: (newFilters: string[]) => void;
|
||||||
wocFilter: string[];
|
wocFilter: string[];
|
||||||
onWocFilterChange: (newFilters: string[]) => void;
|
onWocFilterChange: (newFilters: string[]) => void;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
||||||
@@ -45,7 +47,8 @@ const AdsRow: React.FC<{
|
|||||||
top50Mode: 'eu' | 'uk';
|
top50Mode: 'eu' | 'uk';
|
||||||
stockMap?: Map<string, number>;
|
stockMap?: Map<string, number>;
|
||||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
}> = React.memo(({ item, top50Ranking, top50Mode, stockMap, vendorStockMap }) => {
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
}> = React.memo(({ item, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap }) => {
|
||||||
const asin = item.asin.trim().toUpperCase();
|
const asin = item.asin.trim().toUpperCase();
|
||||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||||
|
|
||||||
@@ -78,6 +81,7 @@ const AdsRow: React.FC<{
|
|||||||
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
|
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||||
)}
|
)}
|
||||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
||||||
|
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -111,7 +115,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
|||||||
onVendorStockFilterChange,
|
onVendorStockFilterChange,
|
||||||
wocFilter,
|
wocFilter,
|
||||||
onWocFilterChange,
|
onWocFilterChange,
|
||||||
top50Mode
|
top50Mode,
|
||||||
|
buyBoxLostMap
|
||||||
}) => {
|
}) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||||
@@ -475,7 +480,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/[0.03]">
|
<tbody className="divide-y divide-white/[0.03]">
|
||||||
{filteredAndSorted.map((p) => (
|
{filteredAndSorted.map((p) => (
|
||||||
<AdsRow key={p.id} item={p} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} vendorStockMap={vendorStockMap} />
|
<AdsRow key={p.id} item={p} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} vendorStockMap={vendorStockMap} buyBoxLostMap={buyBoxLostMap} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface BuyBoxWarningBadgeProps {
|
||||||
|
asin: string;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BuyBoxWarningBadge: React.FC<BuyBoxWarningBadgeProps> = ({ asin, buyBoxLostMap }) => {
|
||||||
|
if (!buyBoxLostMap || !asin) return null;
|
||||||
|
|
||||||
|
const data = buyBoxLostMap.get(asin.toUpperCase());
|
||||||
|
if (!data || data.countries.length === 0) return null;
|
||||||
|
|
||||||
|
const tooltipContent = data.countries
|
||||||
|
.map(c => `${c}: ${data.reasons[c] || 'Unknown'}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-500/20 border border-amber-500/50 text-amber-400 text-[9px] font-bold uppercase tracking-tight cursor-help transition-all hover:bg-amber-500/30 hover:scale-105"
|
||||||
|
title={`⚠️ Buy Box Lost\n${tooltipContent}`}
|
||||||
|
>
|
||||||
|
<span className="text-xs">⚠️</span>
|
||||||
|
<span>BB Lost</span>
|
||||||
|
<span className="text-amber-300/80">{data.countries.join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { StockBadge } from './StockBadge';
|
|||||||
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
||||||
import { Top50Badge } from './Top50Badge';
|
import { Top50Badge } from './Top50Badge';
|
||||||
import { VendorStockBadge } from './VendorStockBadge';
|
import { VendorStockBadge } from './VendorStockBadge';
|
||||||
|
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||||
import { PAN_EU_COUNTRIES } from '../services/dataProcessor';
|
import { PAN_EU_COUNTRIES } from '../services/dataProcessor';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +27,7 @@ interface ForecastViewProps {
|
|||||||
onVendorStockFilterChange: (newFilters: string[]) => void;
|
onVendorStockFilterChange: (newFilters: string[]) => void;
|
||||||
wocFilter: string[];
|
wocFilter: string[];
|
||||||
onWocFilterChange: (newFilters: string[]) => void;
|
onWocFilterChange: (newFilters: string[]) => void;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
@@ -37,7 +39,8 @@ const ForecastRow: React.FC<{
|
|||||||
top50Mode: 'eu' | 'uk';
|
top50Mode: 'eu' | 'uk';
|
||||||
stockMap?: Map<string, number>;
|
stockMap?: Map<string, number>;
|
||||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap }) => {
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap, buyBoxLostMap }) => {
|
||||||
const asin = item.asin.trim().toUpperCase();
|
const asin = item.asin.trim().toUpperCase();
|
||||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||||
|
|
||||||
@@ -105,6 +108,7 @@ const ForecastRow: React.FC<{
|
|||||||
)}
|
)}
|
||||||
{/* WOC Indicator preserved */}
|
{/* WOC Indicator preserved */}
|
||||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
||||||
|
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -174,7 +178,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
onVendorStockFilterChange,
|
onVendorStockFilterChange,
|
||||||
wocFilter,
|
wocFilter,
|
||||||
onWocFilterChange,
|
onWocFilterChange,
|
||||||
top50Mode
|
top50Mode,
|
||||||
|
buyBoxLostMap
|
||||||
}) => {
|
}) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||||
@@ -504,6 +509,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
top50Mode={top50Mode}
|
top50Mode={top50Mode}
|
||||||
stockMap={stockMap}
|
stockMap={stockMap}
|
||||||
vendorStockMap={vendorStockMap}
|
vendorStockMap={vendorStockMap}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { StockBadge } from './StockBadge';
|
|||||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||||
import { Top50Badge } from './Top50Badge';
|
import { Top50Badge } from './Top50Badge';
|
||||||
import { VendorStockBadge } from './VendorStockBadge';
|
import { VendorStockBadge } from './VendorStockBadge';
|
||||||
|
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||||
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
||||||
|
|
||||||
interface WeeklyGridProps {
|
interface WeeklyGridProps {
|
||||||
@@ -25,6 +26,8 @@ interface WeeklyGridProps {
|
|||||||
wocFilter: string[];
|
wocFilter: string[];
|
||||||
onWocFilterChange: (newFilters: string[]) => void;
|
onWocFilterChange: (newFilters: string[]) => void;
|
||||||
velocityMap?: Map<string, number>;
|
velocityMap?: Map<string, number>;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
top50Mode?: 'eu' | 'uk';
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortConfig = {
|
type SortConfig = {
|
||||||
@@ -58,7 +61,8 @@ const WeeklyRow: React.FC<{
|
|||||||
customerFilters: string[];
|
customerFilters: string[];
|
||||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
velocityMap?: Map<string, number>;
|
velocityMap?: Map<string, number>;
|
||||||
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters, vendorStockMap, velocityMap }) => {
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters, vendorStockMap, velocityMap, buyBoxLostMap }) => {
|
||||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||||
const asin = row.asin.trim().toUpperCase();
|
const asin = row.asin.trim().toUpperCase();
|
||||||
|
|
||||||
@@ -101,6 +105,7 @@ const WeeklyRow: React.FC<{
|
|||||||
mode={top50Mode}
|
mode={top50Mode}
|
||||||
avgWeeklySales={velocityMap?.get(asin)}
|
avgWeeklySales={velocityMap?.get(asin)}
|
||||||
/>
|
/>
|
||||||
|
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +164,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
onWocFilterChange,
|
onWocFilterChange,
|
||||||
customerFilters,
|
customerFilters,
|
||||||
top50Mode,
|
top50Mode,
|
||||||
velocityMap
|
velocityMap,
|
||||||
|
buyBoxLostMap
|
||||||
}) => {
|
}) => {
|
||||||
// Pivot data - memoized
|
// Pivot data - memoized
|
||||||
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||||
@@ -638,6 +644,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
renderGrowth={renderGrowth}
|
renderGrowth={renderGrowth}
|
||||||
customerFilters={customerFilters}
|
customerFilters={customerFilters}
|
||||||
velocityMap={velocityMap}
|
velocityMap={velocityMap}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{displayCount < sortedRows.length && (
|
{displayCount < sortedRows.length && (
|
||||||
|
|||||||
@@ -1937,3 +1937,61 @@ export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promi
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sheet configuration for Buy Box tracking
|
||||||
|
const BB_SHEET_CONFIG: { sheet: string; country: string; reasonCol: number }[] = [
|
||||||
|
{ sheet: 'BB_FR', country: 'FR', reasonCol: 18 }, // Col S = index 18
|
||||||
|
{ sheet: 'BB_UK', country: 'UK', reasonCol: 9 }, // Col J = index 9
|
||||||
|
{ sheet: 'BB_DE', country: 'DE', reasonCol: 13 }, // Col N = index 13
|
||||||
|
{ sheet: 'BB_IT', country: 'IT', reasonCol: 12 }, // Col M = index 12
|
||||||
|
{ sheet: 'BB_ES', country: 'ES', reasonCol: 13 }, // Col N = index 13
|
||||||
|
];
|
||||||
|
|
||||||
|
export const processBuyBoxExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, { countries: string[]; reasons: Record<string, string> }>> => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = fileOrBuffer instanceof File
|
||||||
|
? await fileOrBuffer.arrayBuffer()
|
||||||
|
: fileOrBuffer;
|
||||||
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
|
|
||||||
|
// Map: ASIN -> { countries: [], reasons: {} }
|
||||||
|
const buyBoxMap = new Map<string, { countries: string[]; reasons: Record<string, string> }>();
|
||||||
|
|
||||||
|
for (const config of BB_SHEET_CONFIG) {
|
||||||
|
const worksheet = workbook.Sheets[config.sheet];
|
||||||
|
if (!worksheet) {
|
||||||
|
console.warn(`[BuyBox] Sheet ${config.sheet} not found, skipping...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||||
|
|
||||||
|
// Skip header row (index 0)
|
||||||
|
for (let i = 1; i < jsonData.length; i++) {
|
||||||
|
const row = jsonData[i];
|
||||||
|
const asin = String(row[1] || '').trim().toUpperCase(); // Col B = index 1
|
||||||
|
if (!asin || asin.length < 5) continue;
|
||||||
|
|
||||||
|
const rawReason = String(row[config.reasonCol] || '').trim();
|
||||||
|
const reason = rawReason || 'Unknown';
|
||||||
|
|
||||||
|
// Only add if there's actually a BB lost (non-empty reason or explicit entry)
|
||||||
|
if (!buyBoxMap.has(asin)) {
|
||||||
|
buyBoxMap.set(asin, { countries: [], reasons: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = buyBoxMap.get(asin)!;
|
||||||
|
if (!entry.countries.includes(config.country)) {
|
||||||
|
entry.countries.push(config.country);
|
||||||
|
}
|
||||||
|
entry.reasons[config.country] = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[BuyBox] Processed ${buyBoxMap.size} ASINs with BB lost`);
|
||||||
|
return buyBoxMap;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing Buy Box Excel:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -225,3 +225,9 @@ export interface ProductForecastData {
|
|||||||
avgWeeklySales: number; // Added for Weeks of Coverage
|
avgWeeklySales: number; // Added for Weeks of Coverage
|
||||||
monthlyData: Record<string, MonthlyForecastPoint>;
|
monthlyData: Record<string, MonthlyForecastPoint>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BuyBoxLostData {
|
||||||
|
asin: string;
|
||||||
|
countries: string[]; // ['DE', 'FR', 'IT', 'UK', 'ES']
|
||||||
|
reasons: Record<string, string>; // { DE: 'Amazon', FR: 'Unknown' }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user