diff --git a/client/components/bill-modal/AutopayTrustIndicator.jsx b/client/components/bill-modal/AutopayTrustIndicator.tsx similarity index 88% rename from client/components/bill-modal/AutopayTrustIndicator.jsx rename to client/components/bill-modal/AutopayTrustIndicator.tsx index 9fea6c6..3c5efce 100644 --- a/client/components/bill-modal/AutopayTrustIndicator.jsx +++ b/client/components/bill-modal/AutopayTrustIndicator.tsx @@ -1,9 +1,18 @@ import { cn } from '@/lib/utils'; +import type { AutopayStats } from '@/types'; + +interface AutopayTrustIndicatorProps { + isNew?: boolean; + autopay?: boolean; + stats?: AutopayStats | null; + verifiedAt?: Date | null; + onVerify?: () => void; +} // Edit-mode autopay trust panel: success rate over the last 12 months, a // "Mark verified" action, and staleness / failure warnings. Presentational — // the parent owns the verify handler and the optimistic verified-at date. -export default function AutopayTrustIndicator({ isNew, autopay, stats, verifiedAt, onVerify }) { +export default function AutopayTrustIndicator({ isNew, autopay, stats, verifiedAt, onVerify }: AutopayTrustIndicatorProps) { if (isNew || !autopay) return null; const total = stats?.total ?? 0; diff --git a/client/components/bill-modal/DebtDetailsSection.jsx b/client/components/bill-modal/DebtDetailsSection.tsx similarity index 86% rename from client/components/bill-modal/DebtDetailsSection.jsx rename to client/components/bill-modal/DebtDetailsSection.tsx index ccf89ac..d757ed0 100644 --- a/client/components/bill-modal/DebtDetailsSection.jsx +++ b/client/components/bill-modal/DebtDetailsSection.tsx @@ -2,6 +2,28 @@ import { ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; +import type { Dispatch, SetStateAction } from 'react'; + +type FormErrors = Record; +type Validator = (v: string) => string | null; + +interface DebtDetailsSectionProps { + inp?: string; + errors: FormErrors; + setErrors: Dispatch>; + showDebtSection: boolean; + setShowDebtSection: Dispatch>; + isSnowballCategory?: boolean; + showOnSnowball?: boolean; + interestRate: string; setInterestRate: (v: string) => void; + currentBalance: string; setCurrentBalance: (v: string) => void; + minimumPayment: string; setMinimumPayment: (v: string) => void; + validateInterestRate: Validator; + validateCurrentBalance: Validator; + validateMinimumPayment: Validator; + handleBlur: (field: string, value: string, validator: Validator) => void; + onSnowballVisibilityChange: (checked: boolean) => void; +} // Collapsible Debt / Snowball fields (interest rate, current balance, minimum // payment, snowball visibility). State lives in the parent BillModal (the save @@ -17,7 +39,7 @@ export default function DebtDetailsSection({ validateInterestRate, validateCurrentBalance, validateMinimumPayment, handleBlur, onSnowballVisibilityChange, -}) { +}: DebtDetailsSectionProps) { return (
diff --git a/client/components/bill-modal/LinkedTransactionsSection.jsx b/client/components/bill-modal/LinkedTransactionsSection.tsx similarity index 91% rename from client/components/bill-modal/LinkedTransactionsSection.jsx rename to client/components/bill-modal/LinkedTransactionsSection.tsx index 7db71bc..bf0b9c5 100644 --- a/client/components/bill-modal/LinkedTransactionsSection.jsx +++ b/client/components/bill-modal/LinkedTransactionsSection.tsx @@ -3,6 +3,21 @@ import { cn, fmtDate } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import BillMerchantRules from '@/components/BillMerchantRules'; import { transactionTitle, transactionDate, fmtTransactionAmount } from '@/components/bill-modal/transactionDisplay'; +import type { BankTransaction } from '@/types'; + +interface LinkedTransactionsSectionProps { + isNew?: boolean; + billId?: number | string; + billName?: string; + localHasRules?: boolean; + syncingPayments?: boolean; + onSync?: () => void; + onRulesChanged?: () => void; + linkedTransactions: BankTransaction[]; + linkedTransactionsLoading?: boolean; + transactionBusyId?: number | string | null; + onUnmatch: (transaction: BankTransaction) => void; +} // Bank-matching rules (with a Sync-now action) + the list of transactions // confirmed as matched to this bill (each with an Unmatch action). Presentational @@ -19,7 +34,7 @@ export default function LinkedTransactionsSection({ linkedTransactionsLoading, transactionBusyId, onUnmatch, -}) { +}: LinkedTransactionsSectionProps) { return ( <> {/* Bank Matching Rules */} @@ -107,7 +122,7 @@ export default function LinkedTransactionsSection({ 'font-mono text-sm font-semibold tabular-nums', Number(transaction.amount) < 0 ? 'text-destructive' : 'text-emerald-600', )}> - {fmtTransactionAmount(transaction.amount, transaction.currency)} + {fmtTransactionAmount(transaction.amount, transaction.currency ?? undefined)}

@@ -155,7 +183,7 @@ export default function UnmatchDialogs({ size="sm" variant="outline" className="h-6 px-2 text-xs" - onClick={() => setBulkUnmatch(p => ({ ...p, checkedIds: new Set() }))} + onClick={() => setBulkUnmatch(p => p ? { ...p, checkedIds: new Set() } : p)} > None @@ -179,8 +207,9 @@ export default function UnmatchDialogs({ checked={bulkUnmatch.checkedIds.has(tx.id)} onCheckedChange={checked => { setBulkUnmatch(p => { + if (!p) return p; const next = new Set(p.checkedIds); - checked ? next.add(tx.id) : next.delete(tx.id); + if (checked) next.add(tx.id); else next.delete(tx.id); return { ...p, checkedIds: next }; }); }} @@ -196,7 +225,7 @@ export default function UnmatchDialogs({ 'shrink-0 font-mono text-sm font-semibold tabular-nums', Number(tx.amount) < 0 ? 'text-destructive' : 'text-emerald-600', )}> - {fmtTransactionAmount(tx.amount, tx.currency)} + {fmtTransactionAmount(tx.amount, tx.currency ?? undefined)}

))} @@ -213,7 +242,7 @@ export default function UnmatchDialogs({ className="mt-0.5" checked={bulkUnmatch.removeRuleId === rule.id} onCheckedChange={checked => - setBulkUnmatch(p => ({ ...p, removeRuleId: checked ? rule.id : null })) + setBulkUnmatch(p => p ? { ...p, removeRuleId: checked ? rule.id : null } : p) } />
diff --git a/client/components/bill-modal/transactionDisplay.js b/client/components/bill-modal/transactionDisplay.ts similarity index 58% rename from client/components/bill-modal/transactionDisplay.js rename to client/components/bill-modal/transactionDisplay.ts index 9c24b92..7ddace9 100644 --- a/client/components/bill-modal/transactionDisplay.js +++ b/client/components/bill-modal/transactionDisplay.ts @@ -4,25 +4,33 @@ import { formatCentsUSD } from '@/lib/money'; // the linked-transaction list, the unmatch dialogs, and the bulk-unmatch payee // grouping. -export function fmtTransactionAmount(amount, currency = 'USD') { +interface TxLike { + payee?: string | null; + description?: string | null; + memo?: string | null; + posted_date?: string | null; + transacted_at?: string | number | null; +} + +export function fmtTransactionAmount(amount: Parameters[0], currency = 'USD') { return formatCentsUSD(amount, { signed: true, currency }); } -export function transactionDate(tx) { +export function transactionDate(tx: TxLike | null | undefined): string | null { return tx?.posted_date || String(tx?.transacted_at || '').slice(0, 10) || null; } -export function transactionTitle(tx) { +export function transactionTitle(tx: TxLike | null | undefined): string { return tx?.payee || tx?.description || tx?.memo || 'Transaction'; } -export function normalizePayee(s) { +export function normalizePayee(s: string | null | undefined): string { return (s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); } // Two payees are "similar" when one normalized name is a prefix of the other // (min 3 chars) — the grouping used to find related matches to unmatch together. -export function isSimilarPayee(a, b) { +export function isSimilarPayee(a: string | null | undefined, b: string | null | undefined): boolean { const na = normalizePayee(a); const nb = normalizePayee(b); const minLen = Math.min(na.length, nb.length); diff --git a/client/types.ts b/client/types.ts index cd6eb6e..3a3916d 100644 --- a/client/types.ts +++ b/client/types.ts @@ -8,7 +8,25 @@ // Shapes are typed incrementally (endpoint-by-endpoint). Complex nested payloads // that no typed consumer reads yet (trend series, autopay suggestion/stats, // sparkline) are left `unknown` and refined when a consumer needs them. -import type { Dollars } from '@/lib/money'; +import type { Cents, Dollars } from '@/lib/money'; + +// A bank transaction as serialized by the server. Unlike bill/payment amounts, +// raw bank-transaction amounts stay in CENTS on the wire — hence branded `Cents` +// (format them with formatCentsUSD, never formatUSD). +export interface BankTransaction { + id: number; + amount: Cents; + currency?: string | null; + payee?: string | null; + description?: string | null; + memo?: string | null; + posted_date?: string | null; + transacted_at?: string | number | null; + account_name?: string | null; + source_label?: string | null; + source_type_label?: string | null; + [key: string]: unknown; +} // A bill's month status. Mirrors the server's statusService. export type TrackerStatus =