Fix: improve Vendor stock parsing robustness and ASIN extraction for B0D14YBNWB

This commit is contained in:
Christian Vidal Wolf
2026-04-27 11:48:47 +02:00
parent e7f48af099
commit 490cb4a96b
4 changed files with 116 additions and 58 deletions
+59 -48
View File
@@ -1,4 +1,5 @@
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { CombinedKPIs, ColumnFilterCondition, SalesRecord } from '../types'; import { CombinedKPIs, ColumnFilterCondition, SalesRecord } from '../types';
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions, filterData } from '../services/dataProcessor'; import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions, filterData } from '../services/dataProcessor';
@@ -782,12 +783,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
</div> </div>
</th> </th>
{weeks.map((week, idx) => { {weeks.map((week, idx) => {
const [yearStr, weekNum] = week.split('-');
const prevYearWeek = `${parseInt(yearStr) - 1}-${weekNum}`;
const prevWeekTotals = allWeekTotals[weeks[idx + 1]]; const prevWeekTotals = allWeekTotals[weeks[idx + 1]];
const yoyTotals = allWeekTotals[prevYearWeek];
const cur = weekTotals[week]; const cur = weekTotals[week];
const isHovered = hoveredTotalWeek === week;
return ( return (
<th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/10" <th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/10"
@@ -825,50 +822,6 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
</div> </div>
</div> </div>
{isHovered && tooltipPos && (
<div className="pointer-events-none fixed z-[9999] w-64 bg-slate-900 border border-white/20 rounded-xl shadow-2xl p-3 text-left"
style={{ left: tooltipPos.x + 16, top: tooltipPos.y - 16, transform: 'translateY(-100%)' }}
>
<div className="text-[10px] font-black text-white/50 uppercase tracking-widest mb-2">{week}</div>
{/* Semana anterior */}
<div className="mb-2">
<div className="text-[9px] font-black text-indigo-300 uppercase tracking-widest mb-1">Semana anterior ({weeks[idx + 1] || '—'})</div>
{prevWeekTotals ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[11px]">
<span className="text-white/60">Units</span>
<span className="text-white font-bold text-right">{prevWeekTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, prevWeekTotals.units)}</span>
<span className="text-white/60">Revenue</span>
<span className="text-white font-bold text-right">{prevWeekTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, prevWeekTotals.revenue)}</span>
<span className="text-white/60">Ads Spend</span>
<span className="text-rose-400 font-bold text-right">{prevWeekTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, prevWeekTotals.spend)}</span>
<span className="text-white/60">GV</span>
<span className="text-teal-400 font-bold text-right">{prevWeekTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, prevWeekTotals.gv)}</span>
</div>
) : (
<span className="text-white/30 text-[11px]">Sin datos</span>
)}
</div>
<div className="border-t border-white/10 pt-2">
<div className="text-[9px] font-black text-amber-300 uppercase tracking-widest mb-1">Misma semana año anterior ({prevYearWeek})</div>
{yoyTotals && (yoyTotals.units > 0 || yoyTotals.revenue > 0) ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[11px]">
<span className="text-white/60">Units</span>
<span className="text-white font-bold text-right">{yoyTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, yoyTotals.units)}</span>
<span className="text-white/60">Revenue</span>
<span className="text-white font-bold text-right">{yoyTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, yoyTotals.revenue)}</span>
<span className="text-white/60">Ads Spend</span>
<span className="text-rose-400 font-bold text-right">{yoyTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, yoyTotals.spend)}</span>
<span className="text-white/60">GV</span>
<span className="text-teal-400 font-bold text-right">{yoyTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.revenue || 0, yoyTotals.gv)}</span>
</div>
) : (
<span className="text-white/30 text-[11px]">Sin datos del año anterior</span>
)}
</div>
</div>
)}
</th> </th>
); );
})} })}
@@ -928,6 +881,64 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
</div> </div>
</div> </div>
); );
const tooltipPortal = hoveredTotalWeek && tooltipPos ? (() => {
const week = hoveredTotalWeek;
const idx = weeks.indexOf(week);
const [yearStr, weekNum] = week.split('-');
const prevYearWeek = `${parseInt(yearStr) - 1}-${weekNum}`;
const cur = weekTotals[week];
const prevWeekTotals = allWeekTotals[weeks[idx + 1]];
const yoyTotals = allWeekTotals[prevYearWeek];
return ReactDOM.createPortal(
<div
className="pointer-events-none fixed w-64 bg-slate-900 border border-white/20 rounded-xl shadow-2xl p-3 text-left"
style={{ left: tooltipPos.x + 16, top: tooltipPos.y - 16, transform: 'translateY(-100%)', zIndex: 999999 }}
>
<div className="text-[10px] font-black text-white/50 uppercase tracking-widest mb-2">{week}</div>
<div className="mb-2">
<div className="text-[9px] font-black text-indigo-300 uppercase tracking-widest mb-1">Semana anterior ({weeks[idx + 1] || '—'})</div>
{prevWeekTotals ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[11px]">
<span className="text-white/60">Units</span>
<span className="text-white font-bold text-right">{prevWeekTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, prevWeekTotals.units)}</span>
<span className="text-white/60">Revenue</span>
<span className="text-white font-bold text-right">{prevWeekTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, prevWeekTotals.revenue)}</span>
<span className="text-white/60">Ads Spend</span>
<span className="text-rose-400 font-bold text-right">{prevWeekTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, prevWeekTotals.spend)}</span>
<span className="text-white/60">GV</span>
<span className="text-teal-400 font-bold text-right">{prevWeekTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, prevWeekTotals.gv)}</span>
</div>
) : (
<span className="text-white/30 text-[11px]">Sin datos</span>
)}
</div>
<div className="border-t border-white/10 pt-2">
<div className="text-[9px] font-black text-amber-300 uppercase tracking-widest mb-1">Misma semana año anterior ({prevYearWeek})</div>
{yoyTotals && (yoyTotals.units > 0 || yoyTotals.revenue > 0) ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[11px]">
<span className="text-white/60">Units</span>
<span className="text-white font-bold text-right">{yoyTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, yoyTotals.units)}</span>
<span className="text-white/60">Revenue</span>
<span className="text-white font-bold text-right">{yoyTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, yoyTotals.revenue)}</span>
<span className="text-white/60">Ads Spend</span>
<span className="text-rose-400 font-bold text-right">{yoyTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, yoyTotals.spend)}</span>
<span className="text-white/60">GV</span>
<span className="text-teal-400 font-bold text-right">{yoyTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, yoyTotals.gv)}</span>
</div>
) : (
<span className="text-white/30 text-[11px]">Sin datos del año anterior</span>
)}
</div>
</div>,
document.body
);
})() : null;
return <>{mainContent}{tooltipPortal}</>;
}; };
export default WeeklyGrid; export default WeeklyGrid;
+29
View File
@@ -23,6 +23,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/react-dom": "^19.2.3",
"@vercel/node": "^5.5.22", "@vercel/node": "^5.5.22",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2", "typescript": "~5.8.2",
@@ -1741,6 +1742,27 @@
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/use-sync-external-store": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -2858,6 +2880,13 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-array": { "node_modules/d3-array": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+1
View File
@@ -27,6 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/react-dom": "^19.2.3",
"@vercel/node": "^5.5.22", "@vercel/node": "^5.5.22",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2", "typescript": "~5.8.2",
+27 -10
View File
@@ -2327,15 +2327,21 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
if (headerRowIndex === -1) { if (headerRowIndex === -1) {
// Fallback: look for ASIN in first row if not found in header scan // Fallback: look for ASIN in first row if not found in header scan
if (jsonData[0] && (jsonData[0].includes('ASIN') || jsonData[0].includes('asin'))) headerRowIndex = 0; headerRowIndex = jsonData.findIndex(row =>
else { row && row.some(cell => String(cell || '').toUpperCase().includes('ASIN'))
);
if (headerRowIndex === -1) {
console.warn("Could not find header row in Vendor Stock Excel"); console.warn("Could not find header row in Vendor Stock Excel");
return vendorStockMap; return vendorStockMap;
} }
} }
const headers: any[] = jsonData[headerRowIndex]; const headers: any[] = jsonData[headerRowIndex];
const asinIdx = headers.findIndex(h => String(h || '').toUpperCase() === 'ASIN'); const asinIdx = headers.findIndex(h => {
const sh = String(h || '').toUpperCase();
return sh === 'ASIN' || sh === 'PRODUCT ASIN' || sh === 'IDENTIFIER';
});
// Enhanced marketplace detection - includes 'Store code' for PANEU reports // Enhanced marketplace detection - includes 'Store code' for PANEU reports
const marketplaceIdx = headers.findIndex(h => { const marketplaceIdx = headers.findIndex(h => {
@@ -2396,8 +2402,10 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
const row = jsonData[i]; const row = jsonData[i];
if (!row || row.length <= Math.max(finalAsinIdx, finalMarketplaceIdx, finalStockIdx)) continue; if (!row || row.length <= Math.max(finalAsinIdx, finalMarketplaceIdx, finalStockIdx)) continue;
const asin = String(row[finalAsinIdx] || '').trim().toUpperCase(); const asinRaw = String(row[finalAsinIdx] || '').trim().toUpperCase();
if (!asin) continue; const asin = (asinRaw.match(/B[A-Z0-9]{9}/) || [asinRaw])[0];
if (!asin || asin.length < 10) continue;
const marketplace = String(row[finalMarketplaceIdx] || '').trim().toLowerCase(); const marketplace = String(row[finalMarketplaceIdx] || '').trim().toLowerCase();
const stockValue = parseUnits(String(row[finalStockIdx] || '0')); const stockValue = parseUnits(String(row[finalStockIdx] || '0'));
@@ -2409,10 +2417,17 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
const current = vendorStockMap.get(asin)!; const current = vendorStockMap.get(asin)!;
// Map to UK or EU // Map to UK or EU
if (marketplace.includes('uk') || marketplace.includes('kingdom') || marketplace === 'gb' || marketplace === 'united kingdom') { const isUK = marketplace.includes('uk') ||
marketplace.includes('kingdom') ||
marketplace === 'gb' ||
marketplace === 'united kingdom' ||
marketplace === 'amazon.co.uk';
if (isUK) {
current.uk += stockValue; current.uk += stockValue;
} else if (marketplace) { } else {
// Assume everything else with a marketplace is Pan-EU (DE, IT, FR, ES) // Default to EU for any other marketplace or if marketplace is empty
// This ensures we don't lose data if the column detection is slightly off
current.eu += stockValue; current.eu += stockValue;
} }
} }
@@ -2485,8 +2500,10 @@ export const processUKInventoryExcel = async (
const row = jsonData[i]; const row = jsonData[i];
if (!row || row.length <= Math.max(finalAsinIdx, finalStockIdx)) continue; if (!row || row.length <= Math.max(finalAsinIdx, finalStockIdx)) continue;
const asin = String(row[finalAsinIdx] || '').trim().toUpperCase(); const asinRaw = String(row[finalAsinIdx] || '').trim().toUpperCase();
if (!asin) continue; const asin = (asinRaw.match(/B[A-Z0-9]{9}/) || [asinRaw])[0];
if (!asin || asin.length < 10) continue;
const stockValue = parseUnits(String(row[finalStockIdx] || '0')); const stockValue = parseUnits(String(row[finalStockIdx] || '0'));