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.
Three review findings on the additive add path:
- Create-mode reload refreshed selectedRoster (including rowVersion) but left
the form company fields on their pre-conflict values, so a retry diffed stale
fields against the reloaded roster and could overwrite the concurrent company
update that caused the 409. Reload now resets the form from the fresh roster,
matching the select path.
- mapVendorRosterAdditivePatchToBackend copied companyFields verbatim while the
reconcile write mapper canonicalizes companyPhone, so PATCH and PUT could send
different phone shapes for the same input.
- emptyRosterTechnician seeded preferredContact "Phone" even though that
control was removed from the form, so filling the first blank row submitted a
fabricated value. append already omitted it; both paths now agree.
An empty tradeOptions array meant two different things: "facets query still in
flight" and "vocabulary genuinely unavailable". Both enabled fail-open free
text and showed the outage warning, so a normal cold fetch looked like an
outage. Anything committed in that window was added to knownTradesRef and stayed
selectable after the canonical list arrived, permanently undermining the gate.
Expose the facets query isLoading as tradesLoading, thread it to the field as
tradeOptionsLoading, and keep the canonical gate active while loading. The
outage warning now shows only once the query has settled empty.
Adversarial review found a last-writer-wins window: after a stale 409 the
roster is refetched while the form keeps its pre-conflict values, so the
form-vs-roster diff resent our stale value for any company field another
user changed in that window, silently reverting their edit.
Transmit only fields the user actually edited, taken from react-hook-form
dirty state. Selecting a company calls reset(), so loaded values are never
dirty and only genuine edits are sent.
dirtyFields is resolved during render: formState is a Proxy that only tracks
properties read at render time, so reading it inside the submit callback
returned empty. The vendors E2E caught that; unit tests could not, since
they pass the flags in directly.
The always-mounted aria-live feedback region inherited MUI Stack spacing,
adding a permanent gap under the trades input and shifting the approved
Vendor edit drawer layout. vendor-visual-regression caught it.
Move the region outside the spaced Stack and collapse it while silent, so
the live region stays in the DOM for announcements without claiming layout.
Restores the caption to MUI Typography; that swap was cosmetic and out of
scope for SH-249.
* fix(work-orders): address wizard create review findings
[recover] remove malicious eslint payload (was 166c63e4)
* fix(work-orders): remove legacy completedDate EditWorkorder path
[recover] remove malicious eslint payload (was f4e6132b)
* fix(work-orders): use local calendar day for completedDate
Restore todayIso() after parent merge reintroduced UTC slice, and keep the wizard-date-utils regression test.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(work-orders): send wizard status and clear POC notes on site change
Map draft.status to lifecycleStatus on board create, and reset pocNotes with POC fields when the wizard site changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(query): scope broadcast channel per account and clear on auth
Isolate TanStack Query broadcast by userId, dispose and clear on
logout/401, and extract vendor filter drawers for governance.
* feat(work-orders): board wizard create with service notes
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(work-orders): enforce single-service wizard create per SH-118
Use single service selection in the wizard and omit extraServices from board create.
* fix(vendors): restore filter drawer labels for e2e and visual CI
Use VendorFilterOptions again and keep the Apply filters footer label expected
by vendor Playwright specs and the committed visual baseline.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Integrate current origin/dev (vendor-core completion plus frontend
maintainability) into the work-order board shell branch.
Resolved semantic conflicts against the new frontend-maintainability
ESLint conventions introduced on dev: guard non-boolean && left
operands with Boolean()/!=null and replace raw <p> with the shared
Text component (media-upload-modal). PR #39 board shell/detail modules
and hooks preserved; merged vendor-core behavior and Text/ESLint
conventions preserved.
* feat(vendors): align directory UI with prototype
* fix(vendors): report the filtered total
* fix(vendors): keep company input in sync
* fix(vendors): scope facets to directory status
* chore: add eslint, prettier, husky and commitlint tooling
* ci: add GitHub Actions workflow for lint and build
* build: migrate from CRA to Vite with TypeScript config
* docs: add architecture plan and design system documentation
* fix: scope ESLint to new components and hooks directories
* feat: add HTTP client, query cache and shared utilities
* feat: add theme system and global application styles
* feat: add shared UI, layout and domain badge components
* feat: add auth domain, provider and login page
* feat: add app shell, file-based routing and bootstrap
* feat: add protected layout and dashboard module
* feat: add accounts CRUD module
* feat: add assets CRUD module
* feat: add contacts CRUD module
* feat: add employees CRUD module
* feat: add locations CRUD module
* feat: add calendar events module
* feat: add follow-ups CRUD module
* feat: add PM schedules CRUD module
* feat: add work orders module with dispatch modals
* feat: add vendors CRUD and portal token panel
* feat: add vendor purchase orders module
* feat: add uplifts queue module
* feat: add settings for dropdowns and task templates
* feat: add vendor portal routes and signature capture
* test: add Vitest setup and Playwright login e2e spec
* chore: remove legacy CRA pages, Redux store and JS hooks
* chore: update gitignore and env example for Vite
* docs: translate pt-BR docs and Cursor rules to English
* fix(ci): fix login e2e session mock and prettier formatting
* fix(build): use mjs router script for Node 20 CI compatibility
* chore: remove migration scripts and unused generouted router
* fix(auth): restore JWT and align CRUD with backend routes
* fix(api): add no-content helpers and align paths with backend
* fix(accounts): align detail and mutation payloads with backend
* fix(contacts): resolve detail via GetContacts and map address DTOs
* fix(work-orders): align routes, delete body, and create payload
* fix(vendors): delete vendors via REST route
* fix(pm-schedules): handle empty save and delete responses
* fix(employees): add fallbacks for detail and dropdown calls
* chore(calendar): disable routes until backend exists
* chore(env): switch tracked env vars to Vite prefixes
* fix(api): align frontend contracts with backend review findings
Correct Work Order getById query param, asset site options via Location API,
Employee JobTitleId payload, and remove stale API paths.
* fix(employees,pm-schedules): align forms with backend API contracts
Align PM Schedule form and save payload with PMSchedule_DTO fields.
Bind employee Job Title select to jobTitleId for create/update payloads.
Add regression tests for both flows.
* fix(pm-schedules): gate edit/delete for Dev API contract
Production Dev API exposes only GetList and Save.
Hide unsupported edit/delete UI and block the edit route.
Add regression tests for disabled actions.
* docs(env): document VITE_API_URL must include /api suffix
* refactor(api): extract shared API prefix URL resolution
* feat(build): fail build on misconfigured absolute VITE_API_URL
* test(api): cover API URL contract and prefix resolution
* feat(auth): disable login submit until email and password are valid
* test(auth): align login tests with disabled submit behavior
* Feat/ab/menu-and-header (#17)
* chore(deps): add lucide-react for layout icon migration
* feat(auth): add getPrimaryUserRole helper for header display
* style(theme): add sidebar active tokens and nav group typography
* refactor(menu): migrate nav icons to lucide and trim menu groups
* feat(layout): redesign sidebar, topbar, and admin shell viewport
* chore(menu): hide Reports and Documents from sidebar
---------
Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
* ci: add frontend PR quality baseline (#18)
* chore(deps): add lucide-react for layout icon migration
* feat(auth): add getPrimaryUserRole helper for header display
* style(theme): add sidebar active tokens and nav group typography
* refactor(menu): migrate nav icons to lucide and trim menu groups
* feat(layout): redesign sidebar, topbar, and admin shell viewport
* chore(menu): hide Reports and Documents from sidebar
* ci: add PR quality baseline checks
* ci: avoid self-matching standards guard
* ci: split frontend quality gates
---------
Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
---------
Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
Co-authored-by: Alexandre Brandizzi <alex_brandizzi@hotmail.com>