Three real behavior deltas flagged in review, each closing a gap
against the "no behavior change" claim:
- use-vendor-portal-session.ts: add retry:false and
meta:{suppressErrorToast:true} so an invalid/expired token fails
fast with a single request and no duplicate global toast (this was
also the root cause of the failing e2e test - the toast and the
inline error message both had role="alert", tripping a strict-mode
locator match)
- task-template-schema.ts: trim the name before validating so a
whitespace-only name is rejected, matching the old manual
form.name.trim() check
- task-template-detail-form.tsx: block Enter-triggered implicit
submission on the Template Name / Description inputs, since the
original page had no <form> element and Enter did nothing
- task-template-items-field.tsx: surface a visible error when a
loaded item has empty text, since the schema already blocked save
in that case but gave no way to see why; use-task-template-editor.ts
now eagerly validates after loading a template so this reflects
reality immediately instead of only after an unrelated edit
Also extract every user-facing string in the task-templates feature
into TASK_TEMPLATE_COPY (task-template-constants.ts) instead of
inline literals scattered across 5 files.
6 new regression tests cover all of the above; full suite (36 tests
across the 2 refactored areas) still green.
vendor-portal-provider.tsx and task-templates.tsx mixed data-fetching,
state, and rendering in one file, with no caching and hand-rolled form
state. This is a pure refactor with no behavior change.
- vendor-portal-provider.tsx: replace manual useEffect/useState fetch
with useVendorPortalSession (useQuery), closing the token race
condition the old key={token} remount was working around
- task-templates.tsx: split into useTaskTemplateEditor hook plus 4
presentational components; replace hand-rolled form state with
react-hook-form + zod validation; replace the "new" string sentinel
with a NEW_TEMPLATE_ID constant
- add 29 tests (hook, component, and full-page integration) confirming
identical behavior to the original code
Split DashboardPage's scope/paging state into useDashboardScope and the header
controls into DashboardHeaderActions, and flatten parseDashboardDrilldownParams'
hasAny check, keeping every function under the complexity/line-count gates.
- SH-336: add the dispatcher-scope picker (All / My WOs / individual) for
Scheduler/Admin via a new viewAllDispatchersOnDashboard role gate, a static
'My WOs' label for Dispatcher, and hide the dispatcher tables (and skip their
queries) when the viewer cannot see all dispatchers. Scope drives dispatcherId
on stats, workload, performance, and regions (SH-347 contract).
- SH-352: add the Status Distribution module with its empty state and status
drill-down.
- SH-347: show Assigned/Completed counts and colour the completion rate
(green >=90, amber 70-89, red <70) on Dispatcher Performance.
- SH-348: drill a region bar into the board region filter; hold the
unconfirmed 'Unmapped/Other' bucket off the chart.
- SH-349: label Vendor Insights as all-time, company-wide.
- Make the drilldown-filters effect fire only on URL change; add a deterministic
clock to the status-drilldown test; add the dashboard visual regression spec
and baselines.
Wire the backend's GET uplifts/can-approve tier decision into the
pending row and detail modal actions. Approve/Reject now stay disabled
with the plain 'Requires Tier N role' tooltip unless the backend says
the current user may decide at that tier, so roles the backend refuses
(Dispatcher) never get a clickable action or a refusal toast.
Members created before the SH-326 model can carry data the current
backend contract rejects (PUT team-members accepts only the
Dispatcher/Scheduler/Admin roles and the ten palette colors), which made
them impossible to edit.
- Legacy roles (e.g. Manager) now show as "<role> (legacy)" in the edit
form with helper copy asking to choose Dispatcher, Scheduler, or Admin;
save stays blocked until one is chosen and the legacy value is never
sent. View mode marks the stored role as legacy too.
- Stored colors outside the current palette are canonicalized when they
only differ by casing; otherwise the nearest palette color (RGB
distance, first as fallback) is preselected with a notice naming the
replaced value, so saving never fails on an untouched legacy color.
- Role/color options move to team-member-form-options.ts with unit tests
for canonicalization, nearest-color matching, and replacement notices.
Move the two paged dispatcher cards and their paging view-model derivation out
of DashboardPage into DispatcherTablesSection (a fragment, so both cards remain
direct children of the dashboard grid). This keeps DashboardPage under the
project complexity threshold after the pager additions, with no change to the
rendered layout or behaviour.
Workload and Performance page server-side at 10 rows and now return Page,
PageSize, and TotalDispatchers on dev (backend#126/#127), so wire the pager
that SH-346 and SH-347 require: getWorkload/getPerformance send a 1-based
`page` and parse the paging envelope, the hooks and query keys carry the page,
and each dispatcher table renders a 10-row pager. Page state is per-table and
resets on a range change so switching range never lands on an out-of-range
empty page that reads as a false "no dispatchers" state.
Drop `docs=No` from the Aveta Pending drilldown. `docs` filters on
completion-document status, not the Aveta clearance document the tile counts
(SH-294 / backend#138), so it narrowed to the wrong rows. The board has no
missing-Aveta-document filter, so the drilldown is now the Aveta-required work
in the two-day open window — a deliberate superset of the count.
- Gate the Revoke action on the approved queue row and the detail modal
to admin users; non-admins get a disabled button with a plain
'Only admins can revoke uplifts' tooltip and the dialog cannot open.
- Stop nesting an h2 inside DialogTitle in the uplift detail modal
(hydration error) by rendering the title text as a non-heading.
- Handle work-order detail/uplift query 403s quietly in the detail
modal: sections fall back to 'Unavailable' and the queries suppress
the global error toast (QueryCache now honors suppressErrorToast).
- Extract UpliftDecisionDialogs and openUpliftEvidence from the page to
meet the changed-file maintainability gate (complexity 23 -> pass).
computeIsPastDue and the day view's todayKey derived today from
toISOString() (UTC). West of UTC that rolls over hours early, so work
scheduled for today showed as Past Due every evening. Both now use the
local todayIso() helper the board already uses; regression tests pin
23:30 and 00:30 in America/Sao_Paulo with fake Date.
Test stability:
- adv-search page sync: scope pagination queries with within() instead of
scanning ~330 buttons / 3000 nodes on every waitFor poll
- pm-schedules-list-actions: static import of the edit page so module
transform does not count against the 5s test budget
- vendor create modal / detail drawer: userEvent.setup({ delay: null });
15s describe budget for full MUI dialog renders under parallel load