diff --git a/src/app/(app)/quotes/[id]/page.tsx b/src/app/(app)/quotes/[id]/page.tsx index 4959d2c..6faee6b 100644 --- a/src/app/(app)/quotes/[id]/page.tsx +++ b/src/app/(app)/quotes/[id]/page.tsx @@ -3065,15 +3065,23 @@ function ApproveAndSendDialog({ /* eslint-enable react-hooks/set-state-in-effect */ // Initial form state — set when the modal opens, persisted when it - // submits. We use a single open-counter so the effect runs ONCE per - // open and not on every quote re-render. + // submits. We seed ONCE per open cycle (not on every claimerName / + // repEmail update) so async lookups that arrive after the modal + // opens don't clobber the operator's in-progress edits. const [sendEmail, setSendEmail] = useState(true); const [cc, setCc] = useState(""); const [bcc, setBcc] = useState(""); const [subject, setSubject] = useState(""); const [message, setMessage] = useState(""); const [attachAuthLetters, setAttachAuthLetters] = useState(true); - const [openNonce, setOpenNonce] = useState(0); + // Tracks which (open, quote.id) cycle the form has been seeded for. + // Reset to null whenever `open` is false so the next open seeds + // fresh. We use a ref (not state) because mutating it shouldn't + // re-trigger this effect — and critically, we don't include it in + // the deps array (an earlier version used `openNonce` as a state + // counter, but incrementing a state value inside an effect that's + // dep'd on it produced an infinite loop). + const seededFor = useRef(null); // Track which attachments the operator wants to send. We pre-check // all of them (PDF + every outbound file); unchecking removes from // the outbound set. @@ -3081,13 +3089,18 @@ function ApproveAndSendDialog({ /* eslint-disable react-hooks/set-state-in-effect, react-hooks/exhaustive-deps */ useEffect(() => { - if (!open) return; - setOpenNonce((n) => n + 1); + if (!open) { + // Reset the seed marker so the next open seeds fresh defaults. + seededFor.current = null; + return; + } + // Seed exactly once per (open, quote.id) cycle. Subsequent prop + // updates (claimerName arriving async, repEmail refreshing, etc.) + // intentionally do NOT re-seed — the operator's edits win. + if (seededFor.current === quote.id) return; + seededFor.current = quote.id; // Seed defaults from the quote (persisted overrides win, then - // fall back to customer/rep-derived values). We intentionally - // don't depend on every `quote.*` field — we want to seed once - // when the modal opens, not re-clobber the operator's edits - // when something else updates the quote row. + // fall back to customer/rep-derived values). const customer = quote.expand?.customer; const buyerEmail = (customer?.buyer_email || "").trim(); setCc(quote.email_cc || buyerEmail); @@ -3107,7 +3120,7 @@ function ApproveAndSendDialog({ ); setAttachAuthLetters(quote.attach_auth_letters !== false); setSendEmail(true); - }, [open, openNonce, quote.id, claimerName, repEmail]); + }, [open, quote.id]); /* eslint-enable react-hooks/set-state-in-effect, react-hooks/exhaustive-deps */ // Pre-select all outbound attachments when they change. We only