mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:25:22 +02:00
feat: integrate Amazon Vendor Stock data across all views and fix local data loading
This commit is contained in:
@@ -4,6 +4,7 @@ import { CombinedKPIs, FilterState } from '../types';
|
||||
import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons';
|
||||
import { StockBadge } from './StockBadge';
|
||||
import { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||
|
||||
interface AdsPerformanceProps {
|
||||
@@ -17,6 +18,7 @@ interface AdsPerformanceProps {
|
||||
stockFilter: string[];
|
||||
onStockFilterChange: (newFilters: string[]) => void;
|
||||
top50Mode: 'eu' | 'uk';
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
}
|
||||
|
||||
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
||||
@@ -37,7 +39,8 @@ const AdsRow: React.FC<{
|
||||
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
|
||||
top50Mode: 'eu' | 'uk';
|
||||
stockMap?: Map<string, number>;
|
||||
}> = React.memo(({ item, top50Ranking, top50Mode, stockMap }) => {
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
}> = React.memo(({ item, top50Ranking, top50Mode, stockMap, vendorStockMap }) => {
|
||||
const asin = item.asin.trim().toUpperCase();
|
||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||
|
||||
@@ -67,6 +70,7 @@ const AdsRow: React.FC<{
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -88,7 +92,7 @@ const AdsRow: React.FC<{
|
||||
);
|
||||
});
|
||||
|
||||
const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange, top50Mode }) => {
|
||||
const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, top50Mode }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({
|
||||
@@ -393,7 +397,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ran
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.03]">
|
||||
{filteredAndSorted.map((p) => (
|
||||
<AdsRow key={p.id} item={p} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} />
|
||||
<AdsRow key={p.id} item={p} top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} vendorStockMap={vendorStockMap} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForCompa
|
||||
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
||||
import { StockBadge } from './StockBadge';
|
||||
import { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
|
||||
interface DataGridProps {
|
||||
data: SalesRecord[] | CombinedKPIs[];
|
||||
@@ -18,6 +19,7 @@ interface DataGridProps {
|
||||
uk: Map<string, number>;
|
||||
};
|
||||
top50Mode: 'eu' | 'uk';
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
}
|
||||
|
||||
type SortConfig = {
|
||||
@@ -234,7 +236,7 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
|
||||
};
|
||||
|
||||
|
||||
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap, top50Ranking, top50Mode }) => {
|
||||
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode }) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: null, direction: 'desc' });
|
||||
const [showChart, setShowChart] = useState(true);
|
||||
@@ -1090,7 +1092,10 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, s
|
||||
? <div className="flex items-start gap-2">
|
||||
<span className="line-clamp-2" title={row.title}>{row.title || '-'}</span>
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
<>
|
||||
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
<VendorStockBadge asin={row.asin} vendorStockMap={vendorStockMap} mode={top50Mode} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
: dim === 'asin' || dim === 'sku'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ProductForecastData, FilterState, CombinedKPIs } from '../types';
|
||||
import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons';
|
||||
import { StockBadge } from './StockBadge';
|
||||
import { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||
import { PAN_EU_COUNTRIES } from '../services/dataProcessor';
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ interface ForecastViewProps {
|
||||
uk: Map<string, number>;
|
||||
};
|
||||
stockMap?: Map<string, number>;
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
stockFilter: string[];
|
||||
onStockFilterChange: (newFilters: string[]) => void;
|
||||
top50Mode: 'eu' | 'uk';
|
||||
@@ -32,7 +34,8 @@ const ForecastRow: React.FC<{
|
||||
top50Ranking?: { eu: Map<string, number>; uk: Map<string, number> };
|
||||
top50Mode: 'eu' | 'uk';
|
||||
stockMap?: Map<string, number>;
|
||||
}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap }) => {
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap }) => {
|
||||
const asin = item.asin.trim().toUpperCase();
|
||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||
|
||||
@@ -71,6 +74,7 @@ const ForecastRow: React.FC<{
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(item.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
<VendorStockBadge asin={item.asin} vendorStockMap={vendorStockMap} mode={top50Mode} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -112,7 +116,7 @@ const ForecastRow: React.FC<{
|
||||
);
|
||||
});
|
||||
|
||||
const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange, top50Mode }) => {
|
||||
const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, top50Mode }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||
const [displayCount, setDisplayCount] = useState(50);
|
||||
@@ -246,6 +250,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking
|
||||
top50Ranking={top50Ranking}
|
||||
top50Mode={top50Mode}
|
||||
stockMap={stockMap}
|
||||
vendorStockMap={vendorStockMap}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface VendorStockBadgeProps {
|
||||
asin: string;
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
mode: 'eu' | 'uk';
|
||||
}
|
||||
|
||||
export const VendorStockBadge: React.FC<VendorStockBadgeProps> = ({ asin, vendorStockMap, mode }) => {
|
||||
if (!vendorStockMap || !asin) return null;
|
||||
|
||||
const stockData = vendorStockMap.get(asin.toUpperCase());
|
||||
if (!stockData) return null;
|
||||
|
||||
const stock = mode === 'uk' ? stockData.uk : stockData.eu;
|
||||
|
||||
// As per user request: "etiqueta será de color gris claro"
|
||||
// Using a light gray/slate theme that fits the dashboard's premium look
|
||||
const themeClasses = "bg-slate-200 text-slate-800 border-slate-300 shadow-sm";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded border text-[10px] font-bold transition-all hover:scale-105 active:scale-95 cursor-default ${themeClasses}`}
|
||||
title={`Stock en Amazon Warehouse (${mode.toUpperCase()}): ${stock}`}
|
||||
>
|
||||
<svg
|
||||
className="w-3.5 h-3.5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.5 14c.5 0 1 .5 1 1s-.5 1-1 1-1-.5-1-1 .5-1 1-1zm-11 0c.5 0 1 .5 1 1s-.5 1-1 1-1-.5-1-1 .5-1 1-1z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M3 13.5c0 4.7 3.8 8.5 8.5 8.5s8.5-3.8 8.5-8.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M17 18.5c-1.5 1.5-3.5 2.5-5.5 2.5s-4-1-5.5-2.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M19 18.5l1.5 1.5M5 18.5L3.5 20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex flex-col leading-[0.8]">
|
||||
<span className="text-[7px] font-black uppercase opacity-60">Vendor</span>
|
||||
<span>{stock.toLocaleString('de-DE')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES } from '../servi
|
||||
import { StockBadge } from './StockBadge';
|
||||
import { InColumnStockFilter } from './InColumnStockFilter';
|
||||
import { Top50Badge } from './Top50Badge';
|
||||
import { VendorStockBadge } from './VendorStockBadge';
|
||||
|
||||
interface WeeklyGridProps {
|
||||
data: CombinedKPIs[];
|
||||
@@ -17,6 +18,7 @@ interface WeeklyGridProps {
|
||||
stockFilter: string[];
|
||||
onStockFilterChange: (newFilters: string[]) => void;
|
||||
customerFilters: string[];
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
}
|
||||
|
||||
type SortConfig = {
|
||||
@@ -48,7 +50,8 @@ const WeeklyRow: React.FC<{
|
||||
sortConfig: SortConfig;
|
||||
renderGrowth: (current: number, previous: number) => React.ReactNode;
|
||||
customerFilters: string[];
|
||||
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters }) => {
|
||||
vendorStockMap?: Map<string, { eu: number; uk: number }>;
|
||||
}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters, vendorStockMap }) => {
|
||||
const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = [];
|
||||
const asin = row.asin.trim().toUpperCase();
|
||||
|
||||
@@ -85,6 +88,7 @@ const WeeklyRow: React.FC<{
|
||||
{stockMap && (
|
||||
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||
)}
|
||||
<VendorStockBadge asin={row.asin} vendorStockMap={vendorStockMap} mode={top50Mode} />
|
||||
</div>
|
||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||
</div>
|
||||
@@ -129,7 +133,7 @@ const WeeklyRow: React.FC<{
|
||||
);
|
||||
});
|
||||
|
||||
const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => {
|
||||
const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({ data, top50Ranking, onDrillDown, stockMap, vendorStockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => {
|
||||
// Pivot data - memoized
|
||||
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||
|
||||
@@ -545,6 +549,7 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({ da
|
||||
weeks={weeks}
|
||||
onDrillDown={onDrillDown}
|
||||
stockMap={stockMap}
|
||||
vendorStockMap={vendorStockMap}
|
||||
top50Ranking={top50Ranking}
|
||||
top50Mode={top50Mode}
|
||||
sortConfig={sortConfig}
|
||||
|
||||
Reference in New Issue
Block a user