fix(tracker): search filter and bucket improvements
This commit is contained in:
parent
f7ad1c1ebb
commit
f2f9ad83ac
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue