diff --git a/components/DataGrid.tsx b/components/DataGrid.tsx index 6b8172f..8ce821c 100644 --- a/components/DataGrid.tsx +++ b/components/DataGrid.tsx @@ -282,7 +282,7 @@ const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData = const { years } = useMemo(() => { // We still need unique years for columns - const yearsSet = new Set(data.map(d => String((d as any).year))); + const yearsSet = new Set(data.map(d => String(d.year))); return { years: Array.from(yearsSet).sort((a, b) => parseInt(b) - parseInt(a)) }; }, [data]); @@ -451,6 +451,57 @@ const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData = return result; }, [pivotRows, rowFilters, sortConfig, years]); + // Calculate aggregated totals for the header row + const filteredTotalsByYear = useMemo(() => { + const result: { [year: string]: { sellOut: number, units: number, sellOutGrowth?: number, unitsGrowth?: number } } = {}; + + years.forEach((year, index) => { + const totals = processedRows.reduce( + (acc, row) => { + const data = row.totalsByYear[year]; + if (data) { + acc.sellOut += data.sellOut; + acc.units += data.units; + } + return acc; + }, + { sellOut: 0, units: 0 } + ); + + result[year] = totals; + + // Calculate YoY growth if previous year exists + const prevYear = years[index + 1]; + if (prevYear) { + const prevYearTotals = processedRows.reduce( + (acc, row) => { + const data = row.totalsByYear[prevYear]; + if (data) { + acc.sellOut += data.sellOut; + acc.units += data.units; + } + return acc; + }, + { sellOut: 0, units: 0 } + ); + + if (prevYearTotals.sellOut > 0) { + result[year].sellOutGrowth = ((totals.sellOut - prevYearTotals.sellOut) / prevYearTotals.sellOut) * 100; + } else if (totals.sellOut > 0) { + result[year].sellOutGrowth = 100; + } + + if (prevYearTotals.units > 0) { + result[year].unitsGrowth = ((totals.units - prevYearTotals.units) / prevYearTotals.units) * 100; + } else if (totals.units > 0) { + result[year].unitsGrowth = 100; + } + } + }); + + return result; + }, [processedRows, years]); + const paginatedRows = useMemo(() => { const start = (currentPage - 1) * ROWS_PER_PAGE; return processedRows.slice(start, start + ROWS_PER_PAGE); @@ -765,338 +816,299 @@ const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData = )} )} + - {/* Filter Totals Summary */} -
-
+ + {/* Ads Performance Summary - Shows when ads data is loaded */} + {adsSummary && showAdsMetrics && ( +
+
- - Filtered Totals: + + Advertising Data: + {adsSummary.recordCount.toLocaleString('de-DE')} records +
+
+
+ Ad Spend: + €{adsSummary.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })} +
+
+ Attr. Sales: + €{adsSummary.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })} +
+
+ ACOS: + + {adsSummary.acos.toFixed(1)}% + +
+
+ ROAS: + = 3 ? 'text-emerald-400' : adsSummary.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}> + {adsSummary.roas.toFixed(2)}x + +
- {years.map((year, index) => { - const yearTotals = processedRows.reduce( - (acc, row) => { - const data = row.totalsByYear[year]; - if (data) { - acc.sellOut += data.sellOut; - acc.units += data.units; - } - return acc; - }, - { sellOut: 0, units: 0 } - ); - - // Calculate YoY growth if previous year exists - const prevYear = years[index + 1]; - let sellOutGrowth = null; - let unitsGrowth = null; - - if (prevYear) { - const prevYearTotals = processedRows.reduce( - (acc, row) => { - const data = row.totalsByYear[prevYear]; - if (data) { - acc.sellOut += data.sellOut; - acc.units += data.units; - } - return acc; - }, - { sellOut: 0, units: 0 } - ); - - if (prevYearTotals.sellOut > 0) { - sellOutGrowth = ((yearTotals.sellOut - prevYearTotals.sellOut) / prevYearTotals.sellOut) * 100; - } else if (yearTotals.sellOut > 0) { - sellOutGrowth = 100; - } - - if (prevYearTotals.units > 0) { - unitsGrowth = ((yearTotals.units - prevYearTotals.units) / prevYearTotals.units) * 100; - } else if (yearTotals.units > 0) { - unitsGrowth = 100; - } - } - - return ( -
- {year} -
-
- S.O: - - €{yearTotals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })} - - {sellOutGrowth !== null && ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(sellOutGrowth).toFixed(1)}% - - )} -
-
- Units: - - {yearTotals.units.toLocaleString('de-DE')} - - {unitsGrowth !== null && ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(unitsGrowth).toFixed(1)}% - - )} -
-
-
- ); - })}
+ )} - - {/* Ads Performance Summary - Shows when ads data is loaded */} - {adsSummary && showAdsMetrics && ( -
-
-
- - Advertising Data: - {adsSummary.recordCount.toLocaleString('de-DE')} records -
-
-
- Ad Spend: - €{adsSummary.totalSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })} + {/* Data Table */} +
+ + + {/* NEW: Filtered Totals Header Row */} + + - - - {/* Growth Cells */} - {prevYear && ( - <> - - - - )} - - {/* Ads Data Cells */} - {showAdsMetrics && adsSummary && (() => { - const adsYearData = row.adsByYear?.[year]; - return ( - <> - - - - ); - })()} - - ); - })} - - ))} - - {paginatedRows.length === 0 && ( - - - - )} - -
+
+ + + + Filtered Totals
-
- Attr. Sales: - €{adsSummary.attributedSales.toLocaleString('de-DE', { maximumFractionDigits: 0 })} -
-
- ACOS: - - {adsSummary.acos.toFixed(1)}% - -
-
- ROAS: - = 3 ? 'text-emerald-400' : adsSummary.roas >= 2 ? 'text-amber-400' : 'text-red-400'}`}> - {adsSummary.roas.toFixed(2)}x - -
- - - - )} - - {/* Data Table */} -
- - - - {/* Dynamic Dimension Headers */} - {effectiveDimensions.map(dim => { - const label = DIMENSION_OPTIONS.find(d => d.value === dim)?.label || dim; - return ( - + {years.map((year, index) => { + const totals = filteredTotalsByYear[year]; + const prevYear = years[index + 1]; + return ( + + - ); - })} + + {prevYear && } + {showAdsMetrics && adsSummary && } + + ); + })} + + + {/* Dynamic Dimension Headers */} + {effectiveDimensions.map(dim => { + const label = DIMENSION_OPTIONS.find(d => d.value === dim)?.label || dim; + return ( + + ); + })} - {/* Data Columns: Year Groups */} + {/* Data Columns: Year Groups */} + {years.map((year, index) => { + const prevYear = years[index + 1]; // Since years are sorted desc: 2025, 2024... next index is prev year + return ( + + {/* Sell Out & Units */} + + + + {/* Growth Columns (if prev year exists) */} + {prevYear && ( + <> + + + + )} + + {/* Ads Columns - Only show when ads data exists and toggle is on */} + {showAdsMetrics && adsSummary && ( + <> + + + + )} + + ); + })} + + + + {paginatedRows.map((row) => ( + + {/* Dimension Values */} + {effectiveDimensions.map(dim => ( + + ))} + + {/* Metric Values */} {years.map((year, index) => { - const prevYear = years[index + 1]; // Since years are sorted desc: 2025, 2024... next index is prev year + const data = row.totalsByYear[year]; + const prevYear = years[index + 1]; + const prevData = prevYear ? row.totalsByYear[prevYear] : null; + + // Calculate Growth + let sellOutGrowth = null; + let unitsGrowth = null; + + if (prevYear) { + const currSO = data?.sellOut || 0; + const prevSO = prevData?.sellOut || 0; + if (prevSO !== 0) sellOutGrowth = ((currSO - prevSO) / prevSO) * 100; + else if (currSO > 0) sellOutGrowth = 100; // New entry + + const currUnits = data?.units || 0; + const prevUnits = prevData?.units || 0; + if (prevUnits !== 0) unitsGrowth = ((currUnits - prevUnits) / prevUnits) * 100; + else if (currUnits > 0) unitsGrowth = 100; + } + return ( - {/* Sell Out & Units */} - - + + - {/* Growth Columns (if prev year exists) */} + {/* Growth Cells */} {prevYear && ( <> - - + + )} - {/* Ads Columns - Only show when ads data exists and toggle is on */} - {showAdsMetrics && adsSummary && ( - <> - - - - )} + {/* Ads Data Cells */} + {showAdsMetrics && adsSummary && (() => { + const adsYearData = row.adsByYear?.[year]; + return ( + <> + + + + ); + })()} ); })} - - - {paginatedRows.map((row) => ( - - {/* Dimension Values */} - {effectiveDimensions.map(dim => ( - - ))} + ))} - {/* Metric Values */} - {years.map((year, index) => { - const data = row.totalsByYear[year]; - const prevYear = years[index + 1]; - const prevData = prevYear ? row.totalsByYear[prevYear] : null; + {paginatedRows.length === 0 && ( + + + + )} + +
requestSort(dim)} - > -
- {label} {getSortIcon(dim)} +
+
+ + €{totals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })} + + {totals.sellOutGrowth !== undefined && ( + = 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {totals.sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(totals.sellOutGrowth).toFixed(1)}% + + )}
+
+ + {totals.units.toLocaleString('de-DE')} + + {totals.unitsGrowth !== undefined && ( + = 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {totals.unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(totals.unitsGrowth).toFixed(1)}% + + )} +
+
requestSort(dim)} + > +
+ {label} {getSortIcon(dim)} +
+
requestSort(`total_sellOut_${year}`)} + > +
+ Sell Out {year} {getSortIcon(`total_sellOut_${year}`)} +
+
requestSort(`total_units_${year}`)} + > +
+ Units {year} {getSortIcon(`total_units_${year}`)} +
+
requestSort(`growth_sellOut_${year}_${prevYear}`)} + > +
+ S.O. Δ% {getSortIcon(`growth_sellOut_${year}_${prevYear}`)} +
+
requestSort(`growth_units_${year}_${prevYear}`)} + > +
+ Units Δ% {getSortIcon(`growth_units_${year}_${prevYear}`)} +
+
requestSort(`adSpend_${year}`)} + > +
+ Ad Spend {year} {getSortIcon(`adSpend_${year}`)} +
+
requestSort(`tacos_${year}`)} + > +
+ TACOS {year} {getSortIcon(`tacos_${year}`)} +
+
+ {dim === 'title' + ?
{row.title || '-'}
+ : (row[dim as keyof PivotRow] as string) || '-' + } +
requestSort(`total_sellOut_${year}`)} - > -
- Sell Out {year} {getSortIcon(`total_sellOut_${year}`)} -
-
requestSort(`total_units_${year}`)} - > -
- Units {year} {getSortIcon(`total_units_${year}`)} -
-
+ {data ? `€${data.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'} + + {data ? data.units.toLocaleString('de-DE') : '-'} + requestSort(`growth_sellOut_${year}_${prevYear}`)} - > -
- S.O. Δ% {getSortIcon(`growth_sellOut_${year}_${prevYear}`)} -
-
requestSort(`growth_units_${year}_${prevYear}`)} - > -
- Units Δ% {getSortIcon(`growth_units_${year}_${prevYear}`)} -
-
= 0 ? 'text-emerald-500' : 'text-red-500') : 'text-slate-600'}`}> + {sellOutGrowth !== null ? ( + + {sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(sellOutGrowth).toFixed(0)}% + + ) : '-'} + = 0 ? 'text-emerald-500' : 'text-red-500') : 'text-slate-600'}`}> + {unitsGrowth !== null ? ( + + {unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(unitsGrowth).toFixed(0)}% + + ) : '-'} + requestSort(`adSpend_${year}`)} - > -
- Ad Spend {year} {getSortIcon(`adSpend_${year}`)} -
-
requestSort(`tacos_${year}`)} - > -
- TACOS {year} {getSortIcon(`tacos_${year}`)} -
-
+ {adsYearData ? `€${adsYearData.adSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'} + + {adsYearData ? `${adsYearData.tacos.toFixed(1)}%` : '-'} +
- {dim === 'title' - ?
{row.title || '-'}
- : (row[dim as keyof PivotRow] as string) || '-' - } -
+ No data matches your filters. +
+
- // Calculate Growth - let sellOutGrowth = null; - let unitsGrowth = null; - - if (prevYear) { - const currSO = data?.sellOut || 0; - const prevSO = prevData?.sellOut || 0; - if (prevSO !== 0) sellOutGrowth = ((currSO - prevSO) / prevSO) * 100; - else if (currSO > 0) sellOutGrowth = 100; // New entry - - const currUnits = data?.units || 0; - const prevUnits = prevData?.units || 0; - if (prevUnits !== 0) unitsGrowth = ((currUnits - prevUnits) / prevUnits) * 100; - else if (currUnits > 0) unitsGrowth = 100; - } - - return ( - -
- {data ? `€${data.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'} - - {data ? data.units.toLocaleString('de-DE') : '-'} - = 0 ? 'text-emerald-500' : 'text-red-500') : 'text-slate-600'}`}> - {sellOutGrowth !== null ? ( - - {sellOutGrowth >= 0 ? '↑' : '↓'} {Math.abs(sellOutGrowth).toFixed(0)}% - - ) : '-'} - = 0 ? 'text-emerald-500' : 'text-red-500') : 'text-slate-600'}`}> - {unitsGrowth !== null ? ( - - {unitsGrowth >= 0 ? '↑' : '↓'} {Math.abs(unitsGrowth).toFixed(0)}% - - ) : '-'} - - {adsYearData ? `€${adsYearData.adSpend.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'} - - {adsYearData ? `${adsYearData.tacos.toFixed(1)}%` : '-'} -
- No data matches your filters. -
+ {/* Pagination */} +
+
+ Page {currentPage} of {totalPages || 1}
- - {/* Pagination */} -
-
- Page {currentPage} of {totalPages || 1} -
-
- - -
+
+ +