refactor(bill-modal): extract PaymentFormFields (BM2, 4/n)

The add/edit manual-payment form (+ its PAYMENT_METHODS list) moves to its own
presentational component; the parent keeps the form state + submit handler.
Behavior-preserving — BillModal 1502 -> 1432 lines; build + client tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 19:16:10 -05:00
parent afba78e86b
commit c61e3d84a5
2 changed files with 115 additions and 82 deletions

View File

@ -22,6 +22,7 @@ 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 {
BILLING_SCHEDULE_OPTIONS,
billingCycleForSchedule,
@ -41,15 +42,6 @@ function getOrdinalSuffix(day) {
// Radix Select crashes on empty string value
const CAT_NONE = 'none';
const PAYMENT_METHODS = [
['manual', 'Manual'],
['bank', 'Bank Transfer'],
['card', 'Card'],
['autopay', 'Autopay'],
['check', 'Check'],
['cash', 'Cash'],
];
const DEBT_KEYWORDS = ['credit', 'loan', 'mortgage', 'housing', 'debt'];
const SNOWBALL_KEYWORDS = ['credit', 'loan', 'debt', 'mortgage', 'housing'];
const SUBSCRIPTION_TYPES = [
@ -1091,79 +1083,17 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
</div>
{paymentFormOpen && (
<form onSubmit={handlePaymentSubmit} className="mt-3 rounded-lg border border-border/60 bg-muted/20 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{editingPayment ? 'Edit payment' : 'Add payment'}
</p>
<span className="rounded-md border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
manual
</span>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Amount ($)</Label>
<Input
type="number"
min="0"
step="0.01"
value={paymentAmount}
onChange={e => setPaymentAmount(e.target.value)}
className={cn(inp, 'font-mono')}
required
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Paid Date</Label>
<Input
type="date"
value={paymentDate}
onChange={e => setPaymentDate(e.target.value)}
className={cn(inp, 'font-mono')}
required
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Method</Label>
<Select value={paymentMethod} onValueChange={setPaymentMethod}>
<SelectTrigger className={cn(inp, 'w-full')}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAYMENT_METHODS.map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Notes</Label>
<Input
value={paymentNotes}
onChange={e => setPaymentNotes(e.target.value)}
className={inp}
placeholder="Paid from checking"
/>
</div>
</div>
<div className="mt-3 flex justify-end gap-2">
<Button
type="button"
variant="ghost"
disabled={paymentBusy}
onClick={() => {
resetPaymentForm();
setPaymentFormOpen(false);
}}
className="text-xs"
>
Cancel
</Button>
<Button type="submit" disabled={paymentBusy} className="text-xs">
{paymentBusy ? 'Saving...' : editingPayment ? 'Save Payment' : 'Add Payment'}
</Button>
</div>
</form>
<PaymentFormFields
inp={inp}
editingPayment={editingPayment}
paymentBusy={paymentBusy}
amount={paymentAmount} setAmount={setPaymentAmount}
date={paymentDate} setDate={setPaymentDate}
method={paymentMethod} setMethod={setPaymentMethod}
notes={paymentNotes} setNotes={setPaymentNotes}
onSubmit={handlePaymentSubmit}
onCancel={() => { resetPaymentForm(); setPaymentFormOpen(false); }}
/>
)}
</div>
)}

View File

@ -0,0 +1,103 @@
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
const PAYMENT_METHODS = [
['manual', 'Manual'],
['bank', 'Bank Transfer'],
['card', 'Card'],
['autopay', 'Autopay'],
['check', 'Check'],
['cash', 'Cash'],
];
// Add/edit form for a manual payment on a bill (edit mode). Presentational
// the parent owns the form state, the submit handler, and open/close.
export default function PaymentFormFields({
inp,
editingPayment,
paymentBusy,
amount, setAmount,
date, setDate,
method, setMethod,
notes, setNotes,
onSubmit,
onCancel,
}) {
return (
<form onSubmit={onSubmit} className="mt-3 rounded-lg border border-border/60 bg-muted/20 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{editingPayment ? 'Edit payment' : 'Add payment'}
</p>
<span className="rounded-md border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
manual
</span>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Amount ($)</Label>
<Input
type="number"
min="0"
step="0.01"
value={amount}
onChange={e => setAmount(e.target.value)}
className={cn(inp, 'font-mono')}
required
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Paid Date</Label>
<Input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
className={cn(inp, 'font-mono')}
required
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Method</Label>
<Select value={method} onValueChange={setMethod}>
<SelectTrigger className={cn(inp, 'w-full')}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAYMENT_METHODS.map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Notes</Label>
<Input
value={notes}
onChange={e => setNotes(e.target.value)}
className={inp}
placeholder="Paid from checking"
/>
</div>
</div>
<div className="mt-3 flex justify-end gap-2">
<Button
type="button"
variant="ghost"
disabled={paymentBusy}
onClick={onCancel}
className="text-xs"
>
Cancel
</Button>
<Button type="submit" disabled={paymentBusy} className="text-xs">
{paymentBusy ? 'Saving...' : editingPayment ? 'Save Payment' : 'Add Payment'}
</Button>
</div>
</form>
);
}