217 lines
7.7 KiB
TypeScript
217 lines
7.7 KiB
TypeScript
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<string, string>;
|
|
type Validator = (v: string) => string;
|
|
|
|
interface DebtDetailsSectionProps {
|
|
inp?: string;
|
|
errors: FormErrors;
|
|
setErrors: Dispatch<SetStateAction<FormErrors>>;
|
|
showDebtSection: boolean;
|
|
setShowDebtSection: Dispatch<SetStateAction<boolean>>;
|
|
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
|
|
// action reads these values); this is a presentational extraction.
|
|
export default function DebtDetailsSection({
|
|
inp,
|
|
errors,
|
|
setErrors,
|
|
showDebtSection,
|
|
setShowDebtSection,
|
|
isSnowballCategory,
|
|
showOnSnowball,
|
|
interestRate,
|
|
setInterestRate,
|
|
currentBalance,
|
|
setCurrentBalance,
|
|
minimumPayment,
|
|
setMinimumPayment,
|
|
validateInterestRate,
|
|
validateCurrentBalance,
|
|
validateMinimumPayment,
|
|
handleBlur,
|
|
onSnowballVisibilityChange,
|
|
}: DebtDetailsSectionProps) {
|
|
return (
|
|
<div className="col-span-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-px flex-1 bg-border/40" />
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowDebtSection((s) => !s)}
|
|
className="flex items-center gap-1.5 text-[11px] uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
|
>
|
|
<ChevronDown
|
|
className={cn(
|
|
'h-3 w-3 transition-transform duration-150',
|
|
!showDebtSection && '-rotate-90',
|
|
)}
|
|
/>
|
|
Debt / Snowball Details
|
|
{isSnowballCategory && (
|
|
<span className="text-[9px] text-emerald-400 font-semibold tracking-normal normal-case">
|
|
· auto-detected
|
|
</span>
|
|
)}
|
|
{!showOnSnowball && isSnowballCategory && (
|
|
<span className="text-[9px] text-amber-400 font-semibold tracking-normal normal-case">
|
|
· exempt
|
|
</span>
|
|
)}
|
|
</button>
|
|
<div className="h-px flex-1 bg-border/40" />
|
|
</div>
|
|
|
|
{showDebtSection && (
|
|
<div className="grid sm:grid-cols-2 gap-x-5 gap-y-4 mt-3 p-3 rounded-xl bg-muted/20 border border-border/30">
|
|
{/* Interest Rate */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
Interest rate (APR %)
|
|
</Label>
|
|
<Input
|
|
className={cn(
|
|
inp,
|
|
'font-mono',
|
|
errors.interestRate && 'border-red-500 focus-visible:ring-red-500',
|
|
)}
|
|
type="number"
|
|
min="0"
|
|
max="100"
|
|
step="0.01"
|
|
placeholder="Optional"
|
|
value={interestRate}
|
|
onChange={(e) => {
|
|
setInterestRate(e.target.value);
|
|
setTimeout(
|
|
() =>
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
interestRate: validateInterestRate(e.target.value),
|
|
})),
|
|
300,
|
|
);
|
|
}}
|
|
onBlur={() => handleBlur('interestRate', interestRate, validateInterestRate)}
|
|
/>
|
|
{errors.interestRate && (
|
|
<span className="text-[10px] text-red-500 font-medium">{errors.interestRate}</span>
|
|
)}
|
|
<p className="text-[10px] text-muted-foreground/70">Enter 29.99 for 29.99%.</p>
|
|
</div>
|
|
|
|
{/* Current Balance */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
Current Balance ($)
|
|
</Label>
|
|
<Input
|
|
className={cn(
|
|
inp,
|
|
'font-mono',
|
|
errors.currentBalance && 'border-red-500 focus-visible:ring-red-500',
|
|
)}
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="Optional"
|
|
value={currentBalance}
|
|
onChange={(e) => {
|
|
setCurrentBalance(e.target.value);
|
|
setTimeout(
|
|
() =>
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
currentBalance: validateCurrentBalance(e.target.value),
|
|
})),
|
|
300,
|
|
);
|
|
}}
|
|
onBlur={() => handleBlur('currentBalance', currentBalance, validateCurrentBalance)}
|
|
/>
|
|
{errors.currentBalance && (
|
|
<span className="text-[10px] text-red-500 font-medium">{errors.currentBalance}</span>
|
|
)}
|
|
<p className="text-[10px] text-muted-foreground/70">Outstanding debt balance.</p>
|
|
</div>
|
|
|
|
{/* Minimum Payment */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
Minimum Payment ($)
|
|
</Label>
|
|
<Input
|
|
className={cn(
|
|
inp,
|
|
'font-mono',
|
|
errors.minimumPayment && 'border-red-500 focus-visible:ring-red-500',
|
|
)}
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="Optional"
|
|
value={minimumPayment}
|
|
onChange={(e) => {
|
|
setMinimumPayment(e.target.value);
|
|
setTimeout(
|
|
() =>
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
minimumPayment: validateMinimumPayment(e.target.value),
|
|
})),
|
|
300,
|
|
);
|
|
}}
|
|
onBlur={() => handleBlur('minimumPayment', minimumPayment, validateMinimumPayment)}
|
|
/>
|
|
{errors.minimumPayment && (
|
|
<span className="text-[10px] text-red-500 font-medium">{errors.minimumPayment}</span>
|
|
)}
|
|
<p className="text-[10px] text-muted-foreground/70">
|
|
Required minimum monthly payment.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Include in Snowball */}
|
|
<div className="flex flex-col justify-end pb-1 space-y-1">
|
|
<label className="flex items-center gap-2.5 cursor-pointer group">
|
|
<input
|
|
type="checkbox"
|
|
checked={showOnSnowball}
|
|
onChange={(e) => onSnowballVisibilityChange(e.target.checked)}
|
|
className="h-4 w-4 rounded border-border accent-emerald-500"
|
|
/>
|
|
<span className="text-sm text-muted-foreground group-hover:text-foreground transition-colors">
|
|
Show on Debt Snowball
|
|
</span>
|
|
</label>
|
|
<p className="text-[10px] text-muted-foreground/70 pl-6">
|
|
Uncheck to exempt an auto-detected snowball bill, or check to include this bill
|
|
manually.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|