mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:55:22 +02:00
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:
co-authored by
Claude Sonnet 4.6
parent
d9bbf41c7c
commit
acf3faec36
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
<span className="text-xs font-black text-indigo-400 tracking-tighter">{row.sku}</span>
|
<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>
|
||||||
<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;
|
||||||
|
|||||||
Reference in New Issue
Block a user