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:
parent
afba78e86b
commit
c61e3d84a5
|
|
@ -22,6 +22,7 @@ import BillMerchantRules from '@/components/BillMerchantRules';
|
||||||
import DebtDetailsSection from '@/components/bill-modal/DebtDetailsSection';
|
import DebtDetailsSection from '@/components/bill-modal/DebtDetailsSection';
|
||||||
import AutopayTrustIndicator from '@/components/bill-modal/AutopayTrustIndicator';
|
import AutopayTrustIndicator from '@/components/bill-modal/AutopayTrustIndicator';
|
||||||
import PaymentHistoryList from '@/components/bill-modal/PaymentHistoryList';
|
import PaymentHistoryList from '@/components/bill-modal/PaymentHistoryList';
|
||||||
|
import PaymentFormFields from '@/components/bill-modal/PaymentFormFields';
|
||||||
import {
|
import {
|
||||||
BILLING_SCHEDULE_OPTIONS,
|
BILLING_SCHEDULE_OPTIONS,
|
||||||
billingCycleForSchedule,
|
billingCycleForSchedule,
|
||||||
|
|
@ -41,15 +42,6 @@ function getOrdinalSuffix(day) {
|
||||||
|
|
||||||
// Radix Select crashes on empty string value
|
// Radix Select crashes on empty string value
|
||||||
const CAT_NONE = 'none';
|
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 DEBT_KEYWORDS = ['credit', 'loan', 'mortgage', 'housing', 'debt'];
|
||||||
const SNOWBALL_KEYWORDS = ['credit', 'loan', 'debt', 'mortgage', 'housing'];
|
const SNOWBALL_KEYWORDS = ['credit', 'loan', 'debt', 'mortgage', 'housing'];
|
||||||
const SUBSCRIPTION_TYPES = [
|
const SUBSCRIPTION_TYPES = [
|
||||||
|
|
@ -1091,79 +1083,17 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paymentFormOpen && (
|
{paymentFormOpen && (
|
||||||
<form onSubmit={handlePaymentSubmit} className="mt-3 rounded-lg border border-border/60 bg-muted/20 p-3">
|
<PaymentFormFields
|
||||||
<div className="mb-3 flex items-center justify-between gap-3">
|
inp={inp}
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
editingPayment={editingPayment}
|
||||||
{editingPayment ? 'Edit payment' : 'Add payment'}
|
paymentBusy={paymentBusy}
|
||||||
</p>
|
amount={paymentAmount} setAmount={setPaymentAmount}
|
||||||
<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">
|
date={paymentDate} setDate={setPaymentDate}
|
||||||
manual
|
method={paymentMethod} setMethod={setPaymentMethod}
|
||||||
</span>
|
notes={paymentNotes} setNotes={setPaymentNotes}
|
||||||
</div>
|
onSubmit={handlePaymentSubmit}
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
onCancel={() => { resetPaymentForm(); setPaymentFormOpen(false); }}
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue