mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:15:24 +02:00
Add Buy Box Lost detection feature with visual warning badges
This commit is contained in:
@@ -5,6 +5,7 @@ import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons';
|
||||
import { StockBadge } from './StockBadge';
|
||||
import { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
||||
|
||||
@@ -24,6 +25,7 @@ interface AdsPerformanceProps {
|
||||
onVendorStockFilterChange: (newFilters: string[]) => void;
|
||||
wocFilter: string[];
|
||||
onWocFilterChange: (newFilters: string[]) => void;
|
||||
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||
}
|
||||
|
||||
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
||||
@@ -45,7 +47,8 @@ const AdsRow: React.FC<{
|
||||
top50Mode: 'eu' | 'uk';
|
||||
stockMap?: Map<string, 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 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, ''))} />
|
||||
)}
|
||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
||||
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -111,7 +115,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
||||
onVendorStockFilterChange,
|
||||
wocFilter,
|
||||
onWocFilterChange,
|
||||
top50Mode
|
||||
top50Mode,
|
||||
buyBoxLostMap
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||
@@ -475,7 +480,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.03]">
|
||||
{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>
|
||||
</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 { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||
import { PAN_EU_COUNTRIES } from '../services/dataProcessor';
|
||||
import {
|
||||
@@ -26,6 +27,7 @@ interface ForecastViewProps {
|
||||
onVendorStockFilterChange: (newFilters: string[]) => void;
|
||||
wocFilter: string[];
|
||||
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'];
|
||||
@@ -37,7 +39,8 @@ const ForecastRow: React.FC<{
|
||||
top50Mode: 'eu' | 'uk';
|
||||
stockMap?: Map<string, 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 ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||
|
||||
@@ -105,6 +108,7 @@ const ForecastRow: React.FC<{
|
||||
)}
|
||||
{/* WOC Indicator preserved */}
|
||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} avgWeeklySales={item.avgWeeklySales} />
|
||||
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -174,7 +178,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
onVendorStockFilterChange,
|
||||
wocFilter,
|
||||
onWocFilterChange,
|
||||
top50Mode
|
||||
top50Mode,
|
||||
buyBoxLostMap
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
@@ -504,6 +509,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
||||
top50Mode={top50Mode}
|
||||
stockMap={stockMap}
|
||||
vendorStockMap={vendorStockMap}
|
||||
buyBoxLostMap={buyBoxLostMap}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { StockBadge } from './StockBadge';
|
||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||
import { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||
import { WarehouseIcon, AmazonSmileIcon, CoverageIcon } from './Icons';
|
||||
|
||||
interface WeeklyGridProps {
|
||||
@@ -25,6 +26,8 @@ interface WeeklyGridProps {
|
||||
wocFilter: string[];
|
||||
onWocFilterChange: (newFilters: string[]) => void;
|
||||
velocityMap?: Map<string, number>;
|
||||
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||
top50Mode?: 'eu' | 'uk';
|
||||
}
|
||||
|
||||
type SortConfig = {
|
||||
@@ -58,7 +61,8 @@ const WeeklyRow: React.FC<{
|
||||
customerFilters: string[];
|
||||
vendorStockMap?: Map<string, { eu: number; uk: 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 asin = row.asin.trim().toUpperCase();
|
||||
|
||||
@@ -101,6 +105,7 @@ const WeeklyRow: React.FC<{
|
||||
mode={top50Mode}
|
||||
avgWeeklySales={velocityMap?.get(asin)}
|
||||
/>
|
||||
<BuyBoxWarningBadge asin={asin} buyBoxLostMap={buyBoxLostMap} />
|
||||
</div>
|
||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||
</div>
|
||||
@@ -159,7 +164,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
onWocFilterChange,
|
||||
customerFilters,
|
||||
top50Mode,
|
||||
velocityMap
|
||||
velocityMap,
|
||||
buyBoxLostMap
|
||||
}) => {
|
||||
// Pivot data - memoized
|
||||
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||
@@ -638,6 +644,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
renderGrowth={renderGrowth}
|
||||
customerFilters={customerFilters}
|
||||
velocityMap={velocityMap}
|
||||
buyBoxLostMap={buyBoxLostMap}
|
||||
/>
|
||||
))}
|
||||
{displayCount < sortedRows.length && (
|
||||
|
||||
Reference in New Issue
Block a user