mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:45:23 +02:00
feat: sync vendor tab filters with global header filters
This commit is contained in:
@@ -5,7 +5,7 @@ import Dashboard from './components/Dashboard';
|
||||
import FilterBar from './components/FilterBar';
|
||||
import AIChat from './components/AIChat';
|
||||
import CrazeLogo from './components/CrazeLogo';
|
||||
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processVendorCSV, processBSRExcel } from './services/dataProcessor';
|
||||
import { processCSV, processExcel, filterData, aggregateData, processAdsCSV, processAdsExcel, mergeSalesAndAdsData, processTrafficExcel, processStockExcel, filterAdsData, calculateForecastViewData, processVendorStockExcel, processUKInventoryExcel, calculateVelocityMap, getUniqueValues, processForecastExcel, processBuyBoxExcel, processVendorCSV, processBSRExcel, filterBsrData } from './services/dataProcessor';
|
||||
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData, ActiveExperiment, BSRRecord } from './types';
|
||||
import { queryGemini } from './services/geminiService';
|
||||
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
||||
@@ -667,6 +667,11 @@ const App: React.FC = () => {
|
||||
return aggregateData(ytdFilteredData);
|
||||
}, [ytdFilteredData]);
|
||||
|
||||
// Derived Data for Vendor (BSR)
|
||||
const filteredBsrData = useMemo(() => {
|
||||
return filterBsrData(bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode);
|
||||
}, [bsrData, filters, globalAsinMetadata, stockMap, vendorStockMap, top50Mode]);
|
||||
|
||||
// Derived Data for Views
|
||||
const years = useMemo(() => getUniqueValues(rawData, 'year').sort().reverse(), [rawData]);
|
||||
|
||||
@@ -959,7 +964,7 @@ const App: React.FC = () => {
|
||||
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<div className={view === 'vendor' ? '' : 'hidden'}>
|
||||
<VendorDataView bsrData={bsrData} />
|
||||
<VendorDataView bsrData={filteredBsrData} />
|
||||
</div>
|
||||
</Suspense>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { 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> = {
|
||||
@@ -21,73 +20,25 @@ interface ChartPoint {
|
||||
[key: string]: number | string | null;
|
||||
}
|
||||
|
||||
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
|
||||
// Filters
|
||||
const [selectedMarkets, setSelectedMarkets] = useState<string[]>([]);
|
||||
const [selectedAsins, setSelectedAsins] = useState<string[]>([]);
|
||||
const [selectedTopCats, setSelectedTopCats] = useState<string[]>([]);
|
||||
const [selectedDetailCats, setSelectedDetailCats] = useState<string[]>([]);
|
||||
|
||||
// Extract available filter options from the dataset
|
||||
const { availableMarkets, availableAsins, availableTopCats, availableDetailCats } = useMemo(() => {
|
||||
const markets = new Set<string>();
|
||||
const asins = 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.asin) asins.add(r.asin);
|
||||
if (r.topLevelName) topCats.add(r.topLevelName);
|
||||
if (r.detailLevelName) detailCats.add(r.detailLevelName);
|
||||
});
|
||||
|
||||
return {
|
||||
availableMarkets: Array.from(markets).sort(),
|
||||
availableAsins: Array.from(asins).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 (selectedAsins.length > 0) {
|
||||
result = result.filter(r => selectedAsins.includes(r.asin));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [bsrData, selectedMarkets, selectedAsins, selectedTopCats, selectedDetailCats]);
|
||||
|
||||
const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData = [] }) => {
|
||||
// Determine active markets in filtered data for series generation
|
||||
const activeMarkets = useMemo(() => {
|
||||
const m = new Set(filteredData.map(r => r.market));
|
||||
const m = new Set(bsrData.map(r => r.market));
|
||||
return Array.from(m).sort();
|
||||
}, [filteredData]);
|
||||
}, [bsrData]);
|
||||
|
||||
// Determine if daily resolution is available (>= half the records have a date)
|
||||
const useDailyResolution = useMemo(() => {
|
||||
const withDate = filteredData.filter(r => r.date).length;
|
||||
return withDate > filteredData.length / 2;
|
||||
}, [filteredData]);
|
||||
const withDate = bsrData.filter(r => r.date).length;
|
||||
return withDate > bsrData.length / 2;
|
||||
}, [bsrData]);
|
||||
|
||||
// Aggregate Data for Charts — daily if dates available, else weekly
|
||||
const { topBsrChartData, detailBsrChartData, ratingChartData } = useMemo(() => {
|
||||
// Group by date string (YYYY-MM-DD) or by week number
|
||||
const byBucket = new Map<string, BSRRecord[]>();
|
||||
|
||||
filteredData.forEach(r => {
|
||||
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);
|
||||
@@ -144,7 +95,7 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
|
||||
});
|
||||
|
||||
return { topBsrChartData, detailBsrChartData, ratingChartData };
|
||||
}, [filteredData, activeMarkets, useDailyResolution]);
|
||||
}, [bsrData, activeMarkets, useDailyResolution]);
|
||||
|
||||
if (bsrData.length === 0) {
|
||||
return (
|
||||
@@ -166,46 +117,8 @@ const VendorDataView: React.FC<VendorDataViewProps> = ({ bsrData }) => {
|
||||
|
||||
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="ASIN"
|
||||
options={availableAsins}
|
||||
selected={selectedAsins}
|
||||
onChange={setSelectedAsins}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
label="Top Level Category"
|
||||
options={availableTopCats}
|
||||
selected={selectedTopCats}
|
||||
onChange={setSelectedTopCats}
|
||||
/>
|
||||
<MultiSelectDropdown
|
||||
label="Detail Level Category"
|
||||
options={availableDetailCats}
|
||||
selected={selectedDetailCats}
|
||||
onChange={setSelectedDetailCats}
|
||||
/>
|
||||
{(selectedMarkets.length > 0 || selectedAsins.length > 0 || selectedTopCats.length > 0 || selectedDetailCats.length > 0) && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedMarkets([]);
|
||||
setSelectedAsins([]);
|
||||
setSelectedTopCats([]);
|
||||
setSelectedDetailCats([]);
|
||||
}}
|
||||
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>
|
||||
<span className="text-slate-500 text-xs ml-auto">{bsrData.length.toLocaleString()} records filtered</span>
|
||||
</div>
|
||||
|
||||
{/* Top Level BSR Trend Chart */}
|
||||
|
||||
@@ -1152,6 +1152,77 @@ export const filterData = (
|
||||
});
|
||||
};
|
||||
|
||||
export const filterBsrData = (
|
||||
bsrData: BSRRecord[],
|
||||
filters: FilterState,
|
||||
asinMetadata?: Map<string, { sku: string; title: string; line: string }>,
|
||||
stockMap?: Map<string, number>,
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>,
|
||||
top50Mode: 'eu' | 'uk' = 'eu'
|
||||
): BSRRecord[] => {
|
||||
return bsrData.filter(record => {
|
||||
const asin = record.asin.trim().toUpperCase();
|
||||
const meta = asinMetadata?.get(asin);
|
||||
|
||||
const marketMapping: Record<string, string[]> = {
|
||||
'DE': ['Amazon DE', 'Amazon SC'],
|
||||
'UK': ['Amazon UK', 'Amazon SC'],
|
||||
'IT': ['Amazon IT', 'Amazon SC'],
|
||||
'FR': ['Amazon FR', 'Amazon SC'],
|
||||
'ES': ['Amazon ES', 'Amazon SC'],
|
||||
};
|
||||
|
||||
let countryMatch = true;
|
||||
if (filters.customer.length > 0) {
|
||||
const mappedCustomers = marketMapping[record.market] || [];
|
||||
countryMatch = filters.customer.some(c => mappedCustomers.includes(c));
|
||||
} else {
|
||||
countryMatch = PAN_EU_COUNTRIES.some(c => c.toUpperCase().includes(record.market));
|
||||
}
|
||||
|
||||
const weekStr = `W${record.week}`;
|
||||
const weekMatch = filters.week.length === 0 || filters.week.includes(weekStr);
|
||||
|
||||
const asinMatch = filters.asin.length === 0 || filters.asin.some(a => a.toUpperCase() === asin);
|
||||
|
||||
let skuMatch = true;
|
||||
if (filters.sku.length > 0) {
|
||||
skuMatch = meta ? filters.sku.some(s => s.toUpperCase() === meta.sku.toUpperCase()) : false;
|
||||
}
|
||||
|
||||
let lineMatch = true;
|
||||
if (filters.line.length > 0) {
|
||||
lineMatch = meta ? filters.line.includes(meta.line) : false;
|
||||
}
|
||||
|
||||
let titleMatch = true;
|
||||
if (filters.title.length > 0) {
|
||||
titleMatch = meta ? filters.title.includes(meta.title) : false;
|
||||
}
|
||||
|
||||
const stockMatch = checkStockFilter(meta?.sku || '', filters.stock, stockMap);
|
||||
const vendorStockMatch = checkVendorStockFilter(asin, filters.vendorStock, vendorStockMap, top50Mode);
|
||||
|
||||
let bulkMatch = true;
|
||||
if (filters.bulkSearch && filters.bulkSearch.trim()) {
|
||||
const searchTerms = filters.bulkSearch
|
||||
.split(/[\s,\n]+/)
|
||||
.map(t => t.trim().toUpperCase())
|
||||
.filter(t => t.length > 0);
|
||||
|
||||
if (searchTerms.length > 0) {
|
||||
const itemAsin = (record.asin || '').toUpperCase();
|
||||
const itemSku = (meta?.sku || '').toUpperCase();
|
||||
bulkMatch = searchTerms.some(term =>
|
||||
itemAsin.includes(term) || itemSku.includes(term)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return countryMatch && weekMatch && asinMatch && skuMatch && lineMatch && titleMatch && stockMatch && vendorStockMatch && bulkMatch;
|
||||
});
|
||||
};
|
||||
|
||||
const calculateSeasonality = (data: SalesRecord[]): { seasonality: SeasonalityPoint[], seasonalityUnits: SeasonalityPoint[], years: string[] } => {
|
||||
const seasonalityMap = new Map<string, SeasonalityPoint>();
|
||||
const seasonalityUnitsMap = new Map<string, SeasonalityPoint>();
|
||||
|
||||
Reference in New Issue
Block a user