import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar } from 'recharts'; import MultiSelectDropdown from './MultiSelectDropdown'; import { fetchVendorData, fetchVendorFilterOptions, VendorDailyRow } from '../services/supabase'; const MARKET_COLORS: Record = { DE: '#3b82f6', UK: '#ef4444', IT: '#22c55e', FR: '#a855f7', ES: '#f59e0b', }; interface WeeklyBSR { weekLabel: string; [key: string]: number | string | null; // e.g. DE_bsr, UK_bsr } interface WeeklyRating { weekLabel: string; avg_rating: number | null; num_reviews: number | null; } interface WeeklyBuyBox { weekLabel: string; [key: string]: number | string | null; // e.g. DE_amazon_pct } // Get ISO week from date string function getISOWeek(dateStr: string): string { const d = new Date(dateStr); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7)); const week1 = new Date(d.getFullYear(), 0, 4); const weekNum = 1 + Math.round(((d.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7); return `${d.getFullYear()}-W${String(weekNum).padStart(2, '0')}`; } function aggregateToWeekly(rows: VendorDailyRow[], markets: string[]) { // Group by week const byWeek = new Map(); for (const row of rows) { const wk = getISOWeek(row.date); if (!byWeek.has(wk)) byWeek.set(wk, []); byWeek.get(wk)!.push(row); } const sortedWeeks = [...byWeek.keys()].sort(); // BSR: min rank per week per market (best rank) const bsrData: WeeklyBSR[] = sortedWeeks.map(wk => { const weekRows = byWeek.get(wk)!; const point: WeeklyBSR = { weekLabel: wk.replace(/^\d{4}-/, '') }; for (const m of markets) { const marketRows = weekRows.filter(r => r.market === m && r.bsr_detail_rank != null); if (marketRows.length > 0) { // Average BSR across ASINs for that market+week const avg = marketRows.reduce((s, r) => s + r.bsr_detail_rank!, 0) / marketRows.length; point[`${m}_bsr`] = Math.round(avg); } else { point[`${m}_bsr`] = null; } } return point; }); // Ratings: average across all markets/ASINs per week const ratingData: WeeklyRating[] = sortedWeeks.map(wk => { const weekRows = byWeek.get(wk)!; const withRating = weekRows.filter(r => r.avg_rating != null); const withReviews = weekRows.filter(r => r.num_reviews != null); return { weekLabel: wk.replace(/^\d{4}-/, ''), avg_rating: withRating.length > 0 ? Math.round((withRating.reduce((s, r) => s + r.avg_rating!, 0) / withRating.length) * 10) / 10 : null, num_reviews: withReviews.length > 0 ? Math.round(withReviews.reduce((s, r) => s + r.num_reviews!, 0) / withReviews.length) : null, }; }); // Buy Box: % of rows where Amazon has buybox, per market per week const buyBoxData: WeeklyBuyBox[] = sortedWeeks.map(wk => { const weekRows = byWeek.get(wk)!; const point: WeeklyBuyBox = { weekLabel: wk.replace(/^\d{4}-/, '') }; for (const m of markets) { const marketRows = weekRows.filter(r => r.market === m && r.amazon_has_buybox != null); if (marketRows.length > 0) { const amazonCount = marketRows.filter(r => r.amazon_has_buybox === true).length; point[`${m}_pct`] = Math.round((amazonCount / marketRows.length) * 100); } else { point[`${m}_pct`] = null; } } return point; }); return { bsrData, ratingData, buyBoxData }; } const VendorDataView: React.FC = () => { const [vendorRows, setVendorRows] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Filter options const [availableMarkets, setAvailableMarkets] = useState([]); const [availableTags, setAvailableTags] = useState([]); // Selected filters const [selectedMarkets, setSelectedMarkets] = useState([]); const [selectedTags, setSelectedTags] = useState([]); const [asinSearch, setAsinSearch] = useState(''); // Load filter options on mount useEffect(() => { const loadFilters = async () => { try { const opts = await fetchVendorFilterOptions(); setAvailableMarkets(opts.markets); setAvailableTags(opts.tags); } catch (e: any) { console.error('Failed to load vendor filter options', e); } }; loadFilters(); }, []); // Fetch data when filters change const loadData = useCallback(async () => { setLoading(true); setError(null); try { const data = await fetchVendorData({ markets: selectedMarkets.length > 0 ? selectedMarkets : undefined, tags: selectedTags.length > 0 ? selectedTags : undefined, asins: asinSearch.trim() ? asinSearch.split(',').map(a => a.trim()).filter(Boolean) : undefined, }); setVendorRows(data); } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [selectedMarkets, selectedTags, asinSearch]); useEffect(() => { loadData(); }, [loadData]); // Determine active markets from data const activeMarkets = useMemo(() => { const markets = [...new Set(vendorRows.map(r => r.market))].sort(); return markets.length > 0 ? markets : availableMarkets; }, [vendorRows, availableMarkets]); // Aggregate to weekly const { bsrData, ratingData, buyBoxData } = useMemo( () => aggregateToWeekly(vendorRows, activeMarkets), [vendorRows, activeMarkets] ); // Empty state if (!loading && vendorRows.length === 0 && !error) { return (

No Vendor Data Yet

Upload a Vendor Central daily CSV to see BSR trends, ratings, and Buy Box data. Use the data source settings (top-right) to upload.

); } return (
{/* Filters */}
setAsinSearch(e.target.value)} className="bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 w-64" /> {loading && (
Loading...
)} {error && Error: {error}} {vendorRows.length.toLocaleString()} daily records
{/* BSR Trend Chart */}

BSR Detail Category Rank (Weekly Avg)

Lower rank = better position. Averaged across filtered ASINs per market.

{activeMarkets.map(m => ( ))}
{/* Ratings & Reviews Chart */}

Average Rating & Reviews (Weekly)

Averaged across all filtered ASINs and markets.

{/* Buy Box Chart */}

Amazon Buy Box Ownership (Weekly %)

Percentage of daily records where Amazon holds the Buy Box, per market.

`${v}%`} /> `${value}%`} /> {activeMarkets.map(m => ( ))}
); }; export default VendorDataView;