From 9ad8cc7e8a080e64612ff49e29e600800aa3485e Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 20:42:24 -0500 Subject: [PATCH] refactor(ts): convert SearchFilterPanel, RecentlyDeletedBillsDialog, BillRulesManager to TSX (B15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SearchFilterPanel (presentational props), RecentlyDeletedBillsDialog (Bill[], days_left cast, formatUSD on Dollars), BillRulesManager (MerchantRule/RuleGroup, reduce> with `?? (acc[key]=…)` to satisfy noUncheckedIndexedAccess). typecheck 0, build green. Co-Authored-By: Claude Opus 4.8 --- ...lRulesManager.jsx => BillRulesManager.tsx} | 38 +++++++++++++------ ...log.jsx => RecentlyDeletedBillsDialog.tsx} | 18 ++++++--- ...hFilterPanel.jsx => SearchFilterPanel.tsx} | 17 ++++++++- 3 files changed, 55 insertions(+), 18 deletions(-) rename client/components/{BillRulesManager.jsx => BillRulesManager.tsx} (81%) rename client/components/{RecentlyDeletedBillsDialog.jsx => RecentlyDeletedBillsDialog.tsx} (84%) rename client/components/{SearchFilterPanel.jsx => SearchFilterPanel.tsx} (84%) diff --git a/client/components/BillRulesManager.jsx b/client/components/BillRulesManager.tsx similarity index 81% rename from client/components/BillRulesManager.jsx rename to client/components/BillRulesManager.tsx index 1cc99dd..62e325a 100644 --- a/client/components/BillRulesManager.jsx +++ b/client/components/BillRulesManager.tsx @@ -1,22 +1,36 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; import { ChevronDown, Trash2, ToggleLeft, ToggleRight, Settings2 } from 'lucide-react'; import { api } from '@/api'; +import { errMessage } from '@/lib/utils'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; + +interface MerchantRule { + id: number; + bill_id: number; + bill_name?: string; + merchant: string; + auto_attribute_late?: number; +} + +interface RuleGroup { + bill_name?: string; + bill_id: number; + rules: MerchantRule[]; +} export default function BillRulesManager() { const [open, setOpen] = useState(false); - const [rules, setRules] = useState([]); + const [rules, setRules] = useState([]); const [loading, setLoading] = useState(false); const load = useCallback(async () => { setLoading(true); try { - const d = await api.allBillMerchantRules(); + const d = await api.allBillMerchantRules() as { rules?: MerchantRule[] }; setRules(d.rules || []); } catch (err) { - toast.error(err.message || 'Failed to load bill matching rules'); + toast.error(errMessage(err, 'Failed to load bill matching rules')); } finally { setLoading(false); } @@ -24,32 +38,32 @@ export default function BillRulesManager() { useEffect(() => { if (open) load(); }, [open, load]); - const handleDelete = async (billId, ruleId, merchant) => { + const handleDelete = async (billId: number, ruleId: number, merchant: string) => { try { await api.deleteMerchantRule(billId, ruleId); setRules(prev => prev.filter(r => r.id !== ruleId)); toast.success(`Rule "${merchant}" removed`); } catch (err) { - toast.error(err.message || 'Failed to delete rule'); + toast.error(errMessage(err, 'Failed to delete rule')); } }; - const handleToggleAutoLate = async (billId, ruleId, current) => { + const handleToggleAutoLate = async (billId: number, ruleId: number, current: boolean) => { try { await api.toggleRuleAutoAttribute(billId, ruleId, !current); setRules(prev => prev.map(r => r.id === ruleId ? { ...r, auto_attribute_late: current ? 0 : 1 } : r )); } catch (err) { - toast.error(err.message || 'Failed to update rule'); + toast.error(errMessage(err, 'Failed to update rule')); } }; // Group rules by bill - const byBill = rules.reduce((acc, r) => { + const byBill = rules.reduce>((acc, r) => { const key = r.bill_id; - if (!acc[key]) acc[key] = { bill_name: r.bill_name, bill_id: r.bill_id, rules: [] }; - acc[key].rules.push(r); + const g = acc[key] ?? (acc[key] = { bill_name: r.bill_name, bill_id: r.bill_id, rules: [] }); + g.rules.push(r); return acc; }, {}); const groups = Object.values(byBill); diff --git a/client/components/RecentlyDeletedBillsDialog.jsx b/client/components/RecentlyDeletedBillsDialog.tsx similarity index 84% rename from client/components/RecentlyDeletedBillsDialog.jsx rename to client/components/RecentlyDeletedBillsDialog.tsx index 991342d..b31436e 100644 --- a/client/components/RecentlyDeletedBillsDialog.jsx +++ b/client/components/RecentlyDeletedBillsDialog.tsx @@ -5,14 +5,22 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { formatUSD } from '@/lib/money'; +import type { Bill } from '@/types'; -function daysLeftLabel(days) { +function daysLeftLabel(days: number | null | undefined): string | null { if (days == null) return null; if (days <= 0) return 'purges today'; if (days === 1) return '1 day left'; return `${days} days left`; } +interface RecentlyDeletedBillsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + bills?: Bill[]; + onRestore: (bill: Bill) => void | Promise; +} + /** * Lists bills that were soft-deleted within the 30-day recovery window and lets * the user restore them — a durable path beyond the transient "Undo" toast. @@ -20,10 +28,10 @@ function daysLeftLabel(days) { * Presentational: the parent owns the list (`bills`) and the async `onRestore`, * so restoring refreshes the page's active bills too. */ -export default function RecentlyDeletedBillsDialog({ open, onOpenChange, bills = [], onRestore }) { - const [busyId, setBusyId] = useState(null); +export default function RecentlyDeletedBillsDialog({ open, onOpenChange, bills = [], onRestore }: RecentlyDeletedBillsDialogProps) { + const [busyId, setBusyId] = useState(null); - async function handleRestore(bill) { + async function handleRestore(bill: Bill) { setBusyId(bill.id); try { await onRestore(bill); @@ -51,7 +59,7 @@ export default function RecentlyDeletedBillsDialog({ open, onOpenChange, bills = ) : (
    {bills.map(bill => { - const left = daysLeftLabel(bill.days_left); + const left = daysLeftLabel(bill.days_left as number | null | undefined); const busy = busyId === bill.id; return (
  • diff --git a/client/components/SearchFilterPanel.jsx b/client/components/SearchFilterPanel.tsx similarity index 84% rename from client/components/SearchFilterPanel.jsx rename to client/components/SearchFilterPanel.tsx index f405ed4..52e4128 100644 --- a/client/components/SearchFilterPanel.jsx +++ b/client/components/SearchFilterPanel.tsx @@ -1,7 +1,22 @@ +import type { ReactNode } from 'react'; import { ChevronDown, ChevronUp, Search, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; +interface SearchFilterPanelProps { + title?: string; + collapsed?: boolean; + onCollapsedChange?: (collapsed: boolean) => void; + hasFilters?: boolean; + resultLabel?: string; + sortLabel?: string; + onClear?: () => void; + children?: ReactNode; + className?: string; + variant?: 'default' | 'embedded'; + headerActions?: ReactNode; +} + export default function SearchFilterPanel({ title = 'Search & filters', collapsed, @@ -14,7 +29,7 @@ export default function SearchFilterPanel({ className, variant = 'default', headerActions, -}) { +}: SearchFilterPanelProps) { const embedded = variant === 'embedded'; const ToggleIcon = collapsed ? ChevronUp : ChevronDown;