Overdue maps to work order type 8 on create, inline correction, board
reads and the type filter, instead of the Other enum and the past-due
flag. It lists the PM service catalog and never sends a severity. Past
Due moves to its own toggle in the advanced filter Status section and
narrows the other filters.
A request raised from the work order stores the increase, so the vendor card
read a +90 request on a 600 NTE as "Requested $90.00 (was $600.00)". The card
now shows the new total, $690.00, for those requests; vendor requests are
unchanged.
Picking a video as uplift evidence reads its movie header before the file is stored in form state. A submit that landed inside that window validated the previous (usually empty) evidence, showed the missing-evidence error and dropped the request, forcing a second click. The submit button is now disabled and reads "Checking evidence…" while the latest pick is being checked, and the submit handler ignores submissions in that window.
The queue Amount column, the detail modal and the reject/revoke dialogs
showed requestedNTE (the resulting NTE total) while the decision toasts
showed delta (the increase), so a work order with an existing NTE named
two different figures for one request. Route every surface through one
upliftAmount helper that returns the increase, matching the prototype's
single uplift amount, and show it on both Approve buttons.
The evidence picker filtered to PDF/JPG/PNG and its label and missing-file
messages said the same, while the pre-check (and the server, which applies
the SH-116 allowlist and work-order counts to UpliftEvidence uploads)
accepts HEIC, MP4 and MOV. On a phone the picker hid exactly the photos and
clips the validator would pass.
The evidence input now uses the same accept filter as the completion
upload, exported once from document-validation, and the label and both
missing-evidence messages name all six types.
The vendor portal hint listed the photo and video limits but not the PDF one, so a vendor rejected for an 11 MB PDF saw a limit that was never stated. PDFs are the only accepted kind whose cap was missing from the copy; a test now pins the full hint.
The changed-file maintainability gate caps functions at 150 lines;
VendorPortalDocuments reached 156. Move the per-document row into
VendorPortalDocumentRow with no behavior change.
A work order with a zero granted total rendered $0.00 in the Pending
table's Approved on WO column and in the detail modal's Total line.
Both now share isGrantedAmount, so zero or unknown totals render a dash
and the column always matches the modal Total.
Persisted HEIC files and local files with an empty or octet-stream type
were classified as "other", so they did not count toward the 10 photo /
3 video limit. Both classifiers now recognise them by extension.
The Photos & Videos uploader and Extra Docs ignore a new selection while
the previous one is still being screened, and the vendor portal drops an
earlier pick whose video check finishes after a newer pick.
Also removes ticket keys from source comments.
The vendor completion upload and uplift evidence checked type, size and
duration but not the 10-photo / 3-video work-order limit. Use the counts the
dispatch detail now reports (shoc-backend#173), on the same basis as the
server: a new completion version does not count the document it replaces.
When the backend does not report counts yet, the server check still applies.
Extra Docs advertised the 90-second limit but only checked type and size.
Both dispatcher surfaces now share one screening step (type, size, count,
duration), and the Completion Doc media tab and Extra Docs count the whole
work order's photos and videos rather than only their own tab's share.
Failed local uploads no longer count toward the limit.
Shows open work orders with no dispatcher for the selected period and
drills into the same list on the board (dispatcher Unassigned, open
statuses, same dates; All time includes undated work orders).
Photos up to 10 MB (JPG/PNG/HEIC), videos up to 100 MB and 90 s (MP4/MOV),
at most 10 photos and 3 videos per work order, pre-validated with stable
generic messages on the Photos & Videos modal, the Completion Doc media tab,
Extra Docs and the vendor portal. Video duration is read from metadata when
the browser can; unreadable metadata never blocks. Mobile MIME variants
(empty type, octet-stream with a video extension, QuickTime) stay accepted.
The signed completion PDF keeps its 50 MB cap.
The Advanced Filters default was This week, and that value doubled as no
range, so an explicit This week searched every week. Other ranges passed
every undated WO through on the server. No range is now its own state
(nothing selected, like the prototype; clicking a selected range clears it).
It searches every week with undated rows, via includeDateless. Any selected
range narrows strictly. The pinned Unassigned queue and the WO# duplicate
lookup keep spanning every week.
The completed-date cell editor had no Clear control. It now offers Clear
when a date is set and the row is editable, patching completedDate to an
empty value, which the board PATCH persists as null. Completed and
Canceled rows stay locked, matching set/change.
TaskTemplateItemsField owns newItemText locally but stays mounted
across selectTemplate/startNewTemplate (which only call form.reset()),
so a typed draft survived switching templates - the base page cleared
this draft explicitly in both handleSelect and handleNew.
Remount TaskTemplateItemsField on selection change via key={selectedId}
instead of lifting the draft into the editor hook: it's transient
input-only state, not form data, so this keeps the fix local and lets
React's own remount semantics reset it. Confirmed the two new
regression tests fail without the key and pass with it.
The completion-doc upload endpoint sets DocStatus=Yes and bumps the work-order row
version in one transaction. Both signed-PDF upload paths then re-ran a "docStatus=Yes"
work-order patch using the pre-upload row version (the comp-doc dialog re-ran the whole
Generate flow; the slide-over posted docStatus directly). That patch always 409'd — and
in the dialog it also demanded a Date Work Completed — so a successful upload surfaced an
error to the user.
Both paths now stop after the upload settles: the mutation's onSuccess refreshes the
board and the UI reflects the generated state locally, with no second docStatus patch.
Re-apply the requested slide-over tab on every open via an open counter, so a
second comment/mention notification for the work order already on screen still
switches to Comments and re-scrolls to the highlighted comment. Treat a board
comment's mentions id array (plain @Name text) as a mention alongside encoded
@[id:name] tokens so mention items highlight and comment items exclude them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment and mention items open the work order on the Comments tab with the
comments they were raised for highlighted; a grouped assignment opens the
dispatcher's own open queue; uplift decisions open the work order's uplifts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Header bell panel and /notifications page share one feed from GET /api/notifications: labeled sections with live counts, unread styling, session-only dismiss and clear all (acknowledge rows excluded), and deep links to the unassigned queue and a work order's tab via ?wo=&tab=.
Two sections, Work order and Uplift request, divided by a single rule.
Work order shows Service, Vendor / Technician, Assigned Dispatcher and
Scheduled from the queue item, so it no longer fetches the work order
and no longer degrades to Unavailable for account-scoped staff. Uplift
request shows Requested By, Requested At, the justification in a
bordered block, the approved-on-WO breakdown and a horizontal
attachment row whose chips open the evidence in a new tab. Every field
renders data or an explicit placeholder. The footer shows Reject and
Approve for pending uplifts, Revoke for approved ones and nothing for
read-only records; closing is the header X.
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