mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:45:23 +02:00
feat: add stock, vendor and buybox badges to Top Movers table
- Added StockBadge, VendorStockBadge and BuyBoxWarningBadge to TopMovers component - Updated Dashboard to pass stockMap, vendorStockMap, buyBoxLostMap and top50Mode props - Updated App.tsx to pass the required maps to Dashboard - Table now shows 3 new columns: Stock (GMBH), Vendor (Amazon Warehouse), and Buy Box status Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
co-authored by
Qwen-Coder
parent
167273e659
commit
19f1137a30
@@ -836,7 +836,16 @@ const App: React.FC = () => {
|
|||||||
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
|
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className={view === 'dashboard' ? '' : 'hidden'}>
|
<div className={view === 'dashboard' ? '' : 'hidden'}>
|
||||||
<Dashboard data={ytdAggregatedData} filterState={filters} adsData={ytdFilteredAdsData} stockMap={stockMap} rawData={ytdFilteredData} />
|
<Dashboard
|
||||||
|
data={ytdAggregatedData}
|
||||||
|
filterState={filters}
|
||||||
|
adsData={ytdFilteredAdsData}
|
||||||
|
stockMap={stockMap}
|
||||||
|
vendorStockMap={vendorStockMap}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
|
top50Mode={top50Mode}
|
||||||
|
rawData={ytdFilteredData}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ interface DashboardProps {
|
|||||||
contextData?: AggregatedData | null;
|
contextData?: AggregatedData | null;
|
||||||
adsData?: AdsRecord[];
|
adsData?: AdsRecord[];
|
||||||
rawData?: SalesRecord[];
|
rawData?: SalesRecord[];
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
top50Mode?: 'eu' | 'uk';
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9'];
|
const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9'];
|
||||||
@@ -814,7 +818,13 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData, adsData = [],
|
|||||||
{/* Top Movers Table - Full Width */}
|
{/* Top Movers Table - Full Width */}
|
||||||
{rawData && rawData.length > 0 && (
|
{rawData && rawData.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<TopMovers data={rawData} />
|
<TopMovers
|
||||||
|
data={rawData}
|
||||||
|
stockMap={stockMap}
|
||||||
|
vendorStockMap={vendorStockMap}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
|
top50Mode={top50Mode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,23 @@ import React, { useState, useMemo, useCallback } from 'react';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { SalesRecord } from '../types';
|
import { SalesRecord } from '../types';
|
||||||
import { DownloadIcon } from './Icons';
|
import { DownloadIcon } from './Icons';
|
||||||
|
import { StockBadge } from './StockBadge';
|
||||||
|
import { VendorStockBadge } from './VendorStockBadge';
|
||||||
|
import { BuyBoxWarningBadge } from './BuyBoxWarningBadge';
|
||||||
|
|
||||||
interface TopMoversProps {
|
interface TopMoversProps {
|
||||||
data: SalesRecord[];
|
data: SalesRecord[];
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
top50Mode?: 'eu' | 'uk';
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metric = 'sellOut' | 'units';
|
type Metric = 'sellOut' | 'units';
|
||||||
|
|
||||||
interface SkuAggr {
|
interface SkuAggr {
|
||||||
sku: string;
|
sku: string;
|
||||||
|
asin: string;
|
||||||
title: string;
|
title: string;
|
||||||
line: string;
|
line: string;
|
||||||
previousValue: number;
|
previousValue: number;
|
||||||
@@ -27,7 +35,11 @@ const MoversTable: React.FC<{
|
|||||||
previousYear: number;
|
previousYear: number;
|
||||||
currentYear: number;
|
currentYear: number;
|
||||||
type: 'growth' | 'decline';
|
type: 'growth' | 'decline';
|
||||||
}> = ({ title, data, metric, previousYear, currentYear, type }) => {
|
stockMap?: Map<string, number>;
|
||||||
|
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||||
|
buyBoxLostMap?: Map<string, { countries: string[]; reasons: Record<string, string> }>;
|
||||||
|
top50Mode?: 'eu' | 'uk';
|
||||||
|
}> = ({ title, data, metric, previousYear, currentYear, type, stockMap, vendorStockMap, buyBoxLostMap, top50Mode = 'eu' }) => {
|
||||||
|
|
||||||
const formatValue = (val: number) => {
|
const formatValue = (val: number) => {
|
||||||
if (metric === 'sellOut') return `€${val.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`;
|
if (metric === 'sellOut') return `€${val.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`;
|
||||||
@@ -84,6 +96,9 @@ const MoversTable: React.FC<{
|
|||||||
<th className="px-6 py-3 border-b border-border w-16 text-center">Rank</th>
|
<th className="px-6 py-3 border-b border-border w-16 text-center">Rank</th>
|
||||||
<th className="px-6 py-3 border-b border-border">SKU Details</th>
|
<th className="px-6 py-3 border-b border-border">SKU Details</th>
|
||||||
<th className="px-6 py-3 border-b border-border">Product Line</th>
|
<th className="px-6 py-3 border-b border-border">Product Line</th>
|
||||||
|
<th className="px-6 py-3 border-b border-border text-center">Stock</th>
|
||||||
|
<th className="px-6 py-3 border-b border-border text-center">Vendor</th>
|
||||||
|
<th className="px-6 py-3 border-b border-border text-center">Buy Box</th>
|
||||||
<th className="px-6 py-3 border-b border-border text-right">{previousYear}</th>
|
<th className="px-6 py-3 border-b border-border text-right">{previousYear}</th>
|
||||||
<th className="px-6 py-3 border-b border-border text-right">{currentYear}</th>
|
<th className="px-6 py-3 border-b border-border text-right">{currentYear}</th>
|
||||||
<th className="px-6 py-3 border-b border-border text-right">Diff</th>
|
<th className="px-6 py-3 border-b border-border text-right">Diff</th>
|
||||||
@@ -113,6 +128,25 @@ const MoversTable: React.FC<{
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{/* Stock Badge */}
|
||||||
|
<td className="px-6 py-3 text-center">
|
||||||
|
<StockBadge stock={stockMap?.get(item.sku)} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Vendor Stock Badge */}
|
||||||
|
<td className="px-6 py-3 text-center">
|
||||||
|
<VendorStockBadge
|
||||||
|
asin={item.asin}
|
||||||
|
vendorStockMap={vendorStockMap}
|
||||||
|
mode={top50Mode}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Buy Box Warning Badge */}
|
||||||
|
<td className="px-6 py-3 text-center">
|
||||||
|
<BuyBoxWarningBadge asin={item.asin} buyBoxLostMap={buyBoxLostMap} />
|
||||||
|
</td>
|
||||||
|
|
||||||
<td className="px-6 py-3 text-right text-slate-500">
|
<td className="px-6 py-3 text-right text-slate-500">
|
||||||
{formatValue(item.previousValue)}
|
{formatValue(item.previousValue)}
|
||||||
</td>
|
</td>
|
||||||
@@ -148,7 +182,7 @@ const MoversTable: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
|
const TopMovers: React.FC<TopMoversProps> = ({ data, stockMap, vendorStockMap, buyBoxLostMap, top50Mode = 'eu' }) => {
|
||||||
const [metric, setMetric] = useState<Metric>('sellOut');
|
const [metric, setMetric] = useState<Metric>('sellOut');
|
||||||
const [viewMode, setViewMode] = useState<'growth' | 'decline'>('growth');
|
const [viewMode, setViewMode] = useState<'growth' | 'decline'>('growth');
|
||||||
|
|
||||||
@@ -167,14 +201,14 @@ const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
|
|||||||
if (!currentYear || !previousYear) return { growers: [], decliners: [] };
|
if (!currentYear || !previousYear) return { growers: [], decliners: [] };
|
||||||
|
|
||||||
// Map: SKU -> { currentVal, previousVal, metadata }
|
// Map: SKU -> { currentVal, previousVal, metadata }
|
||||||
const map = new Map<string, { current: number; previous: number; title: string; line: string }>();
|
const map = new Map<string, { current: number; previous: number; title: string; line: string; asin: string }>();
|
||||||
|
|
||||||
data.forEach(row => {
|
data.forEach(row => {
|
||||||
// Only care about the two comparison years
|
// Only care about the two comparison years
|
||||||
if (row.year !== currentYear && row.year !== previousYear) return;
|
if (row.year !== currentYear && row.year !== previousYear) return;
|
||||||
|
|
||||||
if (!map.has(row.sku)) {
|
if (!map.has(row.sku)) {
|
||||||
map.set(row.sku, { current: 0, previous: 0, title: row.title, line: row.line });
|
map.set(row.sku, { current: 0, previous: 0, title: row.title, line: row.line, asin: row.asin });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = map.get(row.sku)!;
|
const entry = map.get(row.sku)!;
|
||||||
@@ -204,6 +238,7 @@ const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
|
|||||||
|
|
||||||
list.push({
|
list.push({
|
||||||
sku,
|
sku,
|
||||||
|
asin: val.asin,
|
||||||
title: val.title,
|
title: val.title,
|
||||||
line: val.line,
|
line: val.line,
|
||||||
previousValue: val.previous,
|
previousValue: val.previous,
|
||||||
@@ -297,6 +332,10 @@ const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
|
|||||||
previousYear={previousYear}
|
previousYear={previousYear}
|
||||||
currentYear={currentYear}
|
currentYear={currentYear}
|
||||||
type={viewMode}
|
type={viewMode}
|
||||||
|
stockMap={stockMap}
|
||||||
|
vendorStockMap={vendorStockMap}
|
||||||
|
buyBoxLostMap={buyBoxLostMap}
|
||||||
|
top50Mode={top50Mode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user