refactor(bill-modal): extract AutopayTrustIndicator (BM2, 2/n)
The edit-mode autopay trust panel (12-mo success rate, mark-verified, staleness/ failure warnings) moves to its own presentational component. Behavior-preserving — BillModal 1637 -> 1603 lines; build + client tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1018c55bb3
commit
9d670985fe
|
|
@ -20,6 +20,7 @@ import { api } from '@/api';
|
||||||
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
|
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
|
||||||
import BillMerchantRules from '@/components/BillMerchantRules';
|
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 {
|
import {
|
||||||
BILLING_SCHEDULE_OPTIONS,
|
BILLING_SCHEDULE_OPTIONS,
|
||||||
billingCycleForSchedule,
|
billingCycleForSchedule,
|
||||||
|
|
@ -913,48 +914,13 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Autopay trust indicator — edit mode only */}
|
{/* Autopay trust indicator — edit mode only */}
|
||||||
{!isNew && autopay && (() => {
|
<AutopayTrustIndicator
|
||||||
const stats = bill?.autopay_stats;
|
isNew={isNew}
|
||||||
const total = stats?.total ?? 0;
|
autopay={autopay}
|
||||||
const failures = stats?.failures ?? 0;
|
stats={bill?.autopay_stats}
|
||||||
const daysSince = localVerifiedAt
|
verifiedAt={localVerifiedAt}
|
||||||
? Math.floor((Date.now() - localVerifiedAt.getTime()) / 86400000)
|
onVerify={handleVerifyAutopay}
|
||||||
: null;
|
/>
|
||||||
const needsVerify = daysSince === null || daysSince > 90;
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-border/50 bg-muted/20 px-3 py-2.5 space-y-1.5">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className={cn('text-xs font-medium', failures > 0 ? 'text-amber-500' : total > 0 ? 'text-emerald-500' : 'text-muted-foreground/60')}>
|
|
||||||
{total > 0
|
|
||||||
? `${failures > 0 ? '⚠' : '✓'} ${total - failures}/${total} successful (12 mo)`
|
|
||||||
: 'No payment history yet'}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleVerifyAutopay}
|
|
||||||
className="text-[11px] text-sky-500 hover:text-sky-400 underline underline-offset-2 transition-colors"
|
|
||||||
>
|
|
||||||
Mark verified
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{needsVerify && (
|
|
||||||
<p className="text-[11px] text-amber-500/80">
|
|
||||||
{daysSince === null
|
|
||||||
? "Autopay never confirmed — verify it's still active."
|
|
||||||
: `Last verified ${daysSince}d ago — confirm autopay is still on.`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{!needsVerify && (
|
|
||||||
<p className="text-[11px] text-muted-foreground/60">Verified {daysSince}d ago</p>
|
|
||||||
)}
|
|
||||||
{failures > 0 && stats?.last_failure_date && (
|
|
||||||
<p className="text-[11px] text-amber-500/80">
|
|
||||||
Last failure: {stats.last_failure_date}{stats?.last_failure_notes ? ` — ${stats.last_failure_notes}` : ''}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
<div className="col-span-2 space-y-1.5">
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
// Edit-mode autopay trust panel: success rate over the last 12 months, a
|
||||||
|
// "Mark verified" action, and staleness / failure warnings. Presentational —
|
||||||
|
// the parent owns the verify handler and the optimistic verified-at date.
|
||||||
|
export default function AutopayTrustIndicator({ isNew, autopay, stats, verifiedAt, onVerify }) {
|
||||||
|
if (isNew || !autopay) return null;
|
||||||
|
|
||||||
|
const total = stats?.total ?? 0;
|
||||||
|
const failures = stats?.failures ?? 0;
|
||||||
|
const daysSince = verifiedAt
|
||||||
|
? Math.floor((Date.now() - verifiedAt.getTime()) / 86400000)
|
||||||
|
: null;
|
||||||
|
const needsVerify = daysSince === null || daysSince > 90;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-border/50 bg-muted/20 px-3 py-2.5 space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className={cn('text-xs font-medium', failures > 0 ? 'text-amber-500' : total > 0 ? 'text-emerald-500' : 'text-muted-foreground/60')}>
|
||||||
|
{total > 0
|
||||||
|
? `${failures > 0 ? '⚠' : '✓'} ${total - failures}/${total} successful (12 mo)`
|
||||||
|
: 'No payment history yet'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onVerify}
|
||||||
|
className="text-[11px] text-sky-500 hover:text-sky-400 underline underline-offset-2 transition-colors"
|
||||||
|
>
|
||||||
|
Mark verified
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{needsVerify && (
|
||||||
|
<p className="text-[11px] text-amber-500/80">
|
||||||
|
{daysSince === null
|
||||||
|
? "Autopay never confirmed — verify it's still active."
|
||||||
|
: `Last verified ${daysSince}d ago — confirm autopay is still on.`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!needsVerify && (
|
||||||
|
<p className="text-[11px] text-muted-foreground/60">Verified {daysSince}d ago</p>
|
||||||
|
)}
|
||||||
|
{failures > 0 && stats?.last_failure_date && (
|
||||||
|
<p className="text-[11px] text-amber-500/80">
|
||||||
|
Last failure: {stats.last_failure_date}{stats?.last_failure_notes ? ` — ${stats.last_failure_notes}` : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue