wip
This commit is contained in:
@@ -23,6 +23,348 @@ export type GroupedTableRow = TableRow & {
|
||||
breakdownDisplay: (string | null)[]; // null means show empty cell
|
||||
};
|
||||
|
||||
/**
|
||||
* Row type that supports TanStack Table's expanding feature
|
||||
* Can represent both group header rows and data rows
|
||||
*/
|
||||
export type ExpandableTableRow = TableRow & {
|
||||
subRows?: ExpandableTableRow[];
|
||||
isGroupHeader?: boolean; // True if this is a group header row
|
||||
groupValue?: string; // The value this group represents
|
||||
groupLevel?: number; // The level in the hierarchy (0-based)
|
||||
breakdownDisplay?: (string | null)[]; // For display purposes
|
||||
};
|
||||
|
||||
/**
|
||||
* Hierarchical group structure for better collapse/expand functionality
|
||||
*/
|
||||
export type GroupedItem<T> = {
|
||||
group: string;
|
||||
items: Array<GroupedItem<T> | T>;
|
||||
level: number;
|
||||
groupKey: string; // Unique key for this group (path-based)
|
||||
parentGroupKey?: string; // Key of parent group
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform flat array of items with hierarchical names into nested group structure
|
||||
* This creates a tree structure that makes it easier to toggle specific groups
|
||||
*/
|
||||
export function groupByNames<T extends { names: string[] }>(
|
||||
items: T[],
|
||||
): Array<GroupedItem<T>> {
|
||||
const rootGroups = new Map<string, GroupedItem<T>>();
|
||||
|
||||
for (const item of items) {
|
||||
const names = item.names;
|
||||
if (names.length === 0) continue;
|
||||
|
||||
// Start with the first level (serie name, level -1)
|
||||
const firstLevel = names[0]!;
|
||||
const rootGroupKey = firstLevel;
|
||||
|
||||
if (!rootGroups.has(firstLevel)) {
|
||||
rootGroups.set(firstLevel, {
|
||||
group: firstLevel,
|
||||
items: [],
|
||||
level: -1, // Serie level
|
||||
groupKey: rootGroupKey,
|
||||
});
|
||||
}
|
||||
|
||||
const rootGroup = rootGroups.get(firstLevel)!;
|
||||
|
||||
// Navigate/create nested groups for remaining levels (breakdowns, level 0+)
|
||||
let currentGroup = rootGroup;
|
||||
let parentGroupKey = rootGroupKey;
|
||||
|
||||
for (let i = 1; i < names.length; i++) {
|
||||
const levelName = names[i]!;
|
||||
const groupKey = `${parentGroupKey}:${levelName}`;
|
||||
const level = i - 1; // Breakdown levels start at 0
|
||||
|
||||
// Find existing group at this level
|
||||
const existingGroup = currentGroup.items.find(
|
||||
(child): child is GroupedItem<T> =>
|
||||
typeof child === 'object' &&
|
||||
'group' in child &&
|
||||
child.group === levelName &&
|
||||
'level' in child &&
|
||||
child.level === level,
|
||||
);
|
||||
|
||||
if (existingGroup) {
|
||||
currentGroup = existingGroup;
|
||||
parentGroupKey = groupKey;
|
||||
} else {
|
||||
// Create new group at this level
|
||||
const newGroup: GroupedItem<T> = {
|
||||
group: levelName,
|
||||
items: [],
|
||||
level,
|
||||
groupKey,
|
||||
parentGroupKey,
|
||||
};
|
||||
currentGroup.items.push(newGroup);
|
||||
currentGroup = newGroup;
|
||||
parentGroupKey = groupKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the actual item to the deepest group
|
||||
currentGroup.items.push(item);
|
||||
}
|
||||
|
||||
return Array.from(rootGroups.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a grouped structure back into a flat array of items
|
||||
* Useful for getting all items in a group or its children
|
||||
*/
|
||||
export function flattenGroupedItems<T>(
|
||||
groupedItems: Array<GroupedItem<T> | T>,
|
||||
): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (const item of groupedItems) {
|
||||
if (item && typeof item === 'object' && 'items' in item) {
|
||||
// It's a group, recursively flatten its items
|
||||
result.push(...flattenGroupedItems(item.items));
|
||||
} else if (item) {
|
||||
// It's an actual item
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a group by its groupKey in a nested structure
|
||||
*/
|
||||
export function findGroup<T>(
|
||||
groups: Array<GroupedItem<T>>,
|
||||
groupKey: string,
|
||||
): GroupedItem<T> | null {
|
||||
for (const group of groups) {
|
||||
if (group.groupKey === groupKey) {
|
||||
return group;
|
||||
}
|
||||
|
||||
// Search in nested groups
|
||||
for (const item of group.items) {
|
||||
if (item && typeof item === 'object' && 'items' in item) {
|
||||
const found = findGroup([item], groupKey);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hierarchical groups to TanStack Table's expandable row format
|
||||
* This creates rows with subRows that TanStack Table can expand/collapse natively
|
||||
*/
|
||||
export function groupsToExpandableRows(
|
||||
groups: Array<GroupedItem<TableRow>>,
|
||||
breakdownCount: number,
|
||||
): ExpandableTableRow[] {
|
||||
const result: ExpandableTableRow[] = [];
|
||||
|
||||
function processGroup(
|
||||
group: GroupedItem<TableRow>,
|
||||
parentPath: string[] = [],
|
||||
): ExpandableTableRow[] {
|
||||
const currentPath = [...parentPath, group.group];
|
||||
const subRows: ExpandableTableRow[] = [];
|
||||
|
||||
// Separate nested groups from actual items
|
||||
const nestedGroups: GroupedItem<TableRow>[] = [];
|
||||
const actualItems: TableRow[] = [];
|
||||
|
||||
for (const item of group.items) {
|
||||
if (item && typeof item === 'object' && 'items' in item) {
|
||||
nestedGroups.push(item);
|
||||
} else if (item) {
|
||||
actualItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Process nested groups (they become subRows)
|
||||
for (const nestedGroup of nestedGroups) {
|
||||
subRows.push(...processGroup(nestedGroup, currentPath));
|
||||
}
|
||||
|
||||
// Process actual items
|
||||
actualItems.forEach((item, index) => {
|
||||
const breakdownDisplay: (string | null)[] = [];
|
||||
const breakdownValues = item.breakdownValues;
|
||||
|
||||
// Build breakdownDisplay based on hierarchy
|
||||
if (index === 0) {
|
||||
// First row shows all breakdown values
|
||||
for (let i = 0; i < breakdownCount; i++) {
|
||||
breakdownDisplay.push(breakdownValues[i] ?? null);
|
||||
}
|
||||
} else {
|
||||
// Subsequent rows: show values from parent path, then item values
|
||||
for (let i = 0; i < breakdownCount; i++) {
|
||||
if (i < currentPath.length) {
|
||||
breakdownDisplay.push(currentPath[i] ?? null);
|
||||
} else if (i < breakdownValues.length) {
|
||||
breakdownDisplay.push(breakdownValues[i] ?? null);
|
||||
} else {
|
||||
breakdownDisplay.push(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subRows.push({
|
||||
...item,
|
||||
breakdownDisplay,
|
||||
groupKey: group.groupKey,
|
||||
parentGroupKey: group.parentGroupKey,
|
||||
// Explicitly mark as NOT a group header or summary row
|
||||
isGroupHeader: false,
|
||||
isSummaryRow: false,
|
||||
});
|
||||
});
|
||||
|
||||
// If this group has subRows and is not the last breakdown level, create a group header row
|
||||
// Don't create group headers for the last breakdown level (level === breakdownCount)
|
||||
// because it would just duplicate the rows
|
||||
const shouldCreateGroupHeader =
|
||||
subRows.length > 0 &&
|
||||
(group.level < breakdownCount || group.level === -1); // -1 is serie level
|
||||
|
||||
if (shouldCreateGroupHeader) {
|
||||
// Create a summary row for the group
|
||||
const groupItems = flattenGroupedItems(group.items);
|
||||
const summaryRow = createSummaryRow(
|
||||
groupItems,
|
||||
group.groupKey,
|
||||
breakdownCount,
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
...summaryRow,
|
||||
isGroupHeader: true,
|
||||
groupValue: group.group,
|
||||
groupLevel: group.level,
|
||||
subRows,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return subRows;
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
result.push(...processGroup(group));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hierarchical groups to flat table rows, respecting collapsed groups
|
||||
* This creates GroupedTableRow entries with proper breakdownDisplay values
|
||||
* @deprecated Use groupsToExpandableRows with TanStack Table's expanding feature instead
|
||||
*/
|
||||
export function groupsToTableRows<T extends TableRow>(
|
||||
groups: Array<GroupedItem<T>>,
|
||||
collapsedGroups: Set<string>,
|
||||
breakdownCount: number,
|
||||
): GroupedTableRow[] {
|
||||
const rows: GroupedTableRow[] = [];
|
||||
|
||||
function processGroup(
|
||||
group: GroupedItem<T>,
|
||||
parentPath: string[] = [],
|
||||
parentGroupKey?: string,
|
||||
): void {
|
||||
const isGroupCollapsed = collapsedGroups.has(group.groupKey);
|
||||
const currentPath = [...parentPath, group.group];
|
||||
|
||||
if (isGroupCollapsed) {
|
||||
// Group is collapsed - add summary row
|
||||
const groupItems = flattenGroupedItems(group.items);
|
||||
if (groupItems.length > 0) {
|
||||
const summaryRow = createSummaryRow(
|
||||
groupItems,
|
||||
group.groupKey,
|
||||
breakdownCount,
|
||||
);
|
||||
rows.push(summaryRow);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Group is expanded - process items
|
||||
// Separate nested groups from actual items
|
||||
const nestedGroups: GroupedItem<T>[] = [];
|
||||
const actualItems: T[] = [];
|
||||
|
||||
for (const item of group.items) {
|
||||
if (item && typeof item === 'object' && 'items' in item) {
|
||||
nestedGroups.push(item);
|
||||
} else if (item) {
|
||||
actualItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Process actual items first
|
||||
actualItems.forEach((item, index) => {
|
||||
const breakdownDisplay: (string | null)[] = [];
|
||||
const breakdownValues = item.breakdownValues;
|
||||
|
||||
// For the first item in the group, show all breakdown values
|
||||
// For subsequent items, show values based on hierarchy
|
||||
if (index === 0) {
|
||||
// First row shows all breakdown values
|
||||
for (let i = 0; i < breakdownCount; i++) {
|
||||
breakdownDisplay.push(breakdownValues[i] ?? null);
|
||||
}
|
||||
} else {
|
||||
// Subsequent rows: show values from parent path, then item values
|
||||
for (let i = 0; i < breakdownCount; i++) {
|
||||
if (i < currentPath.length) {
|
||||
// Show value from parent group path
|
||||
breakdownDisplay.push(currentPath[i] ?? null);
|
||||
} else if (i < breakdownValues.length) {
|
||||
// Show current breakdown value from the item
|
||||
breakdownDisplay.push(breakdownValues[i] ?? null);
|
||||
} else {
|
||||
breakdownDisplay.push(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.push({
|
||||
...item,
|
||||
breakdownDisplay,
|
||||
groupKey: group.groupKey,
|
||||
parentGroupKey: group.parentGroupKey,
|
||||
});
|
||||
});
|
||||
|
||||
// Process nested groups
|
||||
for (const nestedGroup of nestedGroups) {
|
||||
processGroup(nestedGroup, currentPath, group.groupKey);
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
processGroup(group);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract unique dates from all series
|
||||
*/
|
||||
@@ -88,8 +430,40 @@ export function createFlatRows(
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform series into grouped table rows
|
||||
* Transform series into hierarchical groups
|
||||
* Uses the new groupByNames function for better structure
|
||||
* Groups by serie name first, then by breakdown values
|
||||
*/
|
||||
export function createGroupedRowsHierarchical(
|
||||
series: IChartData['series'],
|
||||
dates: string[],
|
||||
): Array<GroupedItem<TableRow>> {
|
||||
const flatRows = createFlatRows(series, dates);
|
||||
|
||||
// Sort by sum descending before grouping
|
||||
flatRows.sort((a, b) => b.sum - a.sum);
|
||||
|
||||
const breakdownCount = flatRows[0]?.breakdownValues.length ?? 0;
|
||||
|
||||
if (breakdownCount === 0) {
|
||||
// No breakdowns - return empty array (will be handled as flat rows)
|
||||
return [];
|
||||
}
|
||||
|
||||
// Create hierarchical groups using groupByNames
|
||||
// Group by serie name first, then by breakdown values
|
||||
const itemsWithNames = flatRows.map((row) => ({
|
||||
...row,
|
||||
names: [row.serieName, ...row.breakdownValues], // Serie name + breakdown values
|
||||
}));
|
||||
|
||||
return groupByNames(itemsWithNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform series into grouped table rows (legacy flat format)
|
||||
* Groups rows hierarchically by breakdown values
|
||||
* @deprecated Use createGroupedRowsHierarchical + groupsToTableRows instead
|
||||
*/
|
||||
export function createGroupedRows(
|
||||
series: IChartData['series'],
|
||||
@@ -318,3 +692,46 @@ export function transformToTableData(
|
||||
breakdownPropertyNames,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform chart data into hierarchical groups
|
||||
* Returns hierarchical structure for better group management
|
||||
*/
|
||||
export function transformToHierarchicalGroups(
|
||||
data: IChartData,
|
||||
breakdowns: Array<{ name: string }>,
|
||||
): {
|
||||
groups: Array<GroupedItem<TableRow>>;
|
||||
dates: string[];
|
||||
breakdownPropertyNames: string[];
|
||||
} {
|
||||
const dates = getUniqueDates(data.series);
|
||||
const originalBreakdownPropertyNames = getBreakdownPropertyNames(
|
||||
data.series,
|
||||
breakdowns,
|
||||
);
|
||||
|
||||
// Reorder breakdowns by unique count (fewest first)
|
||||
const { reorderedNames: breakdownPropertyNames, reorderMap } =
|
||||
reorderBreakdownsByUniqueCount(data.series, originalBreakdownPropertyNames);
|
||||
|
||||
// Reorder breakdown values in series before creating rows
|
||||
const reorderedSeries = data.series.map((serie) => {
|
||||
const reorderedNames = [
|
||||
serie.names[0], // Keep serie name first
|
||||
...reorderMap.map((oldIndex) => serie.names[oldIndex + 1] ?? ''), // Reorder breakdown values
|
||||
];
|
||||
return {
|
||||
...serie,
|
||||
names: reorderedNames,
|
||||
};
|
||||
});
|
||||
|
||||
const groups = createGroupedRowsHierarchical(reorderedSeries, dates);
|
||||
|
||||
return {
|
||||
groups,
|
||||
dates,
|
||||
breakdownPropertyNames,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import type { ColumnDef, Header, Row } from '@tanstack/react-table';
|
||||
import {
|
||||
type ExpandedState,
|
||||
type SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getExpandedRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
@@ -25,9 +27,13 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ReportTableToolbar } from './report-table-toolbar';
|
||||
import {
|
||||
type ExpandableTableRow,
|
||||
type GroupedItem,
|
||||
type GroupedTableRow,
|
||||
type TableRow,
|
||||
createSummaryRow,
|
||||
groupsToExpandableRows,
|
||||
groupsToTableRows,
|
||||
transformToHierarchicalGroups,
|
||||
transformToTableData,
|
||||
} from './report-table-utils';
|
||||
import { SerieName } from './serie-name';
|
||||
@@ -70,7 +76,6 @@ const VirtualRow = function VirtualRow({
|
||||
resizingColumnId,
|
||||
setResizingColumnId,
|
||||
}: VirtualRowProps) {
|
||||
console.log('VirtualRow', row.original.id);
|
||||
const cells = row.getVisibleCells();
|
||||
|
||||
return (
|
||||
@@ -161,9 +166,7 @@ export function ReportTable({
|
||||
setVisibleSeries,
|
||||
}: ReportTableProps) {
|
||||
const [grouped, setGrouped] = useState(true);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [expanded, setExpanded] = useState<ExpandedState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [columnSizing, setColumnSizing] = useState<Record<string, number>>({});
|
||||
@@ -180,57 +183,45 @@ export function ReportTable({
|
||||
short: true,
|
||||
});
|
||||
|
||||
// Transform data to table format
|
||||
// Transform data to hierarchical groups or flat rows
|
||||
const {
|
||||
rows: rawRows,
|
||||
groups: hierarchicalGroups,
|
||||
rows: flatRows,
|
||||
dates,
|
||||
breakdownPropertyNames,
|
||||
} = useMemo(
|
||||
() => transformToTableData(data, breakdowns, grouped),
|
||||
[data, breakdowns, grouped],
|
||||
);
|
||||
} = useMemo(() => {
|
||||
if (grouped) {
|
||||
const result = transformToHierarchicalGroups(data, breakdowns);
|
||||
return {
|
||||
groups: result.groups,
|
||||
rows: null,
|
||||
dates: result.dates,
|
||||
breakdownPropertyNames: result.breakdownPropertyNames,
|
||||
};
|
||||
}
|
||||
const result = transformToTableData(data, breakdowns, false);
|
||||
return {
|
||||
groups: null,
|
||||
rows: result.rows as TableRow[],
|
||||
dates: result.dates,
|
||||
breakdownPropertyNames: result.breakdownPropertyNames,
|
||||
};
|
||||
}, [data, breakdowns, grouped]);
|
||||
|
||||
// Filter rows based on collapsed groups and create summary rows
|
||||
const rows = useMemo(() => {
|
||||
if (!grouped || collapsedGroups.size === 0) {
|
||||
return rawRows;
|
||||
// Convert hierarchical groups to expandable rows (for TanStack Table's expanding feature)
|
||||
const expandableRows = useMemo(() => {
|
||||
if (!grouped || !hierarchicalGroups || hierarchicalGroups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const processedRows: (TableRow | GroupedTableRow)[] = [];
|
||||
const groupedRows = rawRows as GroupedTableRow[];
|
||||
|
||||
// Group rows by their groupKey
|
||||
const rowsByGroup = new Map<string, GroupedTableRow[]>();
|
||||
groupedRows.forEach((row) => {
|
||||
if (row.groupKey) {
|
||||
if (!rowsByGroup.has(row.groupKey)) {
|
||||
rowsByGroup.set(row.groupKey, []);
|
||||
}
|
||||
rowsByGroup.get(row.groupKey)!.push(row);
|
||||
} else {
|
||||
// Rows without groupKey go directly to processed
|
||||
processedRows.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
// Process each group
|
||||
rowsByGroup.forEach((groupRows, groupKey) => {
|
||||
if (collapsedGroups.has(groupKey)) {
|
||||
// Group is collapsed - show summary row
|
||||
const summaryRow = createSummaryRow(
|
||||
groupRows,
|
||||
groupKey,
|
||||
return groupsToExpandableRows(
|
||||
hierarchicalGroups,
|
||||
breakdownPropertyNames.length,
|
||||
);
|
||||
processedRows.push(summaryRow);
|
||||
} else {
|
||||
// Group is expanded - show all rows
|
||||
processedRows.push(...groupRows);
|
||||
}
|
||||
});
|
||||
}, [grouped, hierarchicalGroups, breakdownPropertyNames.length]);
|
||||
|
||||
return processedRows;
|
||||
}, [rawRows, collapsedGroups, grouped, breakdownPropertyNames.length]);
|
||||
// Use expandable rows if available, otherwise use flat rows
|
||||
const rows = expandableRows ?? flatRows ?? [];
|
||||
|
||||
// Filter rows based on global search and apply sorting
|
||||
const filteredRows = useMemo(() => {
|
||||
@@ -275,43 +266,42 @@ export function ReportTable({
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting - if grouped, sort within each group
|
||||
if (grouped && sorting.length > 0 && result.length > 0) {
|
||||
const groupedRows = result as GroupedTableRow[];
|
||||
|
||||
// Group rows by their groupKey
|
||||
const rowsByGroup = new Map<string, GroupedTableRow[]>();
|
||||
const ungroupedRows: GroupedTableRow[] = [];
|
||||
|
||||
groupedRows.forEach((row) => {
|
||||
if (row.groupKey) {
|
||||
if (!rowsByGroup.has(row.groupKey)) {
|
||||
rowsByGroup.set(row.groupKey, []);
|
||||
}
|
||||
rowsByGroup.get(row.groupKey)!.push(row);
|
||||
} else {
|
||||
ungroupedRows.push(row);
|
||||
}
|
||||
});
|
||||
// Apply sorting - if grouped, always sort groups by highest count, then sort within each group
|
||||
if (grouped && result.length > 0) {
|
||||
const groupedRows = result as ExpandableTableRow[] | GroupedTableRow[];
|
||||
|
||||
// Sort function based on current sort state
|
||||
const sortFn = (a: GroupedTableRow, b: GroupedTableRow) => {
|
||||
const sortFn = (
|
||||
a: ExpandableTableRow | GroupedTableRow | TableRow,
|
||||
b: ExpandableTableRow | GroupedTableRow | TableRow,
|
||||
) => {
|
||||
// If no sorting is selected, return 0 (no change)
|
||||
if (sorting.length === 0) return 0;
|
||||
|
||||
for (const sort of sorting) {
|
||||
const { id, desc } = sort;
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
if (id === 'serie-name') {
|
||||
aValue = a.serieName;
|
||||
bValue = b.serieName;
|
||||
aValue = a.serieName ?? '';
|
||||
bValue = b.serieName ?? '';
|
||||
} else if (id.startsWith('breakdown-')) {
|
||||
const index = Number.parseInt(id.replace('breakdown-', ''), 10);
|
||||
if ('breakdownDisplay' in a && a.breakdownDisplay) {
|
||||
aValue = a.breakdownDisplay[index] ?? '';
|
||||
} else {
|
||||
aValue = a.breakdownValues[index] ?? '';
|
||||
}
|
||||
if ('breakdownDisplay' in b && b.breakdownDisplay) {
|
||||
bValue = b.breakdownDisplay[index] ?? '';
|
||||
} else {
|
||||
bValue = b.breakdownValues[index] ?? '';
|
||||
}
|
||||
} else if (id.startsWith('metric-')) {
|
||||
const metric = id.replace('metric-', '') as keyof TableRow;
|
||||
aValue = a[metric];
|
||||
bValue = b[metric];
|
||||
aValue = a[metric] ?? 0;
|
||||
bValue = b[metric] ?? 0;
|
||||
} else if (id.startsWith('date-')) {
|
||||
const date = id.replace('date-', '');
|
||||
aValue = a.dateValues[date] ?? 0;
|
||||
@@ -320,31 +310,113 @@ export function ReportTable({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue == null && bValue == null) continue;
|
||||
if (aValue == null) return 1;
|
||||
if (bValue == null) return -1;
|
||||
|
||||
// Compare values
|
||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||
const comparison = aValue.localeCompare(bValue);
|
||||
if (comparison !== 0) return desc ? -comparison : comparison;
|
||||
} else {
|
||||
if (aValue < bValue) return desc ? 1 : -1;
|
||||
if (aValue > bValue) return desc ? -1 : 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Sort groups themselves by their first row's sort value
|
||||
const groupsArray = Array.from(rowsByGroup.entries());
|
||||
groupsArray.sort((a, b) => {
|
||||
const aFirst = a[1][0];
|
||||
const bFirst = b[1][0];
|
||||
if (!aFirst || !bFirst) return 0;
|
||||
return sortFn(aFirst, bFirst);
|
||||
// For expandable rows, we need to sort recursively
|
||||
function sortExpandableRows(
|
||||
rows: ExpandableTableRow[],
|
||||
isTopLevel = true,
|
||||
): ExpandableTableRow[] {
|
||||
// Sort rows: groups by count first (only at top level), then apply user sort
|
||||
const sorted = [...rows].sort((a, b) => {
|
||||
// At top level, sort groups by count first
|
||||
if (isTopLevel) {
|
||||
const aIsGroupHeader = 'isGroupHeader' in a && a.isGroupHeader;
|
||||
const bIsGroupHeader = 'isGroupHeader' in b && b.isGroupHeader;
|
||||
|
||||
if (aIsGroupHeader && bIsGroupHeader) {
|
||||
const aLevel = 'groupLevel' in a ? (a.groupLevel ?? -1) : -1;
|
||||
const bLevel = 'groupLevel' in b ? (b.groupLevel ?? -1) : -1;
|
||||
|
||||
// Same level groups: sort by count first (always, regardless of user sort)
|
||||
if (aLevel === bLevel) {
|
||||
const aCount = a.count ?? 0;
|
||||
const bCount = b.count ?? 0;
|
||||
if (aCount !== bCount) {
|
||||
return bCount - aCount; // Highest first
|
||||
}
|
||||
// If counts are equal, fall through to user sort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply user's sort criteria (for all rows, including within groups)
|
||||
return sortFn(a, b);
|
||||
});
|
||||
|
||||
// Rebuild result with sorted groups
|
||||
const finalResult: GroupedTableRow[] = [];
|
||||
groupsArray.forEach(([, groupRows]) => {
|
||||
const sorted = [...groupRows].sort(sortFn);
|
||||
finalResult.push(...sorted);
|
||||
// Sort subRows recursively (within each group) - these are NOT top level
|
||||
return sorted.map((row) => {
|
||||
if ('subRows' in row && row.subRows) {
|
||||
return {
|
||||
...row,
|
||||
subRows: sortExpandableRows(row.subRows, false),
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
finalResult.push(...ungroupedRows.sort(sortFn));
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
return sortExpandableRows(groupedRows as ExpandableTableRow[]);
|
||||
}
|
||||
|
||||
// For flat mode, apply sorting
|
||||
if (!grouped && result.length > 0 && sorting.length > 0) {
|
||||
return [...result].sort((a, b) => {
|
||||
for (const sort of sorting) {
|
||||
const { id, desc } = sort;
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
if (id === 'serie-name') {
|
||||
aValue = a.serieName ?? '';
|
||||
bValue = b.serieName ?? '';
|
||||
} else if (id.startsWith('breakdown-')) {
|
||||
const index = Number.parseInt(id.replace('breakdown-', ''), 10);
|
||||
aValue = a.breakdownValues[index] ?? '';
|
||||
bValue = b.breakdownValues[index] ?? '';
|
||||
} else if (id.startsWith('metric-')) {
|
||||
const metric = id.replace('metric-', '') as keyof TableRow;
|
||||
aValue = a[metric] ?? 0;
|
||||
bValue = b[metric] ?? 0;
|
||||
} else if (id.startsWith('date-')) {
|
||||
const date = id.replace('date-', '');
|
||||
aValue = a.dateValues[date] ?? 0;
|
||||
bValue = b.dateValues[date] ?? 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue == null && bValue == null) continue;
|
||||
if (aValue == null) return 1;
|
||||
if (bValue == null) return -1;
|
||||
|
||||
// Compare values
|
||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||
const comparison = aValue.localeCompare(bValue);
|
||||
if (comparison !== 0) return desc ? -comparison : comparison;
|
||||
} else {
|
||||
if (aValue < bValue) return desc ? 1 : -1;
|
||||
if (aValue > bValue) return desc ? -1 : 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -374,13 +446,36 @@ export function ReportTable({
|
||||
};
|
||||
});
|
||||
|
||||
// Check if we only have one series (excluding summary rows)
|
||||
const nonSummaryRows = rows.filter((row) => !row.isSummaryRow);
|
||||
const isSingleSeries = nonSummaryRows.length === 1;
|
||||
// Helper function to flatten expandable rows and get only individual rows
|
||||
function getIndividualRows(
|
||||
rows: (ExpandableTableRow | TableRow)[],
|
||||
): TableRow[] {
|
||||
const individualRows: TableRow[] = [];
|
||||
for (const row of rows) {
|
||||
const isGroupHeader =
|
||||
'isGroupHeader' in row && row.isGroupHeader === true;
|
||||
const isSummary = 'isSummaryRow' in row && row.isSummaryRow === true;
|
||||
|
||||
if (!isGroupHeader && !isSummary) {
|
||||
// It's an individual row - add it
|
||||
individualRows.push(row as TableRow);
|
||||
}
|
||||
|
||||
// Always recursively process subRows if they exist (regardless of whether this is a group header)
|
||||
if ('subRows' in row && row.subRows && Array.isArray(row.subRows)) {
|
||||
individualRows.push(...getIndividualRows(row.subRows));
|
||||
}
|
||||
}
|
||||
return individualRows;
|
||||
}
|
||||
|
||||
// Get only individual rows from all rows to ensure consistent ranges
|
||||
const individualRows = getIndividualRows(rows);
|
||||
const isSingleSeries = individualRows.length === 1;
|
||||
|
||||
if (isSingleSeries) {
|
||||
// For single series, calculate ranges from date values
|
||||
const singleRow = nonSummaryRows[0]!;
|
||||
const singleRow = individualRows[0]!;
|
||||
const allDateValues = dates.map(
|
||||
(date) => singleRow.dateValues[date] ?? 0,
|
||||
);
|
||||
@@ -403,12 +498,16 @@ export function ReportTable({
|
||||
metricRanges.min = { min: dateMin, max: dateMax };
|
||||
metricRanges.max = { min: dateMin, max: dateMax };
|
||||
} else {
|
||||
// Multiple series: calculate ranges across rows
|
||||
rows.forEach((row) => {
|
||||
// Multiple series: calculate ranges across individual rows only
|
||||
if (individualRows.length === 0) {
|
||||
// No individual rows found - this shouldn't happen, but handle gracefully
|
||||
console.warn('No individual rows found for range calculation');
|
||||
} else {
|
||||
individualRows.forEach((row) => {
|
||||
// Calculate metric ranges
|
||||
Object.keys(metricRanges).forEach((key) => {
|
||||
const value = row[key as keyof typeof row] as number;
|
||||
if (typeof value === 'number') {
|
||||
if (typeof value === 'number' && !Number.isNaN(value)) {
|
||||
metricRanges[key]!.min = Math.min(metricRanges[key]!.min, value);
|
||||
metricRanges[key]!.max = Math.max(metricRanges[key]!.max, value);
|
||||
}
|
||||
@@ -423,11 +522,14 @@ export function ReportTable({
|
||||
max: Number.NEGATIVE_INFINITY,
|
||||
};
|
||||
}
|
||||
if (typeof value === 'number' && !Number.isNaN(value)) {
|
||||
dateRanges[date]!.min = Math.min(dateRanges[date]!.min, value);
|
||||
dateRanges[date]!.max = Math.max(dateRanges[date]!.max, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { metricRanges, dateRanges };
|
||||
}, [rows, dates]);
|
||||
@@ -439,10 +541,18 @@ export function ReportTable({
|
||||
max: number,
|
||||
className?: string,
|
||||
): { opacity: number; className: string } => {
|
||||
if (value === 0 || max === min) {
|
||||
if (value === 0) {
|
||||
return { opacity: 0, className: '' };
|
||||
}
|
||||
|
||||
// If min equals max (e.g. single row or all values same), show moderate opacity
|
||||
if (max === min) {
|
||||
return {
|
||||
opacity: 0.5,
|
||||
className: cn('bg-highlight dark:bg-emerald-700', className),
|
||||
};
|
||||
}
|
||||
|
||||
const percentage = (value - min) / (max - min);
|
||||
const opacity = Math.max(0.05, Math.min(1, percentage));
|
||||
|
||||
@@ -481,17 +591,12 @@ export function ReportTable({
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle group collapse
|
||||
// Toggle group collapse (now handled by TanStack Table's expanding feature)
|
||||
// This is kept for backward compatibility with header click handlers
|
||||
const toggleGroupCollapse = (groupKey: string) => {
|
||||
setCollapsedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(groupKey)) {
|
||||
next.delete(groupKey);
|
||||
} else {
|
||||
next.add(groupKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
// This will be handled by TanStack Table's row expansion
|
||||
// We can find the row by groupKey and toggle it
|
||||
// For now, this is a no-op as TanStack Table handles it
|
||||
};
|
||||
|
||||
// Define columns
|
||||
@@ -525,9 +630,12 @@ export function ReportTable({
|
||||
original.groupKey &&
|
||||
!original.isSummaryRow
|
||||
) {
|
||||
// Find all rows in this group and get the first one
|
||||
const groupRows = (rawRows as GroupedTableRow[]).filter(
|
||||
(r) => r.groupKey === original.groupKey && !r.isSummaryRow,
|
||||
// Find all rows in this group from the current rows array
|
||||
const groupRows = rows.filter(
|
||||
(r): r is GroupedTableRow =>
|
||||
'groupKey' in r &&
|
||||
r.groupKey === original.groupKey &&
|
||||
!r.isSummaryRow,
|
||||
);
|
||||
|
||||
if (groupRows.length > 0) {
|
||||
@@ -545,8 +653,44 @@ export function ReportTable({
|
||||
}
|
||||
}
|
||||
|
||||
const originalRow = row.original as ExpandableTableRow | TableRow;
|
||||
const isGroupHeader =
|
||||
'isGroupHeader' in originalRow && originalRow.isGroupHeader === true;
|
||||
const canExpand = grouped ? (row.getCanExpand?.() ?? false) : false;
|
||||
const isExpanded = grouped ? (row.getIsExpanded?.() ?? false) : false;
|
||||
const isSerieGroupHeader =
|
||||
isGroupHeader &&
|
||||
'groupLevel' in originalRow &&
|
||||
originalRow.groupLevel === -1;
|
||||
const hasSubRows =
|
||||
'subRows' in originalRow && (originalRow.subRows?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 h-12">
|
||||
{grouped && isSerieGroupHeader && hasSubRows && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
// Toggle expanded state manually
|
||||
setExpanded((prev) => {
|
||||
const newExpanded: ExpandedState =
|
||||
typeof prev === 'object' ? { ...prev } : {};
|
||||
const rowId = row.id;
|
||||
newExpanded[rowId] = !newExpanded[rowId];
|
||||
return newExpanded;
|
||||
});
|
||||
}}
|
||||
className="cursor-pointer hover:opacity-70"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={isVisible}
|
||||
onCheckedChange={() => toggleSerieVisibility(serieId)}
|
||||
@@ -561,7 +705,7 @@ export function ReportTable({
|
||||
className={cn(
|
||||
'truncate',
|
||||
isMuted && 'text-muted-foreground/50',
|
||||
isFirstRowInGroup && 'font-semibold',
|
||||
(isFirstRowInGroup || isGroupHeader) && 'font-semibold',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -592,39 +736,80 @@ export function ReportTable({
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
// Find all unique group keys for this breakdown level
|
||||
const groupKeys = new Set<string>();
|
||||
(rawRows as GroupedTableRow[]).forEach((row) => {
|
||||
if (row.groupKey) {
|
||||
groupKeys.add(row.groupKey);
|
||||
// Find all rows at this breakdown level that can be expanded
|
||||
const rowsAtLevel: string[] = [];
|
||||
if (grouped && expandableRows) {
|
||||
function collectRowIdsAtLevel(
|
||||
rows: ExpandableTableRow[],
|
||||
targetLevel: number,
|
||||
currentLevel = 0,
|
||||
): void {
|
||||
for (const row of rows) {
|
||||
if (
|
||||
row.isGroupHeader &&
|
||||
row.groupLevel === targetLevel &&
|
||||
(row.subRows?.length ?? 0) > 0
|
||||
) {
|
||||
rowsAtLevel.push(row.id);
|
||||
}
|
||||
// Recurse into subRows if we haven't reached target level yet
|
||||
if (currentLevel < targetLevel && row.subRows) {
|
||||
collectRowIdsAtLevel(
|
||||
row.subRows,
|
||||
targetLevel,
|
||||
currentLevel + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
collectRowIdsAtLevel(expandableRows, index);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if all groups at this level are collapsed
|
||||
const allCollapsed = Array.from(groupKeys).every((key) =>
|
||||
collapsedGroups.has(key),
|
||||
// Check if all groups at this level are expanded
|
||||
const allExpanded =
|
||||
rowsAtLevel.length > 0 &&
|
||||
rowsAtLevel.every(
|
||||
(id) => typeof expanded === 'object' && expanded[id] === true,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer hover:opacity-70"
|
||||
onClick={() => {
|
||||
if (!grouped) return;
|
||||
// Toggle all groups at this breakdown level
|
||||
groupKeys.forEach((key) => toggleGroupCollapse(key));
|
||||
setExpanded((prev) => {
|
||||
const newExpanded: ExpandedState =
|
||||
typeof prev === 'object' ? { ...prev } : {};
|
||||
const shouldExpand = !allExpanded;
|
||||
rowsAtLevel.forEach((id) => {
|
||||
newExpanded[id] = shouldExpand;
|
||||
});
|
||||
return newExpanded;
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
groupKeys.forEach((key) => toggleGroupCollapse(key));
|
||||
if (!grouped) return;
|
||||
setExpanded((prev) => {
|
||||
const newExpanded: ExpandedState =
|
||||
typeof prev === 'object' ? { ...prev } : {};
|
||||
const shouldExpand = !allExpanded;
|
||||
rowsAtLevel.forEach((id) => {
|
||||
newExpanded[id] = shouldExpand;
|
||||
});
|
||||
return newExpanded;
|
||||
});
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{allCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
{allExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
<span>{propertyName}</span>
|
||||
</div>
|
||||
@@ -636,19 +821,52 @@ export function ReportTable({
|
||||
breakdownIndex: index,
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const original = row.original as ExpandableTableRow | TableRow;
|
||||
const isGroupHeader =
|
||||
'isGroupHeader' in original && original.isGroupHeader === true;
|
||||
const canExpand = row.getCanExpand?.() ?? false;
|
||||
const isExpanded = row.getIsExpanded?.() ?? false;
|
||||
|
||||
let value: string | null;
|
||||
let isMuted = false;
|
||||
let isFirstRowInGroup = false;
|
||||
|
||||
if ('breakdownDisplay' in original && grouped) {
|
||||
if (
|
||||
'breakdownDisplay' in original &&
|
||||
grouped &&
|
||||
original.breakdownDisplay !== undefined
|
||||
) {
|
||||
value = original.breakdownDisplay[index] ?? null;
|
||||
|
||||
// For group headers, show the group value at the appropriate level
|
||||
if (isGroupHeader && 'groupLevel' in original) {
|
||||
const groupLevel = original.groupLevel ?? 0;
|
||||
if (index === groupLevel) {
|
||||
value = original.groupValue ?? null;
|
||||
} else if (index < groupLevel) {
|
||||
// Show parent group values from the path
|
||||
// This would need to be calculated from the hierarchy
|
||||
value = null; // Will be handled by breakdownDisplay
|
||||
} else {
|
||||
// For breakdowns deeper than the group level, don't show anything
|
||||
// (e.g., if group is at COUNTRY level, don't show CITY)
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is the first row in the group and if this breakdown should be bold
|
||||
if (value && original.groupKey && !original.isSummaryRow) {
|
||||
// Find all rows in this group and get the first one
|
||||
const groupRows = (rawRows as GroupedTableRow[]).filter(
|
||||
(r) => r.groupKey === original.groupKey && !r.isSummaryRow,
|
||||
if (
|
||||
value &&
|
||||
'groupKey' in original &&
|
||||
original.groupKey &&
|
||||
!original.isSummaryRow
|
||||
) {
|
||||
// Find all rows in this group from the current rows array
|
||||
const groupRows = rows.filter(
|
||||
(r): r is GroupedTableRow | ExpandableTableRow =>
|
||||
'groupKey' in r &&
|
||||
r.groupKey === original.groupKey &&
|
||||
!('isSummaryRow' in r && r.isSummaryRow),
|
||||
);
|
||||
|
||||
if (groupRows.length > 0) {
|
||||
@@ -659,7 +877,10 @@ export function ReportTable({
|
||||
isFirstRowInGroup = true;
|
||||
} else {
|
||||
// Only mute if this is not the first row and the value matches
|
||||
const firstRowValue = firstRowInGroup.breakdownValues[index];
|
||||
const firstRowValue =
|
||||
'breakdownValues' in firstRowInGroup
|
||||
? firstRowInGroup.breakdownValues[index]
|
||||
: null;
|
||||
if (firstRowValue === value) {
|
||||
isMuted = true;
|
||||
}
|
||||
@@ -667,25 +888,52 @@ export function ReportTable({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = original.breakdownValues[index] ?? null;
|
||||
value =
|
||||
'breakdownValues' in original
|
||||
? (original.breakdownValues[index] ?? null)
|
||||
: null;
|
||||
}
|
||||
|
||||
const isSummary = original.isSummaryRow ?? false;
|
||||
const isSummary =
|
||||
'isSummaryRow' in original && original.isSummaryRow === true;
|
||||
// Make bold if it's the first row in group and this is one of the first breakdown columns
|
||||
// (all breakdowns except the last one)
|
||||
const shouldBeBold =
|
||||
isFirstRowInGroup && index < breakdownPropertyNames.length - 1;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 h-12">
|
||||
{canExpand &&
|
||||
index ===
|
||||
('groupLevel' in original ? (original.groupLevel ?? 0) : 0) &&
|
||||
index < breakdownPropertyNames.length - 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const handler = row.getToggleExpandedHandler();
|
||||
if (handler) handler();
|
||||
}}
|
||||
className="cursor-pointer hover:opacity-70"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'truncate block leading-[48px] px-4',
|
||||
'truncate block leading-[48px]',
|
||||
(!value || isMuted) && 'text-muted-foreground/50',
|
||||
(isSummary || shouldBeBold) && 'font-semibold',
|
||||
(isSummary || shouldBeBold || isGroupHeader) &&
|
||||
'font-semibold',
|
||||
)}
|
||||
>
|
||||
{value || ''}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -709,9 +957,39 @@ export function ReportTable({
|
||||
size: 100,
|
||||
cell: ({ row }) => {
|
||||
const value = row.original[metric.key];
|
||||
const isSummary = row.original.isSummaryRow ?? false;
|
||||
const original = row.original as ExpandableTableRow | TableRow;
|
||||
const hasIsSummaryRow = 'isSummaryRow' in original;
|
||||
const hasIsGroupHeader = 'isGroupHeader' in original;
|
||||
const isSummary = hasIsSummaryRow && original.isSummaryRow === true;
|
||||
const isGroupHeader =
|
||||
hasIsGroupHeader && original.isGroupHeader === true;
|
||||
const isIndividualRow = !isSummary && !isGroupHeader;
|
||||
const range = metricRanges[metric.key];
|
||||
const { opacity, className } = range
|
||||
|
||||
// Debug: Check first few rows
|
||||
if (metric.key === 'count' && row.index < 5) {
|
||||
console.log(`[FIX CHECK] Row ${row.index}:`, {
|
||||
isSummaryRowValue:
|
||||
'isSummaryRow' in original ? original.isSummaryRow : 'NOT SET',
|
||||
isGroupHeaderValue:
|
||||
'isGroupHeader' in original
|
||||
? original.isGroupHeader
|
||||
: 'NOT SET',
|
||||
isSummary,
|
||||
isGroupHeader,
|
||||
isIndividualRow,
|
||||
});
|
||||
}
|
||||
|
||||
// Only apply colors to individual rows, not summary or group header rows
|
||||
// Also check that range is valid (not still at initial values)
|
||||
const hasValidRange =
|
||||
range &&
|
||||
range.min !== Number.POSITIVE_INFINITY &&
|
||||
range.max !== Number.NEGATIVE_INFINITY;
|
||||
|
||||
const { opacity, className } =
|
||||
isIndividualRow && hasValidRange
|
||||
? getCellBackground(
|
||||
value,
|
||||
range.min,
|
||||
@@ -729,7 +1007,7 @@ export function ReportTable({
|
||||
<div
|
||||
className={cn(
|
||||
'relative text-right font-mono text-sm px-4 h-full flex items-center justify-end',
|
||||
isSummary && 'font-semibold',
|
||||
(isSummary || isGroupHeader) && 'font-semibold',
|
||||
opacity > 0.7 &&
|
||||
'text-white [text-shadow:_0_0_3px_rgb(0_0_0_/_20%)]',
|
||||
)}
|
||||
@@ -753,8 +1031,19 @@ export function ReportTable({
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.dateValues[date] ?? 0;
|
||||
const isSummary = row.original.isSummaryRow ?? false;
|
||||
const isGroupHeader =
|
||||
'isGroupHeader' in row.original &&
|
||||
row.original.isGroupHeader === true;
|
||||
const isIndividualRow = !isSummary && !isGroupHeader;
|
||||
const range = dateRanges[date];
|
||||
const { opacity, className } = range
|
||||
// Only apply colors to individual rows, not summary or group header rows
|
||||
// Also check that range is valid (not still at initial values)
|
||||
const hasValidRange =
|
||||
range &&
|
||||
range.min !== Number.POSITIVE_INFINITY &&
|
||||
range.max !== Number.NEGATIVE_INFINITY;
|
||||
const { opacity, className } =
|
||||
isIndividualRow && hasValidRange
|
||||
? getCellBackground(value, range.min, range.max)
|
||||
: { opacity: 0, className: '' };
|
||||
|
||||
@@ -767,7 +1056,7 @@ export function ReportTable({
|
||||
<div
|
||||
className={cn(
|
||||
'relative text-right font-mono text-sm px-4 h-full flex items-center justify-end',
|
||||
isSummary && 'font-semibold',
|
||||
(isSummary || isGroupHeader) && 'font-semibold',
|
||||
opacity > 0.7 &&
|
||||
'text-white [text-shadow:_0_0_3px_rgb(0_0_0_/_20%)]',
|
||||
)}
|
||||
@@ -788,11 +1077,12 @@ export function ReportTable({
|
||||
number,
|
||||
grouped,
|
||||
visibleSeriesIds,
|
||||
collapsedGroups,
|
||||
rawRows,
|
||||
expandableRows,
|
||||
rows,
|
||||
metricRanges,
|
||||
dateRanges,
|
||||
columnSizing,
|
||||
expanded,
|
||||
]);
|
||||
|
||||
// Create a hash of column IDs to track when columns change
|
||||
@@ -800,26 +1090,63 @@ export function ReportTable({
|
||||
return columns.map((col) => col.id).join(',');
|
||||
}, [columns]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredRows,
|
||||
// Memoize table options to ensure table updates when filteredRows changes
|
||||
const tableOptions = useMemo(
|
||||
() => ({
|
||||
data: filteredRows, // This is already sorted in filteredRows
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: grouped ? getCoreRowModel() : getSortedRowModel(), // Disable TanStack sorting when grouped
|
||||
getExpandedRowModel: grouped ? getExpandedRowModel() : undefined,
|
||||
getSubRows: grouped
|
||||
? (row: ExpandableTableRow | TableRow) =>
|
||||
'subRows' in row ? row.subRows : undefined
|
||||
: undefined,
|
||||
// Sorting is handled manually in filteredRows, so we don't use getSortedRowModel
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
filterFns: {
|
||||
isWithinRange: () => true,
|
||||
},
|
||||
enableColumnResizing: true,
|
||||
columnResizeMode: 'onChange',
|
||||
columnResizeMode: 'onChange' as const,
|
||||
getRowCanExpand: grouped
|
||||
? (row: any) => {
|
||||
const r = row.original as ExpandableTableRow;
|
||||
if (!('isGroupHeader' in r) || !r.isGroupHeader) return false;
|
||||
// Don't allow expansion for the last breakdown level
|
||||
const groupLevel = r.groupLevel ?? -1;
|
||||
const isLastBreakdown =
|
||||
groupLevel === breakdownPropertyNames.length - 1;
|
||||
const hasSubRows = (r.subRows?.length ?? 0) > 0;
|
||||
return !isLastBreakdown && hasSubRows;
|
||||
}
|
||||
: undefined,
|
||||
state: {
|
||||
sorting,
|
||||
sorting, // Keep sorting state for UI indicators
|
||||
columnSizing,
|
||||
expanded: grouped ? expanded : undefined,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
onExpandedChange: grouped ? setExpanded : undefined,
|
||||
globalFilterFn: () => true, // We handle filtering manually
|
||||
manualSorting: grouped, // Manual sorting when grouped
|
||||
});
|
||||
manualSorting: true, // We handle sorting manually for both modes
|
||||
manualFiltering: true, // We handle filtering manually
|
||||
}),
|
||||
[
|
||||
filteredRows,
|
||||
columns,
|
||||
grouped,
|
||||
breakdownPropertyNames.length,
|
||||
sorting,
|
||||
columnSizing,
|
||||
expanded,
|
||||
setSorting,
|
||||
setColumnSizing,
|
||||
setExpanded,
|
||||
],
|
||||
);
|
||||
|
||||
const table = useReactTable(tableOptions);
|
||||
|
||||
// Virtualization setup
|
||||
useEffect(() => {
|
||||
@@ -860,8 +1187,18 @@ export function ReportTable({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Get the row model to use (expanded when grouped, regular otherwise)
|
||||
// filteredRows is already sorted, so getExpandedRowModel/getRowModel should preserve that order
|
||||
// We need to recalculate when filteredRows changes to ensure sorting is applied
|
||||
const rowModelToUse = useMemo(() => {
|
||||
if (grouped) {
|
||||
return table.getExpandedRowModel();
|
||||
}
|
||||
return table.getRowModel();
|
||||
}, [table, grouped, expanded, filteredRows.length, sorting]);
|
||||
|
||||
const virtualizer = useWindowVirtualizer({
|
||||
count: filteredRows.length,
|
||||
count: rowModelToUse.rows.length,
|
||||
estimateSize: () => ROW_HEIGHT,
|
||||
overscan: 10,
|
||||
scrollMargin,
|
||||
@@ -1094,7 +1431,7 @@ export function ReportTable({
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const tableRow = table.getRowModel().rows[virtualRow.index];
|
||||
const tableRow = rowModelToUse.rows[virtualRow.index];
|
||||
if (!tableRow) return null;
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user