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 />}> <Suspense fallback={<LoadingSpinner />}>
<div className={view === 'forecast' ? '' : 'hidden'}> <div className={view === 'forecast' ? '' : 'hidden'}>
<ForecastView asinMetadata={globalAsinMetadata} /> <ForecastView asinMetadata={globalAsinMetadata} vendorStockMap={vendorStockMap} />
</div> </div>
</Suspense> </Suspense>
+43 -4
View File
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useCallback, useRef } from 'react';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { ExcelFilter } from './ExcelFilter'; import { ExcelFilter } from './ExcelFilter';
import { ColumnFilterCondition } from '../types'; import { ColumnFilterCondition } from '../types';
import { AmazonSmileIcon } from './Icons';
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; 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']; const MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
@@ -33,6 +34,8 @@ interface ComparisonRow {
budgetDiscrepancy: boolean; budgetDiscrepancy: boolean;
pctPeriod: number; pctPeriod: number;
pctAnnual: number; pctAnnual: number;
vendorStockEU: number;
vendorStockUK: number;
} }
function parseSellIn(buffer: ArrayBuffer): Map<string, SellInRow> { 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); const barWidth = Math.min(100, row.pctPeriod);
return ( return (
<tr className="hover:bg-indigo-500/5 transition-colors border-b border-slate-800/50 last:border-0"> <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> <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>
<td className="px-4 py-3 max-w-[280px]"> <td className="px-4 py-3 max-w-[280px]">
<span className="text-sm text-slate-200 line-clamp-1" title={row.description}> <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 { interface ForecastViewProps {
asinMetadata?: Map<string, { sku: string; title: string; line: string }>; 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 [sellInData, setSellInData] = useState<Map<string, SellInRow> | null>(null);
const [budgetData, setBudgetData] = useState<Map<string, BudgetRow> | null>(null); const [budgetData, setBudgetData] = useState<Map<string, BudgetRow> | null>(null);
const [sellInFileName, setSellInFileName] = useState(''); const [sellInFileName, setSellInFileName] = useState('');
@@ -258,10 +288,17 @@ const ForecastView: React.FC<ForecastViewProps> = ({ asinMetadata }) => {
const pctPeriod = periodForecast > 0 ? (actualUnits / periodForecast) * 100 : 0; const pctPeriod = periodForecast > 0 ? (actualUnits / periodForecast) * 100 : 0;
const pctAnnual = budgetYearTotal > 0 ? (actualUnits / budgetYearTotal) * 100 : 0; const pctAnnual = budgetYearTotal > 0 ? (actualUnits / budgetYearTotal) * 100 : 0;
const meta = skuMetadata.get(budgetRow.sku); 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({ rows.push({
sku: budgetRow.sku, sku: budgetRow.sku,
description: budgetRow.description, description: budgetRow.description,
asins: meta?.asins ?? [], asins,
title: meta?.title ?? '', title: meta?.title ?? '',
actualUnits, actualUnits,
periodForecast, periodForecast,
@@ -269,10 +306,12 @@ const ForecastView: React.FC<ForecastViewProps> = ({ asinMetadata }) => {
budgetDiscrepancy, budgetDiscrepancy,
pctPeriod, pctPeriod,
pctAnnual, pctAnnual,
vendorStockEU,
vendorStockUK,
}); });
}); });
return rows; return rows;
}, [budgetData, sellInData, cutoffMonth, skuMetadata]); }, [budgetData, sellInData, cutoffMonth, skuMetadata, vendorStockMap]);
const passesTextFilter = useCallback((value: string, filter?: ColumnFilterCondition): boolean => { const passesTextFilter = useCallback((value: string, filter?: ColumnFilterCondition): boolean => {
if (!filter) return true; if (!filter) return true;