mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 23:05:23 +02:00
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
import MultiSelectDropdown from './MultiSelectDropdown';
|
|
import { BSRRecord } from '../types';
|
|
|
|
const MARKET_COLORS: Record<string, string> = {
|
|
DE: '#3b82f6',
|
|
UK: '#ef4444',
|
|
IT: '#22c55e',
|
|
FR: '#a855f7',
|
|
ES: '#f59e0b',
|
|
};
|
|
|
|
interface VendorDataViewProps {
|
|
bsrData: BSRRecord[];
|
|
}
|
|
|
|
interface WeeklyChartPoint {
|
|
weekLabel: string;
|
|
[key: string]: number | string | null;
|
|
}
|
|
|
|
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
|
|
// Filters
|
|
const [selectedMarkets, setSelectedMarkets] = useState<string[]>([]);
|
|
const [selectedTopCats, setSelectedTopCats] = useState<string[]>([]);
|
|
const [selectedDetailCats, setSelectedDetailCats] = useState<string[]>([]);
|
|
const [asinSearch, setAsinSearch] = useState('');
|
|
|
|
// Extract available filter options from the dataset
|
|
const { availableMarkets, availableTopCats, availableDetailCats } = useMemo(() => {
|
|
const markets = new Set<string>();
|
|
const topCats = new Set<string>();
|
|
const detailCats = new Set<string>();
|
|
|
|
bsrData.forEach(r => {
|
|
if (r.market) markets.add(r.market);
|
|
if (r.topLevelName) topCats.add(r.topLevelName);
|
|
if (r.detailLevelName) detailCats.add(r.detailLevelName);
|
|
});
|
|
|
|
return {
|
|
availableMarkets: Array.from(markets).sort(),
|
|
availableTopCats: Array.from(topCats).sort(),
|
|
availableDetailCats: Array.from(detailCats).sort()
|
|
};
|
|
}, [bsrData]);
|
|
|
|
// Apply filters
|
|
const filteredData = useMemo(() => {
|
|
let result = bsrData;
|
|
|
|
if (selectedMarkets.length > 0) {
|
|
result = result.filter(r => selectedMarkets.includes(r.market));
|
|
}
|
|
if (selectedTopCats.length > 0) {
|
|
result = result.filter(r => r.topLevelName && selectedTopCats.includes(r.topLevelName));
|
|
}
|
|
if (selectedDetailCats.length > 0) {
|
|
result = result.filter(r => r.detailLevelName && selectedDetailCats.includes(r.detailLevelName));
|
|
}
|
|
if (asinSearch.trim()) {
|
|
const terms = asinSearch.toLowerCase().split(',').map(t => t.trim()).filter(Boolean);
|
|
result = result.filter(r => terms.some(t => r.asin.toLowerCase().includes(t)));
|
|
}
|
|
|
|
return result;
|
|
}, [bsrData, selectedMarkets, selectedTopCats, selectedDetailCats, asinSearch]);
|
|
|
|
// Determine active markets in filtered data for series generation
|
|
const activeMarkets = useMemo(() => {
|
|
const m = new Set(filteredData.map(r => r.market));
|
|
return Array.from(m).sort();
|
|
}, [filteredData]);
|
|
|
|
// Aggregate Data for Charts
|
|
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
|
|
const byWeek = new Map<number, BSRRecord[]>();
|
|
filteredData.forEach(r => {
|
|
if (!byWeek.has(r.week)) byWeek.set(r.week, []);
|
|
byWeek.get(r.week)!.push(r);
|
|
});
|
|
|
|
const sortedWeeks = Array.from(byWeek.keys()).sort((a, b) => a - b);
|
|
|
|
const topBsrChartData: WeeklyChartPoint[] = [];
|
|
const detailBsrChartData: WeeklyChartPoint[] = [];
|
|
const ratingChartData: WeeklyChartPoint[] = [];
|
|
|
|
sortedWeeks.forEach(week => {
|
|
const row = byWeek.get(week)!;
|
|
const weekLabel = `Week ${week}`;
|
|
|
|
const topPoint: WeeklyChartPoint = { weekLabel };
|
|
const detailPoint: WeeklyChartPoint = { weekLabel };
|
|
const ratingPoint: WeeklyChartPoint = { weekLabel };
|
|
|
|
activeMarkets.forEach(m => {
|
|
const marketRows = row.filter(r => r.market === m);
|
|
|
|
// Top BSR
|
|
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;
|
|
|
|
// Detail BSR
|
|
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;
|
|
|
|
// Rating
|
|
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 };
|
|
}, [filteredData, activeMarkets]);
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 p-4 pb-24 md:pb-4">
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap gap-3 items-center">
|
|
<MultiSelectDropdown
|
|
label="Market"
|
|
options={availableMarkets}
|
|
selected={selectedMarkets}
|
|
onChange={setSelectedMarkets}
|
|
/>
|
|
<MultiSelectDropdown
|
|
label="Top Level Category"
|
|
options={availableTopCats}
|
|
selected={selectedTopCats}
|
|
onChange={setSelectedTopCats}
|
|
/>
|
|
<MultiSelectDropdown
|
|
label="Detail Level Category"
|
|
options={availableDetailCats}
|
|
selected={selectedDetailCats}
|
|
onChange={setSelectedDetailCats}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Search ASINs (comma-separated)"
|
|
value={asinSearch}
|
|
onChange={e => 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"
|
|
/>
|
|
{(selectedMarkets.length > 0 || selectedTopCats.length > 0 || selectedDetailCats.length > 0 || asinSearch) && (
|
|
<button
|
|
onClick={() => {
|
|
setSelectedMarkets([]);
|
|
setSelectedTopCats([]);
|
|
setSelectedDetailCats([]);
|
|
setAsinSearch('');
|
|
}}
|
|
className="text-xs text-slate-400 hover:text-white underline"
|
|
>
|
|
Clear filters
|
|
</button>
|
|
)}
|
|
<span className="text-slate-500 text-xs ml-auto">{filteredData.length.toLocaleString()} records filtered</span>
|
|
</div>
|
|
|
|
{/* Top Level BSR Trend Chart */}
|
|
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
|
<h3 className="text-lg font-bold text-slate-200 mb-4">Top Level BSR Trend (Weekly Avg)</h3>
|
|
<p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p>
|
|
<ResponsiveContainer width="100%" height={350}>
|
|
<LineChart data={topBsrChartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="weekLabel" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
|
|
<YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
|
labelStyle={{ color: '#e2e8f0' }}
|
|
/>
|
|
<Legend />
|
|
{activeMarkets.map(m => (
|
|
<Line
|
|
key={m}
|
|
type="monotone"
|
|
dataKey={`${m}_bsr`}
|
|
name={`${m} BSR`}
|
|
stroke={MARKET_COLORS[m] || '#6b7280'}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
connectNulls
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
{/* Detail Level BSR Trend Chart */}
|
|
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
|
<h3 className="text-lg font-bold text-slate-200 mb-4">Detail Level BSR Trend (Weekly Avg)</h3>
|
|
<p className="text-xs text-slate-500 mb-3">Lower rank = better position. Averaged across filtered ASINs per market.</p>
|
|
<ResponsiveContainer width="100%" height={350}>
|
|
<LineChart data={detailBsrChartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="weekLabel" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
|
|
<YAxis reversed tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
|
labelStyle={{ color: '#e2e8f0' }}
|
|
/>
|
|
<Legend />
|
|
{activeMarkets.map(m => (
|
|
<Line
|
|
key={m}
|
|
type="monotone"
|
|
dataKey={`${m}_bsr`}
|
|
name={`${m} BSR`}
|
|
stroke={MARKET_COLORS[m] || '#6b7280'}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
connectNulls
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
{/* Average Rating Chart */}
|
|
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
|
<h3 className="text-lg font-bold text-slate-200 mb-4">Average Rating (Weekly Avg)</h3>
|
|
<p className="text-xs text-slate-500 mb-3">Averaged across filtered ASINs per market.</p>
|
|
<ResponsiveContainer width="100%" height={350}>
|
|
<LineChart data={ratingChartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|
<XAxis dataKey="weekLabel" tick={{ fill: '#94a3b8', fontSize: 11 }} interval="preserveStartEnd" />
|
|
<YAxis domain={[0, 5]} tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
|
labelStyle={{ color: '#e2e8f0' }}
|
|
/>
|
|
<Legend />
|
|
{activeMarkets.map(m => (
|
|
<Line
|
|
key={m}
|
|
type="monotone"
|
|
dataKey={`${m}_rating`}
|
|
name={`${m} Rating`}
|
|
stroke={MARKET_COLORS[m] || '#6b7280'}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
connectNulls
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VendorDataView;
|