mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 10:55:24 +02:00
Fix: improve Vendor stock parsing robustness and ASIN extraction for B0D14YBNWB
This commit is contained in:
+59
-48
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { CombinedKPIs, ColumnFilterCondition, SalesRecord } from '../types';
|
||||
import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES, checkNumericConditions, filterData } from '../services/dataProcessor';
|
||||
@@ -782,12 +783,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
</div>
|
||||
</th>
|
||||
{weeks.map((week, idx) => {
|
||||
const [yearStr, weekNum] = week.split('-');
|
||||
const prevYearWeek = `${parseInt(yearStr) - 1}-${weekNum}`;
|
||||
const prevWeekTotals = allWeekTotals[weeks[idx + 1]];
|
||||
const yoyTotals = allWeekTotals[prevYearWeek];
|
||||
const cur = weekTotals[week];
|
||||
const isHovered = hoveredTotalWeek === week;
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
@@ -928,6 +881,64 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
||||
</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;
|
||||
|
||||
Generated
+29
@@ -23,6 +23,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vercel/node": "^5.5.22",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
@@ -1741,6 +1742,27 @@
|
||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
||||
"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": {
|
||||
"version": "0.0.6",
|
||||
"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_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": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vercel/node": "^5.5.22",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
|
||||
+27
-10
@@ -2327,15 +2327,21 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
|
||||
|
||||
if (headerRowIndex === -1) {
|
||||
// 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;
|
||||
else {
|
||||
headerRowIndex = jsonData.findIndex(row =>
|
||||
row && row.some(cell => String(cell || '').toUpperCase().includes('ASIN'))
|
||||
);
|
||||
|
||||
if (headerRowIndex === -1) {
|
||||
console.warn("Could not find header row in Vendor Stock Excel");
|
||||
return vendorStockMap;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const marketplaceIdx = headers.findIndex(h => {
|
||||
@@ -2396,8 +2402,10 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
|
||||
const row = jsonData[i];
|
||||
if (!row || row.length <= Math.max(finalAsinIdx, finalMarketplaceIdx, finalStockIdx)) continue;
|
||||
|
||||
const asin = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
||||
if (!asin) continue;
|
||||
const asinRaw = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
||||
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 stockValue = parseUnits(String(row[finalStockIdx] || '0'));
|
||||
@@ -2409,10 +2417,17 @@ export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer):
|
||||
const current = vendorStockMap.get(asin)!;
|
||||
|
||||
// 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;
|
||||
} else if (marketplace) {
|
||||
// Assume everything else with a marketplace is Pan-EU (DE, IT, FR, ES)
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -2485,8 +2500,10 @@ export const processUKInventoryExcel = async (
|
||||
const row = jsonData[i];
|
||||
if (!row || row.length <= Math.max(finalAsinIdx, finalStockIdx)) continue;
|
||||
|
||||
const asin = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
||||
if (!asin) continue;
|
||||
const asinRaw = String(row[finalAsinIdx] || '').trim().toUpperCase();
|
||||
const asin = (asinRaw.match(/B[A-Z0-9]{9}/) || [asinRaw])[0];
|
||||
if (!asin || asin.length < 10) continue;
|
||||
|
||||
|
||||
const stockValue = parseUnits(String(row[finalStockIdx] || '0'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user