fix(tracker): BillModal save/close race and pending badge logic
- Use controlled Dialog state (setDialogOpen) instead of immediate onClose() to let Radix cleanup properly before unmount - Amber 'Pending' badge now only shows for bank-linked bills — unlinked bills skip the pending-cleared check and show 'Paid' directly - TrackerPage onSave no longer nullifies edit state before BillModal can animate closed (batch 0.37.4)
This commit is contained in:
parent
fab4945d50
commit
ca514e5f26
|
|
@ -190,6 +190,10 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
bill?.autopay_verified_at ? new Date(bill.autopay_verified_at) : null
|
bill?.autopay_verified_at ? new Date(bill.autopay_verified_at) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Controls the outer Dialog's open state so it closes via its own animation
|
||||||
|
// rather than being abruptly unmounted, which can leave Radix cleanup in a broken state.
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(true);
|
||||||
|
|
||||||
// Deactivate dialog state
|
// Deactivate dialog state
|
||||||
const [deactivateOpen, setDeactivateOpen] = useState(false);
|
const [deactivateOpen, setDeactivateOpen] = useState(false);
|
||||||
const [deactivateReason, setDeactivateReason] = useState('');
|
const [deactivateReason, setDeactivateReason] = useState('');
|
||||||
|
|
@ -594,7 +598,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
toast.success('Template saved');
|
toast.success('Template saved');
|
||||||
}
|
}
|
||||||
onSave(savedBill);
|
onSave(savedBill);
|
||||||
onClose();
|
setDialogOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
}
|
}
|
||||||
|
|
@ -608,7 +612,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
await api.updateBill(bill.id, payload);
|
await api.updateBill(bill.id, payload);
|
||||||
toast.success(bill.active ? 'Bill deactivated' : 'Bill reactivated');
|
toast.success(bill.active ? 'Bill deactivated' : 'Bill reactivated');
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose();
|
setDialogOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
}
|
}
|
||||||
|
|
@ -617,7 +621,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
const inp = 'bg-background/50 border-border/60 h-9 text-sm w-full';
|
const inp = 'bg-background/50 border-border/60 h-9 text-sm w-full';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={v => { if (!v) onClose(); }}>
|
<Dialog open={dialogOpen} onOpenChange={v => { if (!v) onClose(); }}>
|
||||||
<DialogContent className="max-h-[92svh] overflow-y-auto border-border/60 bg-card/95 backdrop-blur-xl sm:max-w-2xl">
|
<DialogContent className="max-h-[92svh] overflow-y-auto border-border/60 bg-card/95 backdrop-blur-xl sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base font-semibold tracking-tight">
|
<DialogTitle className="text-base font-semibold tracking-tight">
|
||||||
|
|
@ -1422,7 +1426,7 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button type="button" variant="ghost" disabled={isPending} onClick={onClose} className="text-xs">
|
<Button type="button" variant="ghost" disabled={isPending} onClick={() => setDialogOpen(false)} className="text-xs">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form="bill-modal-form" disabled={isPending} className="gap-1.5 text-xs">
|
<Button type="submit" form="bill-modal-form" disabled={isPending} className="gap-1.5 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -961,7 +961,7 @@ export default function TrackerPage() {
|
||||||
initialBill={editBillData.initialBill}
|
initialBill={editBillData.initialBill}
|
||||||
categories={editBillData.categories}
|
categories={editBillData.categories}
|
||||||
onClose={() => setEditBillData(null)}
|
onClose={() => setEditBillData(null)}
|
||||||
onSave={() => { setEditBillData(null); refetch(); }}
|
onSave={() => refetch()}
|
||||||
onDuplicate={bill => setEditBillData({
|
onDuplicate={bill => setEditBillData({
|
||||||
bill: null,
|
bill: null,
|
||||||
initialBill: makeBillDraft(bill, { copy: true, categories: editBillData.categories }),
|
initialBill: makeBillDraft(bill, { copy: true, categories: editBillData.categories }),
|
||||||
|
|
|
||||||
|
|
@ -575,7 +575,10 @@ function getTracker(userId, query = {}, now = new Date()) {
|
||||||
// Only flag manually-entered payments as pending-cleared — bank-synced
|
// Only flag manually-entered payments as pending-cleared — bank-synced
|
||||||
// or bank-matched payments are already settled so they don't need the badge.
|
// or bank-matched payments are already settled so they don't need the badge.
|
||||||
const isManualPayment = r.payment_source !== 'provider_sync' && r.payment_source !== 'transaction_match';
|
const isManualPayment = r.payment_source !== 'provider_sync' && r.payment_source !== 'transaction_match';
|
||||||
if (r.status === 'paid' && r.last_paid_date && isManualPayment) {
|
// Amber "Pending" only makes sense for bills linked to the bank — for unlinked bills
|
||||||
|
// there's no bank clearing to wait for, so they should just show "Paid".
|
||||||
|
const isBankLinked = !!(r.has_merchant_rule || r.has_linked_transactions);
|
||||||
|
if (r.status === 'paid' && r.last_paid_date && isManualPayment && isBankLinked) {
|
||||||
const cutoff = new Date();
|
const cutoff = new Date();
|
||||||
cutoff.setDate(cutoff.getDate() - bankTracking.pending_days);
|
cutoff.setDate(cutoff.getDate() - bankTracking.pending_days);
|
||||||
const paidAt = new Date(r.last_paid_date);
|
const paidAt = new Date(r.last_paid_date);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue