From afba78e86b8febef958452819d52cfbe39a4f4e7 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 3 Jul 2026 19:11:05 -0500 Subject: [PATCH] refactor(bill-modal): extract PaymentHistoryList (BM2, 3/n) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The payment-history list (rows with source badges + edit/remove for manual payments) and its 4 display helpers (isTransactionLinkedPayment, isHistoryOnlyPayment, paymentSourceLabel/Tone) move to their own presentational component; the parent keeps payment state + add/edit/delete handlers and passes them as props. Behavior-preserving — BillModal 1603 -> 1502 lines; build + client tests green. Co-Authored-By: Claude Opus 4.8 --- client/components/BillModal.jsx | 121 ++-------------- .../bill-modal/PaymentHistoryList.jsx | 131 ++++++++++++++++++ 2 files changed, 141 insertions(+), 111 deletions(-) create mode 100644 client/components/bill-modal/PaymentHistoryList.jsx diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx index 5cba163..54d9c64 100644 --- a/client/components/BillModal.jsx +++ b/client/components/BillModal.jsx @@ -1,5 +1,5 @@ import { useActionState, useEffect, useState } from 'react'; -import { Copy, Layers, Link2, Link2Off, Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react'; +import { Copy, Layers, Link2, Link2Off, Loader2, RefreshCw } from 'lucide-react'; import { formatCentsUSD, validateNonNegativeMoney } from '@/lib/money'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -21,6 +21,7 @@ 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 { BILLING_SCHEDULE_OPTIONS, billingCycleForSchedule, @@ -76,36 +77,6 @@ function transactionTitle(tx) { return tx?.payee || tx?.description || tx?.memo || 'Transaction'; } -function isTransactionLinkedPayment(payment) { - return payment?.payment_source === 'transaction_match' || payment?.transaction_id != null; -} - -function isHistoryOnlyPayment(payment) { - return !!payment?.accounting_excluded; -} - -function paymentSourceLabel(source) { - const labels = { - manual: 'Manual', - file_import: 'File import', - provider_sync: 'Sync', - transaction_match: 'Transaction', - auto_match: 'SimpleFIN', - }; - return labels[source] || source || 'Manual'; -} - -function paymentSourceTone(source) { - const tones = { - manual: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', - file_import: 'border-sky-500/25 bg-sky-500/10 text-sky-600 dark:text-sky-400', - provider_sync: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400', - transaction_match: 'border-primary/25 bg-primary/10 text-primary', - auto_match: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400', - }; - return tones[source] || tones.manual; -} - function isDebtCat(categories, catId) { if (!catId || catId === CAT_NONE) return false; const cat = categories.find(c => String(c.id) === catId); @@ -968,86 +939,14 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa {!isNew && (
-
-
-

Payment history

-

{payments.length} recorded

-
- -
- - {paymentsLoading ? ( -
- Loading payment history... -
- ) : payments.length === 0 ? ( -
-

No payments yet

-

Use the form below to record the first payment.

-
- ) : ( -
- {payments.map(payment => { - const linkedPayment = isTransactionLinkedPayment(payment); - const historyOnly = isHistoryOnlyPayment(payment); - return ( -
-
-
-

{fmt(payment.amount)}

- - {paymentSourceLabel(payment.payment_source)} - - {historyOnly && ( - - History only - - )} -
-

- {fmtDate(payment.paid_date)} · {payment.method || (payment.payment_source === 'transaction_match' ? 'Synced' : 'manual')} -

- {payment.notes && ( -

{payment.notes}

- )} -
-
- {historyOnly ? ( - - Overridden - - ) : linkedPayment ? ( - - - Matched - - ) : ( - <> - - - - )} -
-
- ); - })} -
- )} + {/* Bank Matching Rules */} {!isNew && ( diff --git a/client/components/bill-modal/PaymentHistoryList.jsx b/client/components/bill-modal/PaymentHistoryList.jsx new file mode 100644 index 0000000..52a3a6a --- /dev/null +++ b/client/components/bill-modal/PaymentHistoryList.jsx @@ -0,0 +1,131 @@ +import { Plus, Link2, Pencil, Trash2 } from 'lucide-react'; +import { cn, fmt, fmtDate } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +function isTransactionLinkedPayment(payment) { + return payment?.payment_source === 'transaction_match' || payment?.transaction_id != null; +} + +function isHistoryOnlyPayment(payment) { + return !!payment?.accounting_excluded; +} + +function paymentSourceLabel(source) { + const labels = { + manual: 'Manual', + file_import: 'File import', + provider_sync: 'Sync', + transaction_match: 'Transaction', + auto_match: 'SimpleFIN', + }; + return labels[source] || source || 'Manual'; +} + +function paymentSourceTone(source) { + const tones = { + manual: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', + file_import: 'border-sky-500/25 bg-sky-500/10 text-sky-600 dark:text-sky-400', + provider_sync: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400', + transaction_match: 'border-primary/25 bg-primary/10 text-primary', + auto_match: 'border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-400', + }; + return tones[source] || tones.manual; +} + +// Payment-history list for a bill (edit mode): each recorded payment with its +// source badge, plus edit/remove actions for manual payments (matched and +// history-only rows are read-only). Presentational — the parent owns the +// payment state and the add/edit/delete handlers. +export default function PaymentHistoryList({ + payments, + paymentsLoading, + paymentBusy, + onAdd, + onEdit, + onDelete, +}) { + return ( + <> +
+
+

Payment history

+

{payments.length} recorded

+
+ +
+ + {paymentsLoading ? ( +
+ Loading payment history... +
+ ) : payments.length === 0 ? ( +
+

No payments yet

+

Use the form below to record the first payment.

+
+ ) : ( +
+ {payments.map(payment => { + const linkedPayment = isTransactionLinkedPayment(payment); + const historyOnly = isHistoryOnlyPayment(payment); + return ( +
+
+
+

{fmt(payment.amount)}

+ + {paymentSourceLabel(payment.payment_source)} + + {historyOnly && ( + + History only + + )} +
+

+ {fmtDate(payment.paid_date)} · {payment.method || (payment.payment_source === 'transaction_match' ? 'Synced' : 'manual')} +

+ {payment.notes && ( +

{payment.notes}

+ )} +
+
+ {historyOnly ? ( + + Overridden + + ) : linkedPayment ? ( + + + Matched + + ) : ( + <> + + + + )} +
+
+ ); + })} +
+ )} + + ); +}