diff --git a/src/app/(app)/quotes/[id]/process/page.tsx b/src/app/(app)/quotes/[id]/process/page.tsx index 803fb76..b473296 100644 --- a/src/app/(app)/quotes/[id]/process/page.tsx +++ b/src/app/(app)/quotes/[id]/process/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback, useMemo, Fragment } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef, Fragment } from "react"; import { use } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -146,7 +146,8 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str * in the same flow (the PDF route bypasses pricing gates when this * flag is set — see Task 7). */ - const [isNoQuote, setIsNoQuote] = useState(!!quote?.is_no_quote); + const [isNoQuote, setIsNoQuote] = useState(false); + const [noQuoteSyncedFor, setNoQuoteSyncedFor] = useState(null); const [noQuoteSaving, setNoQuoteSaving] = useState(false); /** @@ -532,6 +533,22 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str useEffect(() => { fetchData(); }, [fetchData]); + /** + * Sync the `isNoQuote` toggle once per loaded quote id. The local + * state is otherwise sealed at mount time, when `quote` is still + * null (useState only reads its initial value on the first render), + * so a previously-persisted `is_no_quote: true` quote would render + * the toggle as off. Tracking the last synced id protects + * optimistic toggles from being clobbered by unrelated `fetchData()` + * refreshes once the initial sync is in place. + */ + useEffect(() => { + if (quote && quote.id !== noQuoteSyncedFor) { + setIsNoQuote(!!quote.is_no_quote); + setNoQuoteSyncedFor(quote.id); + } + }, [quote, noQuoteSyncedFor]); + /** * Items to render in the table. Auto-sort: incomplete first, done * last. Original row number preserved via `originalIdx` so the @@ -761,9 +778,12 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str * When flipped on (true), scrolls the `quote_notes` editor into view * so the operator can type the reason in the same flow. */ + const isNoQuoteRef = useRef(false); + isNoQuoteRef.current = isNoQuote; + const onToggleNoQuote = useCallback(async (next: boolean) => { if (!quote) return; - const previous = isNoQuote; + const previous = isNoQuoteRef.current; setIsNoQuote(next); // optimistic setNoQuoteSaving(true); try { @@ -774,7 +794,8 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str }); if (!res.ok) { setIsNoQuote(previous); // rollback - toast.error("Couldn't update — try again."); + const data = (await res.json().catch(() => null)) as { error?: string } | null; + toast.error(data?.error || "Couldn't update — try again."); } else if (next) { // Scroll the quote_notes editor into view so the operator can // type the reason immediately. @@ -788,7 +809,7 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str } finally { setNoQuoteSaving(false); } - }, [isNoQuote, quote]); + }, [quote]); /** * Swap the customer on this quote. Called by the `` @@ -937,12 +958,12 @@ export default function QuoteProcessPage({ params }: { params: Promise<{ id: str className={cn( "mt-3 flex items-center justify-between rounded-md border p-3", isNoQuote - ? "border-amber-200 bg-amber-50" - : "border-transparent bg-transparent", + ? "border-amber-200 bg-amber-50 dark:border-amber-500/30 dark:bg-amber-500/10" + : "border-transparent bg-transparent" )} >
-