diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index a55c2a3..1e2eb89 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import * as XLSX from 'xlsx'; import { CombinedKPIs } from '../types'; import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor'; @@ -196,21 +196,26 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown }); }, []); - // Calculate totals in a SINGLE PASS (O(rows) instead of O(weeks × rows)) + // Calculate totals in a SINGLE PASS const weekTotals = useMemo(() => { const totals: { [weekKey: string]: { units: number, spend: number, gv: number } } = {}; - // Initialize all weeks - weeks.forEach(week => { - totals[week] = { units: 0, spend: 0, gv: 0 }; - }); + const visibleWeeks = weeks; + + // Initialize visible weeks + for (let i = 0; i < visibleWeeks.length; i++) { + totals[visibleWeeks[i]] = { units: 0, spend: 0, gv: 0 }; + } + // Single pass through rows - rows.forEach(row => { - weeks.forEach(week => { - totals[week].units += (row.unitsByWeek[week] || 0); - totals[week].spend += (row.spendByWeek[week] || 0); - totals[week].gv += (row.gvByWeek?.[week] || 0); - }); - }); + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + for (let j = 0; j < visibleWeeks.length; j++) { + const w = visibleWeeks[j]; + totals[w].units += (row.unitsByWeek[w] || 0); + totals[w].spend += (row.spendByWeek[w] || 0); + totals[w].gv += (row.gvByWeek?.[w] || 0); + } + } return totals; }, [rows, weeks]); @@ -274,41 +279,36 @@ const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown // 2. Sort results const sortedRows = useMemo(() => { - if (!sortConfig) return filteredRows; + if (!sortConfig || filteredRows.length === 0) return filteredRows; const result = [...filteredRows]; - const metricKey = sortConfig.metric === 'units' ? 'unitsByWeek' : - sortConfig.metric === 'spend' ? 'spendByWeek' : 'gvByWeek'; - const weekKey = sortConfig.key; - const direction = sortConfig.direction; + const { key: weekKey, direction, metric } = sortConfig; - result.sort((a, b) => { - if (sortConfig.metric === 'rank') { + if (metric === 'rank') { + if (!top50Ranking) return result; + const rankMap = top50Mode === 'eu' ? top50Ranking.eu : top50Ranking.uk; + + result.sort((a, b) => { const asinA = a.asin.trim().toUpperCase(); const asinB = b.asin.trim().toUpperCase(); - - let rankA = 999; - let rankB = 999; - - if (top50Ranking) { - if (top50Mode === 'eu') { - rankA = top50Ranking.eu.get(asinA) || 999; - rankB = top50Ranking.eu.get(asinB) || 999; - } else { - rankA = top50Ranking.uk.get(asinA) || 999; - rankB = top50Ranking.uk.get(asinB) || 999; - } - } - + const rankA = rankMap.get(asinA) || 999; + const rankB = rankMap.get(asinB) || 999; return direction === 'asc' ? rankA - rankB : rankB - rankA; - } - const valA = a[metricKey][weekKey] || 0; - const valB = b[metricKey][weekKey] || 0; - return direction === 'asc' ? valA - valB : valB - valA; - }); + }); + } else { + const metricKey = metric === 'units' ? 'unitsByWeek' : + metric === 'spend' ? 'spendByWeek' : 'gvByWeek'; + + result.sort((a, b) => { + const valA = a[metricKey][weekKey] || 0; + const valB = b[metricKey][weekKey] || 0; + if (valA === valB) return 0; + return direction === 'asc' ? valA - valB : valB - valA; + }); + } return result; - }, [filteredRows, sortConfig]); + }, [filteredRows, sortConfig, top50Ranking, top50Mode]); // 3. Paginate const paginatedRows = useMemo(() => { diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 8d41c7d..dfaf31c 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1483,20 +1483,31 @@ export const pivotWeeklySalesData = (data: CombinedKPIs[]): { rows: WeeklyPivotRow[], weeks: string[] } => { - // 1. Identify all unique weeks and sort descending (YYYY-WW) - const weekKeys = new Set(); - data.forEach(d => { - if (d.week) { - const weekKey = `${d.year}-${String(d.week).padStart(2, '0')}`; - weekKeys.add(weekKey); - } - }); - const sortedWeeks = Array.from(weekKeys).sort((a, b) => b.localeCompare(a)); - + const weekKeysSet = new Set(); const map = new Map(); - for (let i = 0; i < data.length; i++) { + // Cache week keys to avoid repeated string formatting + // Key: year|week, Value: YYYY-WW + const weekCache = new Map(); + + const getWeekKey = (year: number, week: number) => { + const cacheKey = `${year}|${week}`; + let k = weekCache.get(cacheKey); + if (!k) { + k = `${year}-${String(week).padStart(2, '0')}`; + weekCache.set(cacheKey, k); + } + return k; + }; + + const len = data.length; + for (let i = 0; i < len; i++) { const record = data[i]; + if (!record.week) continue; + + const weekKey = getWeekKey(record.year, record.week); + weekKeysSet.add(weekKey); + const key = record.asin || record.sku || `${record.title}-${record.line}`; if (!key) continue; @@ -1516,14 +1527,13 @@ export const pivotWeeklySalesData = (data: CombinedKPIs[]): { map.set(key, row); } - if (record.week) { - const weekKey = `${record.year}-${String(record.week).padStart(2, '0')}`; - row.unitsByWeek[weekKey] = (row.unitsByWeek[weekKey] || 0) + (record.unitsTotal || 0); - row.spendByWeek[weekKey] = (row.spendByWeek[weekKey] || 0) + (record.cost || 0); - row.gvByWeek[weekKey] = (row.gvByWeek[weekKey] || 0) + (record.glanceViews || 0); - } + row.unitsByWeek[weekKey] = (row.unitsByWeek[weekKey] || 0) + (record.unitsTotal || 0); + row.spendByWeek[weekKey] = (row.spendByWeek[weekKey] || 0) + (record.cost || 0); + row.gvByWeek[weekKey] = (row.gvByWeek[weekKey] || 0) + (record.glanceViews || 0); } + const sortedWeeks = Array.from(weekKeysSet).sort((a, b) => b.localeCompare(a)); + return { rows: Array.from(map.values()), weeks: sortedWeeks