mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:45:23 +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 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;
|
||||||
|
|||||||
Generated
+29
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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'));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user