diff --git a/apps/start/src/components/report-chart/common/report-table-toolbar.tsx b/apps/start/src/components/report-chart/common/report-table-toolbar.tsx index a7bf83f0..00536876 100644 --- a/apps/start/src/components/report-chart/common/report-table-toolbar.tsx +++ b/apps/start/src/components/report-chart/common/report-table-toolbar.tsx @@ -3,11 +3,11 @@ import { Input } from '@/components/ui/input'; import { List, Rows3, Search, X } from 'lucide-react'; interface ReportTableToolbarProps { - grouped: boolean; - onToggleGrouped: () => void; + grouped?: boolean; + onToggleGrouped?: () => void; search: string; - onSearchChange: (value: string) => void; - onUnselectAll: () => void; + onSearchChange?: (value: string) => void; + onUnselectAll?: () => void; } export function ReportTableToolbar({ @@ -19,27 +19,33 @@ export function ReportTableToolbar({ }: ReportTableToolbarProps) { return (
-
- - onSearchChange(e.target.value)} - className="pl-8" - /> -
+ {onSearchChange && ( +
+ + onSearchChange(e.target.value)} + className="pl-8" + /> +
+ )}
- - + {onToggleGrouped && ( + + )} + {onUnselectAll && ( + + )}
); diff --git a/apps/start/src/components/report-chart/conversion/conversion-table.tsx b/apps/start/src/components/report-chart/conversion/conversion-table.tsx index 02ae54f4..5242dd1e 100644 --- a/apps/start/src/components/report-chart/conversion/conversion-table.tsx +++ b/apps/start/src/components/report-chart/conversion/conversion-table.tsx @@ -6,7 +6,8 @@ import type { RouterOutputs } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { getChartColor } from '@/utils/theme'; import { getPreviousMetric } from '@openpanel/common'; -import { useMemo } from 'react'; +import type { SortingState } from '@tanstack/react-table'; +import { useMemo, useState } from 'react'; import { PreviousDiffIndicatorPure } from '../common/previous-diff-indicator'; import { ReportTableToolbar } from '../common/report-table-toolbar'; import { SerieIcon } from '../common/serie-icon'; @@ -23,6 +24,8 @@ export function ConversionTable({ visibleSeries, setVisibleSeries, }: ConversionTableProps) { + const [sorting, setSorting] = useState([]); + const [globalFilter, setGlobalFilter] = useState(''); const number = useNumber(); const interval = useSelector((state) => state.report.interval); const formatDate = useFormatDateInterval({ @@ -178,6 +181,125 @@ export function ConversionTable({ }); }; + // Filter and sort rows + const filteredAndSortedRows = useMemo(() => { + let result = rows; + + // Apply search filter + if (globalFilter.trim()) { + const searchLower = globalFilter.toLowerCase(); + result = rows.filter((row) => { + // Search in serie name + if ( + row.serieName.some((name) => + name?.toLowerCase().includes(searchLower), + ) + ) { + return true; + } + + // Search in breakdown values + if ( + row.breakdownValues.some((val) => + val?.toLowerCase().includes(searchLower), + ) + ) { + return true; + } + + // Search in metric values + if ( + String(row.avgRate).toLowerCase().includes(searchLower) || + String(row.total).toLowerCase().includes(searchLower) || + String(row.conversions).toLowerCase().includes(searchLower) + ) { + return true; + } + + // Search in date values + if ( + Object.values(row.dateValues).some((val) => + String(val).toLowerCase().includes(searchLower), + ) + ) { + return true; + } + + return false; + }); + } + + // Apply sorting + if (sorting.length > 0) { + result = [...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.join(' > ') ?? ''; + bValue = b.serieName.join(' > ') ?? ''; + } else if (id === 'metric-avgRate') { + aValue = a.avgRate ?? 0; + bValue = b.avgRate ?? 0; + } else if (id === 'metric-total') { + aValue = a.total ?? 0; + bValue = b.total ?? 0; + } else if (id === 'metric-conversions') { + aValue = a.conversions ?? 0; + bValue = b.conversions ?? 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; + }, [rows, globalFilter, sorting]); + + const handleSort = (columnId: string) => { + setSorting((prev) => { + const existingSort = prev.find((s) => s.id === columnId); + if (existingSort) { + if (existingSort.desc) { + // Toggle to ascending if already descending + return [{ id: columnId, desc: false }]; + } + // Remove sort if already ascending + return []; + } + // Start with descending (highest first) + return [{ id: columnId, desc: true }]; + }); + }; + + const getSortIcon = (columnId: string) => { + const sort = sorting.find((s) => s.id === columnId); + if (!sort) return '⇅'; + return sort.desc ? '↓' : '↑'; + }; + if (allSeries.length === 0) { return null; } @@ -185,141 +307,208 @@ export function ConversionTable({ return (
{}} - search="" - onSearchChange={() => {}} + search={globalFilter} + onSearchChange={setGlobalFilter} onUnselectAll={() => setVisibleSeries([])} /> -
-
- - - - - + ); + })} + +
- Serie - +
+ + + + + - + - + - {dates.map((date) => ( - + {dates.map((date) => ( + - ))} - - - - {rows.map((row) => { - const isVisible = visibleSeriesIds.includes(row.serieId); - const serieIndex = getSerieIndex(row.serieId); - const color = getChartColor(serieIndex); - const previousMetric = - row.prevAvgRate !== undefined - ? getPreviousMetric(row.avgRate, row.prevAvgRate) - : null; + + {getSortIcon(`date-${date}`)} + + + + ))} + + + + {filteredAndSortedRows.map((row) => { + const isVisible = visibleSeriesIds.includes(row.serieId); + const serieIndex = getSerieIndex(row.serieId); + const color = getChartColor(serieIndex); + const previousMetric = + row.prevAvgRate !== undefined + ? getPreviousMetric(row.avgRate, row.prevAvgRate) + : null; - return ( - + + - + + + {dates.map((date) => { + const value = row.dateValues[date] ?? 0; + return ( + - - - {dates.map((date) => { - const value = row.dateValues[date] ?? 0; - return ( - - ); - })} - - ); - })} - -
+
Serie
+
handleSort('metric-avgRate')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSort('metric-avgRate'); + } + }} + > +
Avg Rate -
+ + {getSortIcon('metric-avgRate')} + + + handleSort('metric-total')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSort('metric-total'); + } + }} + > +
Total -
+ + {getSortIcon('metric-total')} + + + handleSort('metric-conversions')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSort('metric-conversions'); + } + }} + > +
Conversions -
+ + {getSortIcon('metric-conversions')} + + + handleSort(`date-${date}`)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSort(`date-${date}`); + } + }} + > +
{formatDate(date)} -
+
+ + toggleSerieVisibility(row.serieId) + } + style={{ + borderColor: color, + backgroundColor: isVisible ? color : 'transparent', + }} + className="h-4 w-4 shrink-0" + /> +
+ + +
+
- -
- - toggleSerieVisibility(row.serieId) - } - style={{ - borderColor: color, - backgroundColor: isVisible ? color : 'transparent', - }} - className="h-4 w-4 shrink-0" - /> -
- - -
-
+ + {number.formatWithUnit(row.avgRate / 100, '%')} + + {previousMetric && ( + )} - > -
- - {number.formatWithUnit(row.avgRate / 100, '%')} - - {previousMetric && ( - +
+
+ {number.format(row.total)} + + {number.format(row.conversions)} + - - {number.format(row.total)} - - {number.format(row.conversions)} - - {number.formatWithUnit(value / 100, '%')} -
-
+ > + {number.formatWithUnit(value / 100, '%')} + + ); + })} +
);