feat: add vendor stock badges to Sell-in vs Budget first column

Shows aggregated EU and UK vendor stock per SKU (sum across all ASINs)
in the first column, matching the style used in other dashboard tabs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-06-17 16:25:41 +02:00
co-authored by Claude Sonnet 4.6
parent d9bbf41c7c
commit acf3faec36
2 changed files with 45 additions and 6 deletions
+1 -1
View File
@@ -898,7 +898,7 @@ const App: React.FC = () => {
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'forecast' ? '' : 'hidden'}>
<ForecastView asinMetadata={globalAsinMetadata} />
<ForecastView asinMetadata={globalAsinMetadata} vendorStockMap={vendorStockMap} />
</div>
</Suspense>
+43 -4
View File
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useCallback, useRef } from 'react';
import * as XLSX from 'xlsx';
import { ExcelFilter } from './ExcelFilter';
import { ColumnFilterCondition } from '../types';
import { AmazonSmileIcon } from './Icons';
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
@@ -33,6 +34,8 @@ interface ComparisonRow {
budgetDiscrepancy: boolean;
pctPeriod: number;
pctAnnual: number;
vendorStockEU: number;
vendorStockUK: number;
}
function parseSellIn(buffer: ArrayBuffer): Map<string, SellInRow> {
@@ -132,8 +135,34 @@ const ComparisonTableRow: React.FC<{ row: ComparisonRow }> = React.memo(({ row }
const barWidth = Math.min(100, row.pctPeriod);
return (
<tr className="hover:bg-indigo-500/5 transition-colors border-b border-slate-800/50 last:border-0">
<td className="px-4 py-3 whitespace-nowrap">
<td className="px-4 py-3">
<div className="flex flex-col gap-1">
<span className="text-xs font-black text-indigo-400 tracking-tighter">{row.sku}</span>
{(row.vendorStockEU > 0 || row.vendorStockUK > 0) && (
<div className="flex flex-col gap-0.5">
{row.vendorStockEU > 0 && (
<div
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10px] font-bold bg-slate-200 text-slate-800 border-slate-300 w-fit"
title={`Stock Amazon Vendor EU: ${row.vendorStockEU}`}
>
<AmazonSmileIcon className="w-3 h-3 shrink-0" />
<span className="text-[7px] font-black uppercase opacity-60">EU</span>
<span>{row.vendorStockEU.toLocaleString('de-DE')}</span>
</div>
)}
{row.vendorStockUK > 0 && (
<div
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10px] font-bold bg-slate-200 text-slate-800 border-slate-300 w-fit"
title={`Stock Amazon Vendor UK: ${row.vendorStockUK}`}
>
<AmazonSmileIcon className="w-3 h-3 shrink-0" />
<span className="text-[7px] font-black uppercase opacity-60">UK</span>
<span>{row.vendorStockUK.toLocaleString('de-DE')}</span>
</div>
)}
</div>
)}
</div>
</td>
<td className="px-4 py-3 max-w-[280px]">
<span className="text-sm text-slate-200 line-clamp-1" title={row.description}>
@@ -188,9 +217,10 @@ const ComparisonTableRow: React.FC<{ row: ComparisonRow }> = React.memo(({ row }
interface ForecastViewProps {
asinMetadata?: Map<string, { sku: string; title: string; line: string }>;
vendorStockMap?: Map<string, { eu: number; uk: number }>;
}
const ForecastView: React.FC<ForecastViewProps> = ({ asinMetadata }) => {
const ForecastView: React.FC<ForecastViewProps> = ({ asinMetadata, vendorStockMap }) => {
const [sellInData, setSellInData] = useState<Map<string, SellInRow> | null>(null);
const [budgetData, setBudgetData] = useState<Map<string, BudgetRow> | null>(null);
const [sellInFileName, setSellInFileName] = useState('');
@@ -258,10 +288,17 @@ const ForecastView: React.FC<ForecastViewProps> = ({ asinMetadata }) => {
const pctPeriod = periodForecast > 0 ? (actualUnits / periodForecast) * 100 : 0;
const pctAnnual = budgetYearTotal > 0 ? (actualUnits / budgetYearTotal) * 100 : 0;
const meta = skuMetadata.get(budgetRow.sku);
const asins = meta?.asins ?? [];
const vendorStockEU = asins.reduce((sum, asin) => {
return sum + (vendorStockMap?.get(asin.trim().toUpperCase())?.eu ?? 0);
}, 0);
const vendorStockUK = asins.reduce((sum, asin) => {
return sum + (vendorStockMap?.get(asin.trim().toUpperCase())?.uk ?? 0);
}, 0);
rows.push({
sku: budgetRow.sku,
description: budgetRow.description,
asins: meta?.asins ?? [],
asins,
title: meta?.title ?? '',
actualUnits,
periodForecast,
@@ -269,10 +306,12 @@ const ForecastView: React.FC<ForecastViewProps> = ({ asinMetadata }) => {
budgetDiscrepancy,
pctPeriod,
pctAnnual,
vendorStockEU,
vendorStockUK,
});
});
return rows;
}, [budgetData, sellInData, cutoffMonth, skuMetadata]);
}, [budgetData, sellInData, cutoffMonth, skuMetadata, vendorStockMap]);
const passesTextFilter = useCallback((value: string, filter?: ColumnFilterCondition): boolean => {
if (!filter) return true;