18 KiB
UI Phase 8 — Nurse Business & Verification — Report (2026-07-19)
What was built
Real go-live switch + one activation checklist (src/components/ActivationChecklist/, new shared
component + useActivationChecklist hook):
- The profiles seam gained
setAcceptingBookings(types +clientApi+mockApi+ a newuseSetAcceptingBookingshook) — wiring the real, previously-unwiredPOST nurse_profiles/set_accepting_bookingsendpoint the audit found. Invalidates the nurse-profile query on success; the mock mirrors the flip (never touchingisVerified). ActivationChecklistfolds five already-cached queries (verification, nurse profile,useMyVariants,useServiceAreas,useNurseBankAccounts) into rows with two-tier honesty: identity/profile complete/≥1 active service/≥1 coverage area drive search visibility (is_searchable); the verified bank row is labelled separately as "برای دریافت درآمد" and never gates search. Collapses to a compact «فعال در جستجو» confirmation once every row passes and the nurse is actively accepting bookings. Mounted on/nurse/services(above the offerings list, insideMyServicesList) and inDashboardActivationSlot(replacing ui-phase-7's single-row placeholder) — one shared component.PublishGaterewritten from scratch: consumes the sameuseActivationCheckliststate (no duplicated condition logic) and renders one of three real states — unmet conditions → guidance naming exactly what's missing (publish_unmet_intro); met but paused → «شروع پذیرش رزرو» calling the real mutation; live → the on state + «توقف موقت پذیرش». Success/error toasts fire only after the mutation resolves — the oldenqueueSnackbar('published')no-op is gone (grep confirms no reachablepublish_done/publish_ctastring remains).
Unified vertical verification journey (nurse/verification/):
verificationSteps.tsgained the presentation-grouping layer (StepGroupKey,GROUP_ORDER,stepGroup/groupLabelKey/groupRoute/groupedDisplaySteps/groupStatus) — folds the existing data-driven step catalog into three journey groups (هویت / مدارک حرفهای / بانک) without touching the catalog itself.VerificationChecklist.tsxrebuilt as ONE vertical spine of grouped step cards (replacing the flat "X از Y" meter + 7-row list) — each card shows its steps, per-step reason/auto-note, and a single "برو"/"رفع مشکل" CTA to the screen that owns that group.TrustBadgePreviewPanel.tsx(new) — the hub's payoff: a liveTrustBadge(ownBadgeState, never invents a state) + a per-group fill indicator, framed as «این نشان را خانوادهها میبینند».VerificationJourneyHeader.tsx(new, shared across B4/B5/B6) — replaces the competing bare 3-stepStepperHeaderthose three screens rendered alongside the hub's own meter. One progress answer now: the hub's grouped spine is the only place progress is shown; B4/B5/B6 get a group name + «بازگشت به مسیر تأیید» back link.- B4 (
identity/page.tsx): addedCaptureGuideFrame— a cheap, dependency-free CSS viewfinder (corner brackets for the ID card, an oval for the selfie) + a static hint line above eachDocumentUpload, per §3.3's capture-guidance ask. No client-side blur/darkness heuristic — judged not "trivially cheap" without a canvas-pixel pass, so left out (documented, not silently skipped). - B5 (
credentials/page.tsx): hydrates INO/specialties/registry fields from a new mock-tolerantstatus.credentialSubmissionfield (REQ-056, filed). Once the INO number is on file it locks into a "شمارهٔ نظام ثبت شد" confirmation row (a «تغییر» link re-opens it) — never re-prompted as if lost, never silently re-sent blank (the raw number is never read back by design, and the server'sCredentialDetailsInput.inoNumberis required, so re-submitting without re-entering it isn't possible). The two nativetype="date"fields are nowJalaliDateFields. The submit gate now considers server-side document state (in_review/passed), not just this session's uploads, so a returning nurse is never dead-ended on a disabled button with no explanation; when genuinely nothing is on file yet, an explanatory caption replaces the silent disable. - B6 (
review/page.tsx): a Shamsi submitted timestamp from a new mock-tolerantstatus.submittedAtfield (REQ-055, filed) + aStatusTimelinewhat-happens-next (مدارک ثبت شد → بررسی توسط کارشناس → فعالسازی نشان) + the sameVerificationJourneyHeaderas its siblings. - Verification + B4/B5/B6 pages adopted the ui-phase-7
CONTENT_MAX_WIDTH+mx:'auto'+PageHeaderconvention (closing the trust-ops audit's "verification (620), nomx:auto" width-chaos finding that ui-phase-7 didn't reach).
DocumentUpload precedence fix + capture guidance (src/components/DocumentUpload/):
- Fixed the render-branch ordering:
state === 'uploading'and the success flash now win over the still-truerejectedprop, so a re-upload shows live progress (with the original rejection reason kept visible above the bar) instead of staying frozen on the red rejected card; the re-upload button is never rendered mid-flight (no double-submit). New final order: uploading → success → rejected (resting) → local error → idle. - Test suite gained a scripted rejected→re-upload→progress→success case asserting the reason stays visible and the re-upload button disappears during the upload.
Services & variant builder (nurse/services/):
VariantCardgained aninteractiveprop (defaulttrue;falsedrops the Edit/Deactivate row) — used for read-only preview contexts without forking the component.VariantBuilderstep 3 now renders the realVariantCard(interactive={false}) as «اینگونه در جستجو دیده میشوید», composing the entered name/category/price, replacing the old price-only estimate panel (PriceDisplay's estimate line is still shown — it's insideVariantCard— nothing was lost).- Step 2's wrapped
ToggleButtonGroup(broken borders on wrap) replaced with a chip group, matching the house pattern used elsewhere (B5 specialties, the profile's specializations). - The duplicate-listing warning restyled as an
AccentCard(tone="warning") withtext.primarybody and warning reserved for the edge/icon (was amber-on-paper, a light-mode contrast fail) — plus a new "ویرایش خدمت موجود" affordance that resolves the colliding existing variant viaoptionSetSignatureagainst the already-cacheduseMyVariants()list and jumps straight into editing it (onEditExisting, wired throughNurseServicesPage).
Coverage — one control owns whole-city (nurse/coverage/page.tsx):
- Dropped the separate whole-city/districts scope
ToggleButtonGroupentirely.CascadingRegionSelect's own district level (its «کل شهر» empty option) is now the only control —districtId = nullis a complete, valid whole-city submission, never an error state. The "district required" error class the audit flagged can no longer be triggered by picking a UI-offered option, because there is no longer a second control that could disagree with the select.CascadingRegionSelectitself is untouched — no prop-gating was needed, so addresses/search are provably unaffected. removeArea.mutategained anonErrortoast (was silent).- The optional map visualization was not built — deliberately deferred; the phase doc marks it optional ("(if built)" in the testing script) and real tile rendering is explicitly deferred to Phase 9.
Bank — an accounts section (nurse/bank/page.tsx):
- Restructured around a persistent «افزودن حساب دیگر» CTA once ≥1 account exists (replacing the
form-only-when-
accounts.length===0gate) with a cancel affordance on the now-optional form. A faileduseNurseBankAccountsquery now rendersErrorState+ retry — never the empty-state form (which invited a duplicate-IBAN submission blind). The pending-inquiry copy is now explicit about the wait («در حال استعلام صحت شبا؛ معمولاً چند دقیقه طول میکشد…»).setPrimarygained anonErrortoast.BankStatusPanel's three-state design is untouched.
Profile — qualifications editable + public preview (nurse/profile/):
- Education level/field are now real
selectfields (a curated preset list + a «سایر» free-text fallback — there is no server-side enum, so the client stores stable internal codes, never a locale-baked label, matching how every other coded field in this app works) submitted through the existing upsert (no REQ, no server change — confirmed the server already accepts+persists them). Specializations are chips off the sharedSPECIALTY_PRESETSvocabulary (same codes B5 uses). - Avatar upload + profile save already had
onErrortoasts (the audit's citation was stale — confirmed by reading the current code before touching it); added abeforeunloadguard so a staged-but-unsaved avatar is never silently discarded on a reload/tab-close. (In-app route-away interception was judged out of scope for a page-local fix — no cross-app unsaved-changes framework exists yet; noted below.) - New route
/nurse/profile/preview(«نمایهٔ عمومی من») — composes the C3 trust-dossier pieces (TrustBadge,VerificationPanel,ServicePriceRow) entirely from the nurse's own cached data (own profile +useMyVariants+useServiceAreas+ own badge viauseNurseTrustBadge(me.id)) — no dependency on the search index, so it renders truthfully pre-publish. Linked from both the profile page and the services list (MyServicesList).
What is now testable (and exactly how)
- As the seeded unverified nurse →
/nurse/services:ActivationChecklistshows the unmet rows (identity/profile/services/coverage) each with a «تکمیل» deep link, plus the separately-labelled bank row;PublishGatebelow it shows the blocked guidance naming the same unmet items — no button that fakes success. - Complete verification via the dev admin sim (
__mockApproveAll, B3's mock controls) → the identity row flips; once profile/services/coverage are also done,PublishGateshows «شروع پذیرش رزرو» → click → Network tab showsPOST nurse_profiles/set_accepting_bookings→ the panel flips to the live state + «توقف موقت پذیرش»; add a verified bank account too →ActivationChecklistcollapses to «فعال در جستجو». /nurse/verification: one vertical spine (هویت / مدارک حرفهای / بانک cards) + theTrustBadgePreviewPanelabove it. Open B4/B5 → no 3-stepStepperHeaderanywhere, just the group header + back link. B4 shows a dashed viewfinder + hint above each capture. B6 shows the Shamsi submitted date + the 3-node what-happens-next timeline.- In B5, upload a manual-step doc, reject it via the B3 mock admin control
(
__mockRejectStep('moh_competency_license', 'blurry_scan')), then re-upload from B5 → the progress bar animates while the rejection reason stays visible above it; the re-upload button is not rendered mid-flight. Leave (/nurse/verification) and return to B5 → the INO number shows the "on file" confirmation row (never blank), specialties/dates are pre-filled, dates open the Jalali picker. /nurse/services→ add a variant: step 2 renders chip groups (no broken borders on wrap); step 3 shows the liveVariantCardpreview; submitting the exact same category+options twice shows the restyled (readable, non-amber-body) duplicate warning with a "ویرایش خدمت موجود" link that opens the builder already editing the colliding listing./nurse/coverage: only one control (the district select) offers whole-city; picking «کل شهر» there and submitting never errors. Kill theremoveAreamutation (e.g. force a network error) → a toast appears instead of silent failure./nurse/bankwith a verified account → «افزودن حساب دیگر» opens the form (with a cancel action); submitting shows the explicit multi-minute inquiry copy while pending; kill the API and reload →ErrorState+ retry, never the empty-state form./nurse/profile: pick an education level/field (or «سایر» + free text), toggle specialization chips, save, reload → values persist. Stage an avatar (don't save), then try to reload the tab → the browser's native "leave site?" prompt appears. Open «پیشنمایش نمایهٔ عمومی من» (from both/nurse/profileand/nurse/services) → the own-data listing renders with TrustBadge +VerificationPanel+ priced services + coverage chips, reachable even before the nurse is search-visible.- Repeat the key screens on
/en, dark mode, and a ~390px viewport.
Verification performed: npm run check (tsc + eslint) is green. npm run test:ci is green — 107
suites / 462 tests, including the new ActivationChecklist suite, the extended DocumentUpload and
VariantCard suites, and every other pre-existing suite (no regressions). i18n key parity between
en.json/fa.json was verified programmatically (0 keys only-in-one-file). Not performed: a live
authenticated click-through in a browser (no browser-automation tool available in this session, same
constraint ui-phase-7 noted) — the "How to test" steps above are written for a human to run that pass
manually.
What is mocked / waiting on a real service
services/profiles— real by default (USE_PROFILES_MOCK=false);setAcceptingBookingstargets the real, already-livePOST nurse_profiles/set_accepting_bookingsroute — this is not a new mock, it is finishing the wiring of an endpoint that already existed server-side. The mock implementation was still added (mirrors the flip) so local dev withUSE_PROFILES_MOCK=truekeeps working. See the updatedmocks-registry.mdrow.services/verification— mock-primary (USE_VERIFICATION_MOCK=true, unchanged, per the phase doc's explicit instruction not to flip it). Two new mock-tolerant fields added toVerificationStatus:submittedAt(REQ-055) andcredentialSubmission(REQ-056) — bothundefinedon the real path, degrading gracefully (B6 omits the timestamp; B5 falls back to blank fields, same as before this phase). See the updatedmocks-registry.mdrow.- No other domain's mock flag changed. No new seams introduced.
Contracts
- Consumed:
dev/contracts/domains/{identity-profiles,verification}.md(unchanged this phase — no new contract landed to consume;set_accepting_bookingswas already documented server-side, just unwired client-side). - Filed to
for-backend.md: REQ-055 (submittedAton the nurse-facingVerificationStatusDto), REQ-056 (nurse-facing read-back of submitted credential details — INO-on-file boolean + specialties/registry fields, never the raw number).
Docs updated
client/CLAUDE.md"Project Structure": the nurse route tree (profile/→preview/page.tsx,services/PublishGate/VariantBuilder changes,coverage/page.tsx,bank/page.tsx, theverification/subtree's newTrustBadgePreviewPanel.tsx/VerificationJourneyHeader.tsx+ regroupedVerificationChecklist.tsx/verificationSteps.ts), the shared component tree (ActivationChecklist/,VariantCard'sinteractiveprop), theservices/profilesentry (setAcceptingBookings), and the i18n namespace list (nurseProfile/bank/coverage/services/verificationdeltas + the newactivationnamespace).dev/shared-working-context/reports/mocks-registry.md:ProfilesApiandVerificationApirows updated in place with this phase's additions.dev/constants/routes.ts(client): addedROUTES.NURSE_PROFILE_PREVIEW.
Follow-ups for later phases
- REQ-055/REQ-056 are genuinely useful, low-effort backend additions (both are pure reads over data the server likely already has, or can trivially derive) — good candidates for an early slice of a future backend refinement pass.
- The optional coverage map visualization (§3.6) was not built — deferred to Phase 9, which owns the real map-picker work this would reuse.
- In-app (client-side-routed) unsaved-avatar interception was scoped down to the
beforeunloadguard (covers reload/tab-close, the highest-value real risk). Intercepting an in-appLinknavigation away from the profile page would need a small cross-app "confirm navigation" primitive that doesn't exist yet — worth building once a second page needs the same guard, not invented single-purpose here. - The education-level/field select-with-"سایر"-fallback pattern stores a stable internal code (not a
wire enum —
NurseProfileDto.educationLevel/educationFieldare plain strings with no server-side vocabulary). If a future phase wants these queryable/filterable server-side, that would need a real enum contract — flagged here as a design note, not filed as a REQ (no current feature needs it). VariantCard'svisibilityon/visibilityoffdeactivate/reactivate icons are not registered inAppIcon/config.ts(pre-existing gap predating this phase, confirmed viagit blame-equivalent code read — not introduced here; surfaces as a harmless dev-only console warning in tests). Out of this phase's scope; a one-lineAppIcon/config.tsregistration whenever someone's next in that file.