Copying only the version left the cached detail with pre-save fields, so a
queued second panel save could send them with a fresh version and silently
revert what the first save wrote (e.g. POC name).
Keep the last saved row per work order so a queued panel save does not depend
on the board-detail cache, and pass the completion-doc row as a fallback so
saving a completion document works after the order leaves the list.
A save from the detail panel left the cached detail on the pre-save version
until its refetch settled, so a quick second save, Cancel WO or Complete could
conflict. The saved row's version now updates the detail cache before the patch
queue releases, and panel patches read their base at run time.
The board patch read the work order version from the loaded list only. Once a
save moved the order out of that list (e.g. to another week), the next save,
Cancel or Complete from the open panel failed for lack of a version. The panel
now passes its server row as the fallback patch base.
All six vendor-portal dispatch mutation hooks lacked meta.suppressErrorToast,
so a failed checklist toggle, signoff, comment, or uplift request/withdraw/
revise raised both the global MutationCache toast (leaking the raw server
message) and the section's own inline error, contradicting this ticket's
"no behavior change" goal versus main's try/catch-only handling.
Also fixes a related gap in checklist-section.tsx: updateItem.isError only
reflects the latest call on the shared mutation observer, so a failed toggle
whose item was not the most recent click showed no error at all. Tracks
failure locally instead, mirroring main's per-call error state.
Added regression coverage with the app's real query client (createAppQueryClient)
for comments, signoff, and uplift withdraw, asserting the inline message shows
and the global toast does not fire; plus a mixed pass/fail concurrent-toggle
case for the checklist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A site saved before the contacts list existed maps with no contacts, only
the legacy contact/phone fields. The site-record sync read the contacts
list alone, so the dialog blanked the POC, blocked a notes-only Save until
the contact was retyped, and when the work order had no POC the legacy
autofill made the diff report a contact edit nobody made. The legacy
contact is now the site's main contact for display, baseline and request.
Save sends the values it had when clicked, so the dialog's fields are now
disabled while the site update is in flight instead of accepting edits
that neither save would include.
The site-update mutation already toasts its own failure, so it sets
meta.suppressErrorToast and the global mutation toast no longer repeats it.
The Uplift column hid its trigger on a Completed or Canceled work order
with no live uplift, so once cancelled/revoked uplifts stop counting as
"has uplift", their read-only history had no way in. The cell now always
opens the uplifts dialog; closed work orders show a dash instead of Manage.
Admins may revoke only admin-approved uplifts. The Work Order Uplifts
dialog showed Revoke to whoever requested an auto-approved uplift, so an
admin requester saw it and was sent into the optional-reason revoke flow.
Both the template preview and a work order's completion document render
through the shared frame, which now splits the letterhead and blocks into
A4-sized sheets with the brand bars on every sheet and shows Page X of Y
in the preview pill.
The invite page showed "invalid or has expired, ask your admin for a new
invite" for any failed resolve call, including offline, timeouts and 5xx.
A member on a flaky connection was sent to their admin for a resend, which
revokes a token that was still valid.
Only the server's invalid_invite answer now ends on "Invite unavailable".
Any other failure shows "Couldn't load your invite" with a Try again
button that re-runs resolve, without a global error toast. Regression
tests cover a network rejection and a 500 followed by a successful retry.
The delete dialog states the server's full open work-order count, but the
server returns at most 200 ids. "View open work orders" navigated with
that capped list, so a site with 240 open work orders showed 200, and an
empty list opened the unfiltered board.
The exact-id link is now used only when the ids cover the whole count.
Otherwise the link opens Work Orders filtered to the site and every open
status across all weeks, the ticket's "board filtered to that site". The
board drilldown now reads a `sites` param for this; `ids` still wins.
Two groups of components were calling the API directly instead of using
this codebase's standard React Query hooks, with no shared caching and
duplicated submit/error-handling boilerplate.
- checklist-section.tsx, comments-section.tsx, signoff-section.tsx,
uplift-requests-section.tsx: replace direct vendorPortalApi calls and
hand-rolled submitting/error state with mutation hooks in the new
use-vendor-portal-dispatch-actions.ts. uplift-requests-section keeps
its existing multi-step evidence-upload orchestration intact and only
swaps the 3 raw API calls for mutateAsync, since restructuring that
flow around declarative mutation callbacks carried more risk than
value. Caught and fixed a real bug along the way: two components were
passing a prop callback directly as a mutation's onSuccess, which
would have forwarded React Query's extra callback arguments into it.
- followups/index.tsx, vendors/preferences.tsx: split from a single
file doing state, API calls, and rendering into a hook (owns data
and handlers) plus small presentational components, matching the
task-templates refactor pattern. Added a missing centralized query
key (vendorOperations.sitePreferences) that preferences.tsx was
building as a raw inline array.
- use-list-page-filters.ts: small shared hook for search/page/pageSize
state, used by the new followups hook.
29 new/updated tests across all touched files; none of these had
coverage for their mutation/error paths before.