Files
CrazeAnalytix/components/VendorDataView.tsx
T

722 lines
29 KiB
TypeScript
Raw Normal View History

import React, { useMemo } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { BSRRecord, CombinedKPIs } from '../types';
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
import { StockBadge } from './StockBadge';
import { VendorStockBadge } from './VendorStockBadge';
import { Top50Badge } from './Top50Badge';
import { BSRUnitsCorrelationChart } from './BSRUnitsCorrelationChart';
2026-03-02 15:22:12 +01:00
interface VendorDataViewProps {
bsrData: BSRRecord[];
asinMetadata?: Map<string, { sku: string; title: string; line: string }>;
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
combinedSalesData?: CombinedKPIs[];
stockMap?: Map<string, number>;
vendorStockMap?: Map<string, { eu: number; uk: number }>;
top50Mode?: 'eu' | 'uk';
velocityMap?: Map<string, number>;
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
2026-03-02 15:22:12 +01:00
}
interface ChartPoint {
label: string;
sortKey: string;
2026-03-02 15:22:12 +01:00
[key: string]: number | string | null;
}
const CATEGORY_TRANSLATIONS: Record<string, string> = {
// === TOP-LEVEL (DE) ===
'Spielzeug': 'Toys & Games',
'Küche, Haushalt & Wohnen': 'Kitchen & Home',
'Baby': 'Baby',
'Bürobedarf & Schreibwaren': 'Stationery & Office Supplies',
'Baumarkt': 'DIY & Tools',
'Drogerie & Körperpflage': 'Health & Beauty',
'Drogerie & Körperpflege': 'Health & Beauty',
'Garten': 'Garden',
'Kosmetik': 'Beauty',
'Musik-CDs & Vinyl': 'Music CDs & Vinyl',
'Bücher': 'Books',
'Küche & Haushalt': 'Kitchen & Home',
// === TOP-LEVEL (FR) ===
'Jeux et Jouets': 'Toys & Games',
'Bébé et Puériculture': 'Baby',
'Beauté et Parfum': 'Beauty',
'Cuisine et Maison': 'Kitchen & Home',
'Fournitures de bureau': 'Office Supplies',
'Hygiène et Santé': 'Health & Hygiene',
'Jeux vidéo': 'Video Games',
'Mode': 'Fashion',
'Livres': 'Books',
// === TOP-LEVEL (IT) ===
'Giochi e giocattoli': 'Toys & Games',
'Prima infanzia': 'Baby',
'Bellezza': 'Beauty',
'Casa e cucina': 'Kitchen & Home',
'Cancelleria e prodotti per ufficio': 'Stationery & Office Supplies',
'Videogiochi': 'Video Games',
'Moda': 'Fashion',
'Informatica': 'Computers',
'Salute e cura della persona': 'Health & Personal Care',
'CD e Vinili': 'Music CDs & Vinyl',
'Altro': 'Other',
// === TOP-LEVEL (ES) ===
'Juguetes y juegos': 'Toys & Games',
'Bebé': 'Baby',
'Belleza': 'Beauty',
'Hogar y cocina': 'Kitchen & Home',
'Oficina y papelería': 'Office Supplies',
'Deportes y aire libre': 'Sports & Outdoors',
'Salud y cuidado personal': 'Health & Personal Care',
// === DETAIL (DE) ===
'Badebomben': 'Bath Bombs',
'Badesalz': 'Bath Salts',
'Badesalze': 'Bath Salts',
'Badezusätze': 'Bath Additives',
'Badewannenspielzeug': 'Bath Toys',
'Adventskalender': 'Advent Calendars',
'Dekorative Adventskalender': 'Decorative Advent Calendars',
'Kinder-Bastelsets': 'Kids Craft Sets',
'Kinderknete': 'Kids Clay & Dough',
'Basteln, Malen & Handarbeiten': 'Arts, Crafts & Sewing',
'Experimentierkästen': 'Science Kits',
'Puzzles': 'Puzzles',
'Klassische Puzzles': 'Classic Puzzles',
'Brettspiele': 'Board Games',
'Spielzeugfiguren': 'Toy Figures',
'Plüsch-Spielzeug': 'Plush Toys',
'Stofftiere & Teddybären': 'Stuffed Animals & Teddy Bears',
'Puppen': 'Dolls',
'Figuren': 'Figures',
'Actionfiguren': 'Action Figures',
'Wasserpistolen': 'Water Guns',
'Schaumstoff-Blaster': 'Foam Blasters',
'Kreisel': 'Spinning Tops',
'Handkreisel': 'Hand Spinners',
'Haargummis': 'Hair Ties',
'Haarclips': 'Hair Clips',
'Haarspangen': 'Hair Clips',
'Haarschmuck': 'Hair Accessories',
'Schmuck-Sets': 'Jewellery Sets',
'Schmucksets': 'Jewellery Sets',
'Armbänder': 'Bracelets',
'Ohrringe': 'Earrings',
'Gummibänder & -ringe': 'Rubber Bands',
'Bügelperlen': 'Fuse Beads',
'Perlen': 'Beads',
'Malen nach Zahlen': 'Paint by Numbers',
'Malsets': 'Painting Sets',
'Mosaik': 'Mosaic',
'Kinder Schmuckkästchen': 'Kids Jewellery Boxes',
'Slime & Putty Spielzeuge': 'Slime & Putty Toys',
'Pop Fidget-Spielzeug': 'Pop Fidget Toys',
'Gesichtsfarben': 'Face Paints',
'Schminke': 'Makeup',
'Masken für Erwachsene': 'Adult Masks',
'Tattoos': 'Tattoos',
'Tiere': 'Animals',
'Dinosaurier & prähistorische Kreaturen': 'Dinosaurs & Prehistoric Creatures',
'Fantastische Kreaturen': 'Fantasy Creatures',
'Spielsets': 'Play Sets',
'Reise- & Kompaktspiele': 'Travel & Compact Games',
'Party- & Scherzartikel': 'Party & Novelty Items',
'Partygeschenk-Packs mit verschiedenen Artikeln': 'Party Gift Packs',
'Lernuhren': 'Teaching Clocks',
'Grußkarten': 'Greeting Cards',
'Saisonale Deko': 'Seasonal Decorations',
'Werkzeuge & Zubehör': 'Tools & Accessories',
'Sticker': 'Stickers',
'Papeterie & Sticker': 'Stationery & Stickers',
'Drucken & Stempeln': 'Printing & Stamping',
'Zeichnen': 'Drawing',
'Wachsmalstifte': 'Wax Crayons',
'Zierschmuck': 'Decorative Jewellery',
'Armband-Packs': 'Bracelet Packs',
'Bausätze für Diamantmalerei': 'Diamond Painting Kits',
'Schlüsselringe & Schlüsselbänder für Damen': 'Key Rings & Lanyards',
'Geschenktaschen': 'Gift Bags',
'Sortier- & Stapelspielzeug': 'Sorting & Stacking Toys',
'Lernspiele': 'Educational Games',
'Spieluhren': 'Music Boxes',
'Trinkspiele': 'Drinking Games',
// === DETAIL (FR) ===
'Boules effervescentes': 'Bath Bombs',
'Sels de bain': 'Bath Salts',
'Bains moussants': 'Bubble Baths',
'Produits de bain': 'Bath Products',
'Jouets de bain': 'Bath Toys',
'Calendriers de l\'Avent': 'Advent Calendars',
'Jouets anti-stress': 'Stress Relief Toys',
'Poupées': 'Dolls',
'Figurines d\'action': 'Action Figures',
'Figurines étirables': 'Stretchy Figures',
'Animaux en peluche': 'Stuffed Animals',
'Peluches': 'Plush Toys',
'Jeux de plateau': 'Board Games',
'Puzzles classiques': 'Classic Puzzles',
'Bracelets': 'Bracelets',
'Boucles d\'oreilles': 'Earrings',
'Bijoux et perles': 'Jewellery & Beads',
'Perles à repasser': 'Fuse Beads',
'Kits de broderie diamant': 'Diamond Painting Kits',
'Kits pour bijoux': 'Jewellery Making Kits',
'Slime et mastic': 'Slime & Putty',
'Toupies': 'Spinning Tops',
'Toupies d\'éveil': 'Baby Spinning Tops',
'Pistolets': 'Toy Guns',
'Pistolets et fusils à eau': 'Water Guns',
'Animaux': 'Animals',
'Créatures fantastiques': 'Fantasy Creatures',
'Dinosaures': 'Dinosaurs',
'Décorations de cheveux': 'Hair Accessories',
'Barrettes': 'Hair Clips',
'Pinces': 'Hair Clips',
'Élastiques': 'Elastic Bands',
'Sacs cadeaux': 'Gift Bags',
'Coffrets cadeaux': 'Gift Sets',
'Masques': 'Masks',
'Tampons à imprimer': 'Printing Stamps',
'Maisons de poupées': 'Dollhouses',
'Boîtes à bijoux pour enfants': 'Kids Jewellery Boxes',
'Boîtes à musique et figurines': 'Music Boxes & Figurines',
'Parure de bijoux': 'Jewellery Sets',
'Parures': 'Jewellery Sets',
'Ensembles': 'Sets',
'Coffrets': 'Gift Boxes',
'Pochettes-surprises': 'Mystery Bags',
// === DETAIL (IT) ===
'Bombe da bagno': 'Bath Bombs',
'Sali da bagno': 'Bath Salts',
'Schiuma da bagno': 'Bubble Bath',
'Prodotti per il bagno': 'Bath Products',
'Set di accessori bagno': 'Bath Accessory Sets',
'Giochi per il bagnetto': 'Bath Toys',
'Calendari dell\'avvento': 'Advent Calendars',
'Giochi di società': 'Board Games',
'Puzzle classici': 'Classic Puzzles',
'Bambole': 'Dolls',
'Set di bambole e accessori': 'Doll & Accessory Sets',
'Animali di peluche': 'Stuffed Animals',
'Animali': 'Animals',
'Creature fantastiche': 'Fantasy Creatures',
'Dinosauri e creature preistoriche': 'Dinosaurs & Prehistoric Creatures',
'Personaggi d\'azione': 'Action Figures',
'Personaggi giocattolo': 'Toy Characters',
'Personaggi di gomma': 'Rubber Characters',
'Argilla e pasta modellabile': 'Clay & Modelling Dough',
'Lavoretti con la sabbia': 'Sand Art',
'Kit per pittura diamante': 'Diamond Painting Kits',
'Kit per creazione gioielli': 'Jewellery Making Kits',
'Perline a fusione': 'Fuse Beads',
'Mosaici': 'Mosaics',
'Braccialetti': 'Bracelets',
'Orecchini': 'Earrings',
'Gioielli': 'Jewellery',
'Portagioie per bambini': 'Kids Jewellery Boxes',
'Mollette': 'Hair Clips',
'Mollette per capelli': 'Hair Clips',
'Decorazioni per capelli': 'Hair Accessories',
'Unghie finte': 'Fake Nails',
'Unghie a pressione': 'Press-on Nails',
'Set trucco e gioielli': 'Makeup & Jewellery Sets',
'Parure di gioielli': 'Jewellery Sets',
'Elastici': 'Elastic Bands',
'Slime': 'Slime',
'Giochi da schiacciare': 'Squeeze Toys',
'Pop fidget toys antistress': 'Pop Fidget Toys',
'Cubi fidget': 'Fidget Cubes',
'Fidget toys semplici': 'Simple Fidget Toys',
'Trottole': 'Spinning Tops',
'Trottole da combattimento': 'Battle Spinning Tops',
'Pistole d\'acqua': 'Water Guns',
'Pistole in gommapiuma': 'Foam Blasters',
'Giocattoli prima infanzia': 'Early Childhood Toys',
'Giochi per attività motorie': 'Motor Activity Toys',
'Giochi': 'Games',
'Giochi alcolici': 'Drinking Games',
'Stampini e timbri': 'Stamps',
'Maschere': 'Masks',
'Miniature': 'Miniatures',
'Carillon e carillon con statuetta': 'Music Boxes',
'Articoli per giochi di magia': 'Magic Trick Items',
'Sabbiere e giocattoli da spiaggia': 'Sand & Beach Toys',
'Uova di Pasqua giocattolo': 'Toy Easter Eggs',
// === DETAIL (ES) ===
'Bombas de baño': 'Bath Bombs',
'Sales de baño': 'Bath Salts',
'Aditivos para el baño': 'Bath Additives',
'Aceite perfumado': 'Perfumed Oil',
'Aceites de masaje': 'Massage Oils',
'Juguetes de baño': 'Bath Toys',
'Calendarios de adviento': 'Advent Calendars',
'Calendarios de pared': 'Wall Calendars',
'Juegos de estrategia': 'Strategy Games',
'Juegos de misterio': 'Mystery Games',
'Juegos de cartas con baraja específica': 'Specific Card Games',
'Juegos de bebida': 'Drinking Games',
'Juguetes antiestrés': 'Stress Relief Toys',
'Muñecas': 'Dolls',
'Muñecas y accesorios': 'Dolls & Accessories',
'Muñecos y figuras de acción': 'Action Figures',
'Muñecos cabezones': 'Bobblehead Figures',
'Animales de peluche': 'Stuffed Animals',
'Animales': 'Animals',
'Animales y figuras': 'Animals & Figures',
'Criaturas fantásticas': 'Fantasy Creatures',
'Dinosaurios y criaturas prehistóricas': 'Dinosaurs & Prehistoric Creatures',
'Personas': 'People',
'Arcilla y plastilina': 'Clay & Modelling Dough',
'Manualidades con arena': 'Sand Art',
'Arte y manualidades': 'Arts & Crafts',
'Kits para pintura con diamantes': 'Diamond Painting Kits',
'Kits para hacer bisutería': 'Jewellery Making Kits',
'Cuentas para planchar': 'Fuse Beads',
'Mosaicos': 'Mosaics',
'Pulseras': 'Bracelets',
'Pendientes': 'Earrings',
'Joyería': 'Jewellery',
'Joyeros para niños': 'Kids Jewellery Boxes',
'Clips de pelo': 'Hair Clips',
'Horquillas': 'Hair Pins',
'Adornos para el pelo': 'Hair Accessories',
'Uñas adhesivas': 'Press-on Nails',
'Puntas de uñas': 'Nail Tips',
'Sets de uñas': 'Nail Sets',
'Conjuntos de maquillaje y bisutería': 'Makeup & Jewellery Sets',
'Conjuntos': 'Sets',
'Sets de accesorios': 'Accessory Sets',
'Gomas elásticas': 'Elastic Bands',
'Pegatinas': 'Stickers',
'Masas viscosas': 'Slime',
'Peonzas': 'Spinning Tops',
'Peonzas de batalla': 'Battle Spinning Tops',
'Novedades en peonzas': 'Novelty Spinning Tops',
'Pistolas de agua': 'Water Guns',
'Lanzadores de espuma': 'Foam Blasters',
'Grabados y sellos': 'Printing & Stamping',
'Pintura': 'Painting',
'Mascarillas': 'Masks/Face Masks',
'Cromos coleccionables': 'Collectible Cards',
'Pack de artículos para fiestas': 'Party Item Packs',
'Paquetes multielementos de cotillones': 'Multi-item Party Packs',
'Bolsas para regalo': 'Gift Bags',
'Decoración original': 'Original Decorations',
'Llaveros': 'Key Rings',
'Varitas': 'Wands',
'Relojes de aprendizaje': 'Teaching Clocks',
'Patines': 'Skates',
'Monopatines': 'Skateboards',
'Gafas': 'Glasses',
'Miniaturas': 'Miniatures',
};
const translateCategory = (name: string): string =>
CATEGORY_TRANSLATIONS[name] ?? name;
const VendorDataView: React.FC<VendorDataViewProps> = ({
bsrData = [],
asinMetadata,
buyBoxLostMap,
combinedSalesData = [],
stockMap,
vendorStockMap,
top50Mode = 'eu',
velocityMap,
top50Ranking
}) => {
const activeMarkets = useMemo(() => {
const m = new Set(bsrData.map(r => r.market));
2026-03-02 15:22:12 +01:00
return Array.from(m).sort();
}, [bsrData]);
const useDailyResolution = useMemo(() => {
const withDate = bsrData.filter(r => r.date).length;
return withDate > bsrData.length / 2;
}, [bsrData]);
const uniqueAsins = useMemo(() => {
const asinSet = new Set(bsrData.map(r => r.asin.trim().toUpperCase()));
return Array.from(asinSet);
}, [bsrData]);
const productInfo = useMemo(() => {
if (uniqueAsins.length !== 1) return null;
const asin = uniqueAsins[0];
const metadata = asinMetadata?.get(asin);
if (!metadata) return null;
let topCategory = '';
let detailCategory = '';
for (const r of bsrData) {
if (!topCategory && r.topLevelName) topCategory = r.topLevelName;
if (!detailCategory && r.detailLevelName) detailCategory = r.detailLevelName;
if (topCategory && detailCategory) break;
}
return {
asin,
sku: metadata.sku,
title: metadata.title,
line: metadata.line,
topCategory,
detailCategory
};
}, [uniqueAsins, asinMetadata, bsrData]);
2026-03-02 15:22:12 +01:00
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
const byBucket = new Map<string, BSRRecord[]>();
bsrData.forEach(r => {
const key = useDailyResolution && r.date ? r.date : `W${String(r.week).padStart(2, '0')}`;
if (!byBucket.has(key)) byBucket.set(key, []);
byBucket.get(key)!.push(r);
2026-03-02 15:22:12 +01:00
});
const sortedKeys = Array.from(byBucket.keys()).sort();
2026-03-02 15:22:12 +01:00
const topBsrChartData: ChartPoint[] = [];
const detailBsrChartData: ChartPoint[] = [];
const ratingChartData: ChartPoint[] = [];
2026-03-02 15:22:12 +01:00
sortedKeys.forEach(key => {
const rows = byBucket.get(key)!;
2026-03-02 15:22:12 +01:00
let label: string;
if (useDailyResolution && key.includes('-')) {
const d = new Date(key + 'T12:00:00Z');
label = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
} else {
label = key;
}
const topPoint: ChartPoint = { label, sortKey: key };
const detailPoint: ChartPoint = { label, sortKey: key };
const ratingPoint: ChartPoint = { label, sortKey: key };
2026-03-02 15:22:12 +01:00
activeMarkets.forEach(m => {
const marketRows = rows.filter(r => r.market === m);
2026-03-02 15:22:12 +01:00
const topBsrRows = marketRows.filter(r => r.topLevelBSR != null);
topPoint[`${m}_bsr`] = topBsrRows.length > 0
? Math.round(topBsrRows.reduce((sum, r) => sum + r.topLevelBSR!, 0) / topBsrRows.length)
: null;
const detailBsrRows = marketRows.filter(r => r.detailLevelBSR != null);
detailPoint[`${m}_bsr`] = detailBsrRows.length > 0
? Math.round(detailBsrRows.reduce((sum, r) => sum + r.detailLevelBSR!, 0) / detailBsrRows.length)
: null;
const ratingRows = marketRows.filter(r => r.avgRating != null);
ratingPoint[`${m}_rating`] = ratingRows.length > 0
? Math.round((ratingRows.reduce((sum, r) => sum + r.avgRating!, 0) / ratingRows.length) * 10) / 10
: null;
});
topBsrChartData.push(topPoint);
detailBsrChartData.push(detailPoint);
ratingChartData.push(ratingPoint);
});
return { topBsrChartData, detailBsrChartData, ratingChartData };
}, [bsrData, activeMarkets, useDailyResolution]);
2026-03-02 15:22:12 +01:00
if (bsrData.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-[60vh] gap-4 text-center px-4">
<div className="p-4 bg-emerald-500/10 rounded-full text-emerald-400">
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
</svg>
</div>
2026-03-02 15:22:12 +01:00
<h2 className="text-xl font-bold text-slate-200">No BSR Data Yet</h2>
<p className="text-sm text-slate-400 max-w-md">
2026-03-02 15:22:12 +01:00
Ensure BSR.xlsx is present and loading to see Top Level BSR, Detail Level BSR, and Average Rating trends.
</p>
</div>
);
}
const cardBg = '#161b24';
const gridLineColor = '#ffffff';
// One distinct color per market
const MARKET_COLORS: Record<string, string> = {
'Amazon DE': '#f37526',
'Amazon ES': '#2acbd6',
'Amazon FR': '#7950f2',
'Amazon IT': '#10b981',
'Amazon UK': '#f59e0b',
};
const FALLBACK_COLORS = ['#f37526', '#2acbd6', '#7950f2', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
const getMarketColor = (m: string, idx: number) => MARKET_COLORS[m] ?? FALLBACK_COLORS[idx % FALLBACK_COLORS.length];
return (
<div className="p-4 md:p-6 lg:px-8 pb-24 max-w-[800px] mx-auto space-y-6">
{/* Product Details Header Card */}
{productInfo && (
<div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md relative">
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-3 flex-1">
<div>
<span className="inline-block bg-[#1f2b4a] text-[#4f86f7] text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-[4px] mb-3">
PRODUCT DETAILS
</span>
<h2 className="text-xl md:text-2xl font-bold text-white mb-1">
{productInfo.title}
</h2>
</div>
<div className="flex items-center gap-6 mt-1">
<div className="text-[13px] md:text-sm">
<span className="text-[#64748b] font-semibold mr-2">SKU:</span>
<span className="text-[#e2e8f0] font-medium">{productInfo.sku}</span>
</div>
<div className="text-[13px] md:text-sm">
<span className="text-[#64748b] font-semibold mr-2">ASIN:</span>
<span className="text-[#4f86f7] font-medium tracking-wide">{productInfo.asin}</span>
</div>
</div>
</div>
<div className="flex-shrink-0 pt-1 flex flex-col items-end gap-3">
<div className="flex items-center gap-3">
{stockMap && (
<StockBadge stock={stockMap.get(productInfo.sku?.replace(/(DE|EN)$/i, ''))} />
)}
{(() => {
const asin = productInfo.asin.trim().toUpperCase();
if (asin && top50Ranking) {
if (top50Mode === 'eu') {
const rank = top50Ranking.eu.get(asin);
if (rank) return <Top50Badge rank={rank} label="EU" theme="indigo" />;
} else {
const rank = top50Ranking.uk.get(asin);
if (rank) return <Top50Badge rank={rank} label="UK" theme="blue" />;
}
}
return null;
})()}
<VendorStockBadge
asin={productInfo.asin}
vendorStockMap={vendorStockMap}
mode={top50Mode}
avgWeeklySales={velocityMap?.get(productInfo.asin.trim().toUpperCase())}
/>
<BuyBoxWarningBadge asin={productInfo.asin} buyBoxLostMap={buyBoxLostMap} />
</div>
</div>
</div>
</div>
)}
{/* BSR vs Units Correlation Chart */}
<BSRUnitsCorrelationChart bsrData={bsrData} combinedSalesData={combinedSalesData} />
2026-03-02 15:22:12 +01:00
{/* Top Level BSR Trend Chart */}
<div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col">
<div className="mb-4">
<div className="flex flex-wrap items-center gap-3 mb-1">
<h3 className="text-[17px] font-semibold text-white tracking-wide">Top Level BSR Trend</h3>
{productInfo?.topCategory && (
<span className="text-[11px] font-medium text-[#e2e8f0] bg-[#334155]/60 px-2.5 py-0.5 rounded border border-[#475569]/50 tracking-wider">
{translateCategory(productInfo.topCategory)}
</span>
)}
</div>
<p className="text-[13px] text-[#64748b]">Weekly Average Rank Lower is better</p>
</div>
<div className="h-[220px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={topBsrChartData} margin={{ top: 20, right: 20, left: 10, bottom: 5 }}>
<CartesianGrid strokeDasharray="0" stroke={gridLineColor} strokeOpacity={0.03} vertical={false} />
<XAxis
dataKey="label"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
dy={10}
/>
<YAxis
reversed
domain={['auto', 'auto']}
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
tickFormatter={(v: number) => v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1).replace(/\.0$/, '')}M` : v >= 1_000 ? `${Math.round(v / 1_000)}K` : String(v)}
width={45}
/>
<Tooltip
contentStyle={{ backgroundColor: '#111827', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#9ca3af', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#374151', strokeWidth: 1, strokeDasharray: '3 3' }}
/>
{activeMarkets.map((m, i) => {
const color = getMarketColor(m, i);
return (
<Area
key={m}
type="monotone"
dataKey={`${m}_bsr`}
name={`${m} BSR`}
stroke={color}
strokeWidth={2.5}
fillOpacity={1}
fill="transparent"
activeDot={{ r: 6, strokeWidth: 0, fill: color }}
dot={{ r: 4, fill: cardBg, strokeWidth: 2, stroke: color }}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-6 flex justify-center items-center gap-3 flex-wrap">
{activeMarkets.map((m, i) => (
<div key={m} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: getMarketColor(m, i) }}></div>
<span className="text-[12px] text-[#8b9bb4]">{m} BSR</span>
</div>
))}
</div>
</div>
2026-03-02 15:22:12 +01:00
{/* Detail Level BSR Trend Chart */}
<div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col">
<div className="mb-4">
<div className="flex flex-wrap items-center gap-3 mb-1">
<h3 className="text-[17px] font-semibold text-white tracking-wide">Detail Level BSR Trend</h3>
{productInfo?.detailCategory && (
<span className="text-[11px] font-medium text-[#e2e8f0] bg-[#334155]/60 px-2.5 py-0.5 rounded border border-[#475569]/50 tracking-wider">
{translateCategory(productInfo.detailCategory)}
</span>
)}
</div>
<p className="text-[13px] text-[#64748b]">Sub-category Performance Filtered ASINs</p>
</div>
<div className="h-[220px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={detailBsrChartData} margin={{ top: 20, right: 20, left: 10, bottom: 5 }}>
<CartesianGrid strokeDasharray="0" stroke={gridLineColor} strokeOpacity={0.03} vertical={false} />
<XAxis
dataKey="label"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
dy={10}
/>
<YAxis
reversed
domain={['auto', 'auto']}
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
tickFormatter={(v: number) => v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1).replace(/\.0$/, '')}M` : v >= 1_000 ? `${Math.round(v / 1_000)}K` : String(v)}
width={45}
/>
<Tooltip
contentStyle={{ backgroundColor: '#111827', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#9ca3af', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#374151', strokeWidth: 1, strokeDasharray: '3 3' }}
/>
{activeMarkets.map((m, i) => {
const color = getMarketColor(m, i);
return (
<Area
key={m}
type="monotone"
dataKey={`${m}_bsr`}
name={`${m} Rank`}
stroke={color}
strokeWidth={2.5}
fillOpacity={1}
fill="transparent"
activeDot={{ r: 6, strokeWidth: 0, fill: color }}
dot={{ r: 4, fill: cardBg, strokeWidth: 2, stroke: color }}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-6 flex justify-center items-center gap-3 flex-wrap">
{activeMarkets.map((m, i) => (
<div key={m} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: getMarketColor(m, i) }}></div>
<span className="text-[12px] text-[#8b9bb4]">{m} Rank</span>
</div>
))}
</div>
2026-03-02 15:22:12 +01:00
</div>
{/* Average Rating Chart */}
<div className="bg-[#161b24] border border-[#2a3040] rounded-2xl p-6 shadow-md flex flex-col">
<div className="mb-4">
<h3 className="text-[17px] font-semibold text-white tracking-wide mb-1">Average Rating</h3>
<p className="text-[13px] text-[#64748b]">Weekly Average (1.0 - 5.0)</p>
</div>
<div className="h-[220px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={ratingChartData} margin={{ top: 20, right: 20, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="0" stroke={gridLineColor} strokeOpacity={0.03} vertical={false} />
<XAxis
dataKey="label"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
dy={10}
/>
<YAxis
domain={[0, 5]}
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 11, fontWeight: 500 }}
dx={-10}
/>
<Tooltip
contentStyle={{ backgroundColor: '#111827', border: '1px solid #374151', borderRadius: '8px', color: '#fff' }}
itemStyle={{ fontSize: '12px' }}
labelStyle={{ color: '#9ca3af', fontSize: '11px', marginBottom: '4px' }}
cursor={{ stroke: '#374151', strokeWidth: 1, strokeDasharray: '3 3' }}
/>
{activeMarkets.map((m, i) => (
<Area
key={m}
type="monotone"
dataKey={`${m}_rating`}
name={m}
stroke={getMarketColor(m, i)}
strokeWidth={2.5}
fillOpacity={1}
fill="transparent"
activeDot={{ r: 6, strokeWidth: 0, fill: getMarketColor(m, i) }}
dot={{ r: 4, fill: cardBg, strokeWidth: 2, stroke: getMarketColor(m, i) }}
/>
))}
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-6 flex justify-center items-center gap-3 flex-wrap">
{activeMarkets.map((m, i) => (
<div key={m} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: getMarketColor(m, i) }}></div>
<span className="text-[12px] text-[#8b9bb4]">{m} Rating</span>
</div>
))}
</div>
</div>
</div>
);
};
export default VendorDataView;