diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index 86e148c..8788a05 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -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 = ({ {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 ( = ({ - {isHovered && tooltipPos && ( -
-
{week}
- - {/* Semana anterior */} -
-
Semana anterior ({weeks[idx + 1] || '—'})
- {prevWeekTotals ? ( -
- Units - {prevWeekTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, prevWeekTotals.units)} - Revenue - €{prevWeekTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, prevWeekTotals.revenue)} - Ads Spend - €{prevWeekTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, prevWeekTotals.spend)} - GV - {prevWeekTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, prevWeekTotals.gv)} -
- ) : ( - Sin datos - )} -
- -
-
Misma semana año anterior ({prevYearWeek})
- {yoyTotals && (yoyTotals.units > 0 || yoyTotals.revenue > 0) ? ( -
- Units - {yoyTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, yoyTotals.units)} - Revenue - €{yoyTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, yoyTotals.revenue)} - Ads Spend - €{yoyTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, yoyTotals.spend)} - GV - {yoyTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.revenue || 0, yoyTotals.gv)} -
- ) : ( - Sin datos del año anterior - )} -
-
- )} ); })} @@ -928,6 +881,64 @@ const WeeklyGrid: React.FC = ({ ); + + 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( +
+
{week}
+ +
+
Semana anterior ({weeks[idx + 1] || '—'})
+ {prevWeekTotals ? ( +
+ Units + {prevWeekTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, prevWeekTotals.units)} + Revenue + €{prevWeekTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, prevWeekTotals.revenue)} + Ads Spend + €{prevWeekTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, prevWeekTotals.spend)} + GV + {prevWeekTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, prevWeekTotals.gv)} +
+ ) : ( + Sin datos + )} +
+ +
+
Misma semana año anterior ({prevYearWeek})
+ {yoyTotals && (yoyTotals.units > 0 || yoyTotals.revenue > 0) ? ( +
+ Units + {yoyTotals.units.toLocaleString('de-DE')} {renderGrowth(cur?.units || 0, yoyTotals.units)} + Revenue + €{yoyTotals.revenue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.revenue || 0, yoyTotals.revenue)} + Ads Spend + €{yoyTotals.spend.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {renderGrowth(cur?.spend || 0, yoyTotals.spend)} + GV + {yoyTotals.gv.toLocaleString('de-DE')} {renderGrowth(cur?.gv || 0, yoyTotals.gv)} +
+ ) : ( + Sin datos del año anterior + )} +
+
, + document.body + ); + })() : null; + + return <>{mainContent}{tooltipPortal}; }; export default WeeklyGrid; diff --git a/package-lock.json b/package-lock.json index 390ee97..6c11406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 29b22fd..6d59669 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 8e193b5..885c031 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -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'));