From ad68d965b639dd69ca359d6d16db98894a32fd05 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 3 Jul 2026 19:22:58 -0500 Subject: [PATCH] refactor(bill-modal): extract LinkedTransactionsSection (BM2, 6/n) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bank-matching rules (+ Sync action) and the linked-transaction list (+ Unmatch) move to LinkedTransactionsSection. The big inline sync onClick is lifted to a named parent handler (handleSyncBillPayments) sharing a refreshAfterImport helper with handleRulesChanged. Dropped now-unused imports (BillMerchantRules, Link2, Link2Off, RefreshCw, transactionDate, fmtTransactionAmount). Behavior-preserving — BillModal 1193 -> 1105 lines; build + client tests green. Co-Authored-By: Claude Opus 4.8 --- client/components/BillModal.jsx | 199 +++++------------- .../bill-modal/LinkedTransactionsSection.jsx | 131 ++++++++++++ 2 files changed, 186 insertions(+), 144 deletions(-) create mode 100644 client/components/bill-modal/LinkedTransactionsSection.jsx diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx index 9837b1c..a5d66d9 100644 --- a/client/components/BillModal.jsx +++ b/client/components/BillModal.jsx @@ -1,5 +1,5 @@ import { useActionState, useEffect, useState } from 'react'; -import { Copy, Link2, Link2Off, Loader2, RefreshCw } from 'lucide-react'; +import { Copy, Loader2 } from 'lucide-react'; import { validateNonNegativeMoney } from '@/lib/money'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -17,13 +17,13 @@ import { } from '@/components/ui/select'; import { api } from '@/api'; import { cn, fmt, fmtDate, todayStr } from '@/lib/utils'; -import BillMerchantRules from '@/components/BillMerchantRules'; import DebtDetailsSection from '@/components/bill-modal/DebtDetailsSection'; import AutopayTrustIndicator from '@/components/bill-modal/AutopayTrustIndicator'; import PaymentHistoryList from '@/components/bill-modal/PaymentHistoryList'; import PaymentFormFields from '@/components/bill-modal/PaymentFormFields'; import UnmatchDialogs from '@/components/bill-modal/UnmatchDialogs'; -import { transactionTitle, transactionDate, fmtTransactionAmount, isSimilarPayee } from '@/components/bill-modal/transactionDisplay'; +import LinkedTransactionsSection from '@/components/bill-modal/LinkedTransactionsSection'; +import { transactionTitle, isSimilarPayee } from '@/components/bill-modal/transactionDisplay'; import { BILLING_SCHEDULE_OPTIONS, billingCycleForSchedule, @@ -177,6 +177,44 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa loadLinkedTransactions(); }, [bill?.id]); + // Imported payments (via sync or a merchant-rule historical import) must + // refresh the payment list AND the Tracker behind the modal, not just the + // linked transactions — matching the unmatch handlers. + async function refreshAfterImport() { + await Promise.all([loadPayments(), loadLinkedTransactions()]); + onSave?.(); + } + + async function handleSyncBillPayments() { + setSyncingPayments(true); + const promise = api.syncBillSimplefinPayments(sourceBill.id); + toast.promise(promise, { + loading: 'Scanning bank history…', + success: (result) => result.added > 0 + ? `${result.added} payment${result.added !== 1 ? 's' : ''} imported from bank history.` + : 'No new matching transactions found.', + error: (err) => err.message || 'Sync failed.', + }); + try { + const result = await promise; + if (result.added > 0) await refreshAfterImport(); + if (result.late_attributions?.length) { + window.dispatchEvent(new CustomEvent('tracker:late-attributions', { + detail: { attributions: result.late_attributions }, + })); + } + } catch { + // toast.promise already surfaced the error + } finally { + setSyncingPayments(false); + } + } + + async function handleRulesChanged() { + setLocalHasRules(true); + await refreshAfterImport(); + } + const validateName = (val) => { if (!val || val.trim() === '') return 'Name is required'; if (val.trim().length < 2) return 'Name must be at least 2 characters'; @@ -917,147 +955,20 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa onDelete={setDeletePaymentTarget} /> - {/* Bank Matching Rules */} - {!isNew && ( -
-
-
-

Bank matching rules

-

- Transactions whose description contains these patterns are automatically imported as payments. -

-
- {localHasRules && ( - - )} -
-
- { - setLocalHasRules(true); - // A historical import (fired after adding a rule) creates - // payments, so refresh the payment list AND the Tracker too — - // not just the linked transactions. - await Promise.all([loadPayments(), loadLinkedTransactions()]); - onSave?.(); - }} - /> -
-
- )} - -
-
-
-

Linked transactions

-

{linkedTransactions.length} confirmed matches

-
- 0 - ? 'border-primary/20 bg-primary/5 text-primary' - : 'border-border/60 bg-muted/30 text-muted-foreground', - )}> - - {linkedTransactions.length} - -
- - {linkedTransactionsLoading ? ( -
- Loading linked transactions... -
- ) : linkedTransactions.length === 0 ? ( -
- - No transactions linked to this bill yet. -
- ) : ( -
- {linkedTransactions.map(transaction => ( -
-
-
-

{transactionTitle(transaction)}

- - {transaction.source_label || transaction.source_type_label || 'Transaction'} - -
-

- {transactionDate(transaction) ? fmtDate(transactionDate(transaction)) : 'No date'} · {transaction.description || transaction.memo || 'No description'} -

- {transaction.account_name && ( -

{transaction.account_name}

- )} -
-
-

- {fmtTransactionAmount(transaction.amount, transaction.currency)} -

- -
-
- ))} -
- )} -
+ {/* Bank matching rules + linked transactions */} + {paymentFormOpen && ( + {/* Bank Matching Rules */} + {!isNew && ( +
+
+
+

Bank matching rules

+

+ Transactions whose description contains these patterns are automatically imported as payments. +

+
+ {localHasRules && ( + + )} +
+
+ +
+
+ )} + +
+
+
+

Linked transactions

+

{linkedTransactions.length} confirmed matches

+
+ 0 + ? 'border-primary/20 bg-primary/5 text-primary' + : 'border-border/60 bg-muted/30 text-muted-foreground', + )}> + + {linkedTransactions.length} + +
+ + {linkedTransactionsLoading ? ( +
+ Loading linked transactions... +
+ ) : linkedTransactions.length === 0 ? ( +
+ + No transactions linked to this bill yet. +
+ ) : ( +
+ {linkedTransactions.map(transaction => ( +
+
+
+

{transactionTitle(transaction)}

+ + {transaction.source_label || transaction.source_type_label || 'Transaction'} + +
+

+ {transactionDate(transaction) ? fmtDate(transactionDate(transaction)) : 'No date'} · {transaction.description || transaction.memo || 'No description'} +

+ {transaction.account_name && ( +

{transaction.account_name}

+ )} +
+
+

+ {fmtTransactionAmount(transaction.amount, transaction.currency)} +

+ +
+
+ ))} +
+ )} +
+ + ); +}