mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:25:23 +02:00
New BSRUnitsCorrelationChart component added to the Vendor tab showing: - Per-marketplace tabs (DE, ES, FR, IT, UK) with market accent colors - Dual-axis combo chart: bars (units sold, right axis) + line (BSR rank, left axis) - Inverted BSR axis by default (lower rank = top) with toggle to flip - Pearson r correlation calculated and displayed with strength interpretation - Wired to existing combinedAdsData (filtered) and filteredBsrData Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
680 lines
28 KiB
TypeScript
680 lines
28 KiB
TypeScript
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 { BSRUnitsCorrelationChart } from './BSRUnitsCorrelationChart';
|
|
|
|
interface VendorDataViewProps {
|
|
bsrData: BSRRecord[];
|
|
asinMetadata?: Map<string, { sku: string; title: string; line: string }>;
|
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
|
combinedSalesData?: CombinedKPIs[];
|
|
}
|
|
|
|
interface ChartPoint {
|
|
label: string;
|
|
sortKey: string;
|
|
[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 = [] }) => {
|
|
const activeMarkets = useMemo(() => {
|
|
const m = new Set(bsrData.map(r => r.market));
|
|
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]);
|
|
|
|
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);
|
|
});
|
|
|
|
const sortedKeys = Array.from(byBucket.keys()).sort();
|
|
|
|
const topBsrChartData: ChartPoint[] = [];
|
|
const detailBsrChartData: ChartPoint[] = [];
|
|
const ratingChartData: ChartPoint[] = [];
|
|
|
|
sortedKeys.forEach(key => {
|
|
const rows = byBucket.get(key)!;
|
|
|
|
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 };
|
|
|
|
activeMarkets.forEach(m => {
|
|
const marketRows = rows.filter(r => r.market === m);
|
|
|
|
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]);
|
|
|
|
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>
|
|
<h2 className="text-xl font-bold text-slate-200">No BSR Data Yet</h2>
|
|
<p className="text-sm text-slate-400 max-w-md">
|
|
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">
|
|
<BuyBoxWarningBadge asin={productInfo.asin} buyBoxLostMap={buyBoxLostMap} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* BSR vs Units Correlation Chart */}
|
|
<BSRUnitsCorrelationChart bsrData={bsrData} combinedSalesData={combinedSalesData} />
|
|
|
|
{/* 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>
|
|
|
|
{/* 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>
|
|
</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;
|