fix(tracker): search filter and bucket improvements

This commit is contained in:
null 2026-06-07 17:33:31 -05:00
parent f7ad1c1ebb
commit f2f9ad83ac
3 changed files with 110 additions and 46 deletions

View File

@ -13,6 +13,7 @@ export default function SearchFilterPanel({
children,
className,
variant = 'default',
headerActions,
}) {
const embedded = variant === 'embedded';
const ToggleIcon = collapsed ? ChevronUp : ChevronDown;
@ -49,6 +50,7 @@ export default function SearchFilterPanel({
Clear
</Button>
)}
{headerActions}
</div>
{!collapsed && (

View File

@ -1,17 +1,12 @@
import { useState } from 'react';
import { LayoutGroup } from 'framer-motion';
import { ArrowDown, ArrowUp, Settings2 } from 'lucide-react';
import { ArrowDown, ArrowUp } from 'lucide-react';
import { cn, fmt } from '@/lib/utils';
import {
Table, TableHeader, TableBody, TableHead, TableRow, TableCell,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import {
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { TRACKER_SORT_ASC, TRACKER_SORT_DEFAULT, moveInArray } from '@/lib/trackerUtils';
import { TRACKER_TABLE_COLUMNS, DEFAULT_TRACKER_TABLE_COLUMNS } from '@/lib/trackerTableColumns';
import { DEFAULT_TRACKER_TABLE_COLUMNS } from '@/lib/trackerTableColumns';
import { TrackerRow as Row } from '@/components/tracker/TrackerRow';
import { MobileTrackerRow } from '@/components/tracker/MobileTrackerRow';
@ -61,8 +56,6 @@ export function TrackerBucket({
onSort,
driftedIds = new Set(),
visibleColumns = DEFAULT_TRACKER_TABLE_COLUMNS,
onColumnToggle,
columnsSaving,
}) {
const [draggingId, setDraggingId] = useState(null);
const [dropTargetId, setDropTargetId] = useState(null);
@ -185,37 +178,6 @@ export function TrackerBucket({
{!reorderEnabled && rows.length > 1 && (
<span className="hidden text-[11px] text-muted-foreground/60 xl:inline">Clear filters to reorder</span>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="sm"
variant="outline"
className="hidden h-8 gap-1.5 px-2 text-[11px] lg:inline-flex"
disabled={columnsSaving}
>
<Settings2 className="h-3.5 w-3.5" />
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Visible columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked disabled>
Bill
</DropdownMenuCheckboxItem>
{TRACKER_TABLE_COLUMNS.map(column => (
<DropdownMenuCheckboxItem
key={column.key}
checked={showColumn(column.key)}
onSelect={event => event.preventDefault()}
onCheckedChange={checked => onColumnToggle?.(column.key, checked)}
>
{column.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, EyeOff } from 'lucide-react';
import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, Eye, EyeOff, Settings2 } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api.js';
import { useTracker, useDriftReport } from '@/hooks/useQueries';
@ -19,6 +19,10 @@ import {
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import {
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import StartingAmountsEditDialog from '@/components/tracker/StartingAmountsEditDialog';
import OverdueCommandCenter from '@/components/tracker/OverdueCommandCenter';
@ -30,7 +34,7 @@ import {
TRACKER_SORT_DEFAULT_DIRS, TRACKER_SORT_LABELS,
normalizeTrackerSortKey, normalizeTrackerSortDir, sortTrackerRows,
} from '@/lib/trackerUtils';
import { parseTrackerTableColumns, trackerTableColumnsToSetting } from '@/lib/trackerTableColumns';
import { TRACKER_TABLE_COLUMNS, parseTrackerTableColumns, trackerTableColumnsToSetting } from '@/lib/trackerTableColumns';
import { FilterChip } from '@/components/tracker/FilterChip';
import { SummaryCard, TrendCard } from '@/components/tracker/SummaryCards';
import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog';
@ -118,6 +122,44 @@ function BankProjectionBanner({ bankTracking, onSnooze, onIgnore, busy }) {
);
}
function TrackerColumnMenu({ visibleColumns, onColumnToggle, saving }) {
const visibleSet = new Set(visibleColumns);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="sm"
variant="outline"
className="h-8 gap-1.5 px-2.5 text-xs"
disabled={saving}
>
<Settings2 className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Columns</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Table columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked disabled>
Bill
</DropdownMenuCheckboxItem>
{TRACKER_TABLE_COLUMNS.map(column => (
<DropdownMenuCheckboxItem
key={column.key}
checked={visibleSet.has(column.key)}
onSelect={event => event.preventDefault()}
onCheckedChange={checked => onColumnToggle?.(column.key, checked)}
>
{column.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
// Main page
function LateAttributionDialog({ attr, remaining, busy, onAccept, onDismiss }) {
if (!attr) return null;
@ -391,6 +433,18 @@ export default function TrackerPage() {
tracker_table_columns: trackerTableColumnsToSetting(nextColumns),
});
}
function hideSearchSortPanel() {
saveTrackerSettings({
tracker_show_search_sort: 'false',
}, 'Search & sort hidden.');
}
function showSearchSortPanel() {
saveTrackerSettings({
tracker_show_search_sort: 'true',
});
}
const toggleFilter = (key) => {
const paramMap = { autopay: 'ap', firstBucket: 'b1', fifteenthBucket: 'b2', unpaid: 'un', overdue: 'ov', debt: 'de' };
updateParams({ [paramMap[key]]: !filters[key] });
@ -438,6 +492,8 @@ export default function TrackerPage() {
|| filters.debt
|| hasSort
);
const searchResultLabel = `${filteredRows.length} of ${rows.length} shown`;
const searchSortLabel = hasSort ? `sorted by ${TRACKER_SORT_LABELS[sortKey]} ${sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'}` : null;
const resetFilters = () => {
updateParams({ q: null, fc: null, cy: null, ap: null, b1: null, b2: null, un: null, ov: null, de: null, sort: null, dir: null });
};
@ -617,9 +673,29 @@ export default function TrackerPage() {
collapsed={searchPanelCollapsed}
onCollapsedChange={setSearchPanelCollapsed}
hasFilters={hasFilters}
resultLabel={`${filteredRows.length} of ${rows.length} shown`}
sortLabel={hasSort ? `sorted by ${TRACKER_SORT_LABELS[sortKey]} ${sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'}` : null}
resultLabel={searchResultLabel}
sortLabel={searchSortLabel}
onClear={resetFilters}
headerActions={(
<div className="flex items-center gap-2">
<TrackerColumnMenu
visibleColumns={visibleTableColumns}
onColumnToggle={handleTableColumnToggle}
saving={savingTrackerSetting}
/>
<Button
type="button"
size="sm"
variant="ghost"
onClick={hideSearchSortPanel}
disabled={savingTrackerSetting}
className="h-8 gap-1.5 px-2.5 text-xs text-muted-foreground hover:text-foreground"
>
<EyeOff className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Hide</span>
</Button>
</div>
)}
>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-[minmax(220px,1fr)_220px_180px_220px_auto] xl:items-center">
<label className="relative">
@ -693,6 +769,30 @@ export default function TrackerPage() {
</div>
</SearchFilterPanel>
)}
{!showSearchSort && (
<button
type="button"
onClick={showSearchSortPanel}
disabled={savingTrackerSetting}
className="group flex w-full items-center justify-between gap-3 rounded-xl border border-dashed border-border/80 bg-card/55 px-4 py-3 text-left shadow-sm transition-colors hover:border-primary/40 hover:bg-card/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="flex min-w-0 items-center gap-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
<Search className="h-4 w-4" />
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-foreground">Show Search & sort</span>
<span className="block truncate text-xs text-muted-foreground">
{[searchResultLabel, hasFilters ? 'filters active' : 'no filters', searchSortLabel].filter(Boolean).join(' · ')}
</span>
</span>
</span>
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors group-hover:bg-accent group-hover:text-foreground">
<Eye className="h-3.5 w-3.5" />
Restore
</span>
</button>
)}
{/* ── Summary cards (backend already excludes skipped from totals) ── */}
{showSummaryCards && loading ? (
@ -865,8 +965,8 @@ export default function TrackerPage() {
)}
{!isError && (first.length > 0 || second.length > 0) && (
<div className="space-y-5">
{first.length > 0 && <Bucket label="1st 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('1st', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} onColumnToggle={handleTableColumnToggle} columnsSaving={savingTrackerSetting} />}
{second.length > 0 && <Bucket label="15th 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('15th', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} onColumnToggle={handleTableColumnToggle} columnsSaving={savingTrackerSetting} />}
{first.length > 0 && <Bucket label="1st 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('1st', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} />}
{second.length > 0 && <Bucket label="15th 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('15th', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} />}
</div>
)}