The dialog's wording, its actions and its behaviour all diverged from the
design prototype. It refused deactivation outright when the vendor had
open work orders, which SH-44 and SH-82 left as an open question rather
than a decided requirement.
Deactivation with open work orders now proceeds on explicit confirmation:
the design's title and body copy, the affected work orders listed as
links, and "Deactivate anyway" in place of a disabled button. The API
call carries confirmOpenWorkOrders so the server-side guard is cleared
deliberately rather than removed.
Three divergences in the vendor detail drawer:
- The header rendered the company as a subtitle under a title that had
already fallen back to the company, printing the same name twice when
the row carried no technician. The subtitle is now dropped when it
would repeat the title.
- The Google Maps link appeared only when a URL was stored, unlabelled,
so an empty value left no trace of the field. It is now a labelled
field below Address that reads em-dash when unset, as every other
field in the panel does.
- The status sat immediately beside Total Jobs. It now sits opposite it
at the right edge, and reuses the vendors table's dot-plus-text badge
so the state never reads by colour alone.
The drawer's Active toggle asked to deactivate whenever the switch moved
from on to off, reading the unsaved form value. An inactive vendor toggled
on and back off without saving therefore raised "Deactivate this vendor"
for a vendor that was already inactive.
Gate on the persisted technician status from the loaded roster instead,
falling back to the clicked row. Deactivation is a property of the saved
vendor, not of the toggle.
The reload fix re-seeded the form from queryClient.fetchQuery, which inherits the
global 5-minute staleTime. A 409 never invalidates that key -
useSaveVendorCompanyRoster invalidates only in onSuccess and its onError returns
early for conflicts - and the Add flow has no mounted observer on it, so reload
returned the cached roster with the same stale rowVersion and every retry 409'd
again. Update mode was unaffected because it recovers via query.refetch(), which
bypasses staleTime.
Pass staleTime: 0 on that fetch, since its sole purpose is the freshest
rowVersion. The regression test uses a client with the app's real staleTime; the
default test client uses 0, which masked this entirely.
The loading-aware gate pushed VendorTradeSpecialtiesField to 151 lines, one over
the changed-file maintainability cap. Move the rejection-message resolution and
the contract explanation to module scope; no behavior change.
Merging dev broke this branch: SH-126/SH-184 made Add-On a backend-authoritative
indicator (isAddOn) and removed it from the WOType union, but
read-detail-closability-fields still accepted "Add-On" and mapped "AddOn" onto
it, so tsc failed on both the mapper and the closability test.
Drop Add-On from the detail type set so a legacy payload normalizes to "", and
cover the normalization directly instead of asserting it through
detailToClosabilityInput, which only forwards the already-typed value.