diff --git a/src/components/reports/DynamicReportTable.tsx b/src/components/reports/DynamicReportTable.tsx index 4f76cc5..53af951 100644 --- a/src/components/reports/DynamicReportTable.tsx +++ b/src/components/reports/DynamicReportTable.tsx @@ -1,4 +1,4 @@ -import { Fragment, useState } from "react"; +import { Fragment, useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { ArrowUpDown } from "lucide-react"; import type { PivotConfig, PivotResult, PivotResultRow } from "../../shared/types"; @@ -13,19 +13,54 @@ interface DynamicReportTableProps { result: PivotResult; } +/** A pivoted row: one per unique combination of row dimensions */ +interface PivotedRow { + rowKeys: Record; // row-dimension values + cells: Record>; // colValue → measure → value +} + +/** Pivot raw result rows into one PivotedRow per unique row-key combination */ +function pivotRows( + rows: PivotResultRow[], + rowDims: string[], + colDim: string | undefined, + measures: string[], +): PivotedRow[] { + const map = new Map(); + + for (const row of rows) { + const rowKey = rowDims.map((d) => row.keys[d] || "").join("\0"); + let pivoted = map.get(rowKey); + if (!pivoted) { + const rowKeys: Record = {}; + for (const d of rowDims) rowKeys[d] = row.keys[d] || ""; + pivoted = { rowKeys, cells: {} }; + map.set(rowKey, pivoted); + } + + const colKey = colDim ? (row.keys[colDim] || "") : "__all__"; + if (!pivoted.cells[colKey]) pivoted.cells[colKey] = {}; + for (const m of measures) { + pivoted.cells[colKey][m] = (pivoted.cells[colKey][m] || 0) + (row.measures[m] || 0); + } + } + + return Array.from(map.values()); +} + interface GroupNode { key: string; label: string; - rows: PivotResultRow[]; + pivotedRows: PivotedRow[]; children: GroupNode[]; } -function buildGroups(rows: PivotResultRow[], rowDims: string[], depth: number): GroupNode[] { +function buildGroups(rows: PivotedRow[], rowDims: string[], depth: number): GroupNode[] { if (depth >= rowDims.length) return []; const dim = rowDims[depth]; - const map = new Map(); + const map = new Map(); for (const row of rows) { - const key = row.keys[dim] || ""; + const key = row.rowKeys[dim] || ""; if (!map.has(key)) map.set(key, []); map.get(key)!.push(row); } @@ -34,21 +69,23 @@ function buildGroups(rows: PivotResultRow[], rowDims: string[], depth: number): groups.push({ key, label: key, - rows: groupRows, + pivotedRows: groupRows, children: buildGroups(groupRows, rowDims, depth + 1), }); } return groups; } -function computeSubtotals(rows: PivotResultRow[], measures: string[], colDim: string | undefined): Record> { - // colValue → measure → sum +function computeSubtotals( + rows: PivotedRow[], + measures: string[], + colValues: string[], +): Record> { const result: Record> = {}; - for (const row of rows) { - const colKey = colDim ? (row.keys[colDim] || "") : "__all__"; - if (!result[colKey]) result[colKey] = {}; + for (const colVal of colValues) { + result[colVal] = {}; for (const m of measures) { - result[colKey][m] = (result[colKey][m] || 0) + (row.measures[m] || 0); + result[colVal][m] = rows.reduce((sum, r) => sum + (r.cells[colVal]?.[m] || 0), 0); } } return result; @@ -69,7 +106,18 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl }); }; - if (result.rows.length === 0) { + const rowDims = config.rows; + const colDim = config.columns[0] || undefined; + const colValues = colDim ? result.columnValues : ["__all__"]; + const measures = config.values; + + // Pivot the flat SQL rows into one PivotedRow per unique row-key combo + const pivotedRows = useMemo( + () => pivotRows(result.rows, rowDims, colDim, measures), + [result.rows, rowDims, colDim, measures], + ); + + if (pivotedRows.length === 0) { return (
{t("reports.pivot.noData")} @@ -77,16 +125,8 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl ); } - const rowDims = config.rows; - const colDim = config.columns[0] || undefined; - const colValues = colDim ? result.columnValues : ["__all__"]; - const measures = config.values; - - // Build row groups from first row dimension - const groups = rowDims.length > 0 ? buildGroups(result.rows, rowDims, 0) : []; - - // Grand totals - const grandTotals = computeSubtotals(result.rows, measures, colDim); + const groups = rowDims.length > 0 ? buildGroups(pivotedRows, rowDims, 0) : []; + const grandTotals = computeSubtotals(pivotedRows, measures, colValues); const fieldLabel = (id: string) => t(`reports.pivot.${id === "level1" ? "level1" : id === "level2" ? "level2" : id === "type" ? "categoryType" : id}`); const measureLabel = (id: string) => t(`reports.pivot.${id}`); @@ -108,17 +148,15 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl - {/* Row dimension headers */} {rowDims.map((dim) => ( ))} - {/* Column headers: colValue × measure */} {colValues.map((colVal) => measures.map((m) => ( )) )} @@ -126,17 +164,13 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl {rowDims.length === 0 ? ( - // No row dims — single row with totals {colValues.map((colVal) => - measures.map((m) => { - const val = grandTotals[colVal]?.[m] || 0; - return ( - - ); - }) + measures.map((m) => ( + + )) )} ) : ( @@ -144,7 +178,6 @@ export default function DynamicReportTable({ config, result }: DynamicReportTabl 1 && !isLeafLevel ? ( @@ -210,25 +241,26 @@ function GroupRows({ ) : null; if (isLeafLevel) { - // Render leaf rows: one per unique combination of remaining keys + // Render one table row per pivoted row (already deduplicated by row keys) return ( <> - {group.rows.map((row, i) => ( + {group.pivotedRows.map((pRow, i) => ( {rowDims.map((dim, di) => ( - ))} {colValues.map((colVal) => - measures.map((m) => { - const matchesCol = !colDim || row.keys[colDim] === colVal; - return ( - - ); - }) + measures.map((m) => ( + + )) )} ))} @@ -240,7 +272,6 @@ function GroupRows({
{fieldLabel(dim)} - {colDim ? `${colVal} — ${measureLabel(m)}` : measureLabel(m)} + {colDim ? (measures.length > 1 ? `${colVal} — ${measureLabel(m)}` : colVal) : measureLabel(m)}
- {cadFormatter(val)} - + {cadFormatter(grandTotals[colVal]?.[m] || 0)} +
- {di === depth ? row.keys[dim] || "" : di > depth ? row.keys[dim] || "" : ""} + + {pRow.rowKeys[dim] || ""} - {matchesCol ? cadFormatter(row.measures[m] || 0) : ""} - + {cadFormatter(pRow.cells[colVal]?.[m] || 0)} +