22 KiB
UI Phase 8 — Nurse business & verification
Mission: the nurse's business tools work, but the go-live journey is scattered across four disconnected nags, the verification flow runs two competing progress metaphors, and the publish CTA fires a success snackbar while publishing nothing. This phase unifies the setup journey into one activation checklist, rebuilds verification as a single vertical trust journey with the TrustBadge payoff visible, and fixes the real defects along the way (rejected-upload feedback, dead-end bank form, non-hydrating credentials, whole-city double-encoding). Verification is the product's core ritual; after this phase it should feel like it.
Track: frontend · Depends on: Phases 0–2; Phase 4 (reuses its verification-explainer / trust-dossier components) · Unlocks: the supply side can set up, get verified, and go live with confidence Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the frontend-designer skill — both are mandatory.
1. Context — where this sits
Balinyaar is a trust-first marketplace: a nurse's verification badge is what a family buys. The nurse
workspace (client/src/app/[locale]/(private-routes)/nurse/) is functionally complete — profile,
services/variant builder, coverage, bank, and the B3–B6 verification flow all exist and are
token-disciplined — but the composition undersells the product. Diagnosed root causes (all verified):
- The publish CTA fakes success.
nurse/services/PublishGate.tsx:64—onClick={() => enqueueSnackbar(t('publish_done'), { variant: 'success' })}. Nothing is published. The server computes searchability itself:is_searchable = isVerified && isAcceptingBookings && status != Suspended && variant.IsActive, one index row per variant × coverage area (SearchIndexMaintainer.cs:248NurseBookable) — so ≥1 coverage area is structurally required too. AndIsAcceptingBookingsdefaults tofalse(NurseProfileConfig.cs:19) with a real, unwired toggle endpoint (NurseProfilesController.SetAcceptingBookings) that reindexes in the same transaction. A fully verified nurse can still be invisible, and no UI surfaces why. - Four disconnected go-live nags for one journey: the blocked-until-verified banner (profile), PublishGate (services), the empty-coverage state, and the bank empty state each warn in isolation.
- Two competing progress metaphors: the B3 hub counts 7 checklist steps («X از Y» +
LinearProgress, inflated by the synthetic mobile step —verificationSteps.ts:17MOBILE_STEP) while B4/B5/B6 show an unrelated bare 3-stepStepperHeader(verification/review/page.tsx:28). - Rejected-upload recovery shows no feedback: in
components/DocumentUpload/DocumentUpload.tsxthe{rejected ? (branch (line 155) wins over: state === 'uploading' ?(line 193) — a re-upload stays frozen on the red rejected card, and the re-upload button stays enabled mid-flight. - The credentials form doesn't survive re-entry:
verification/credentials/page.tsxinitialises every field to''/[](lines 37–44); submit isdisabled={... || !anyUploaded}(line 255) whereanyUploadedreads only this-session local state (line 106); license dates are native Gregoriantype="date"inputs (lines 221–236) on a trust-critical Persian form. - Bank is a dead end once verified:
nurse/bank/page.tsx:31—showFormNow = !isLoading && (accounts.length === 0 || showForm);setShowForm(true)exists only in the mismatch branch (line 81). A nurse who switches banks cannot add an account. - Whole-city is encoded twice: the coverage scope toggle (
coverage/page.tsx:198–210) vsCascadingRegionSelect.tsx:152's own<MenuItem value="">{t('whole_city')}</MenuItem>— picking the latter under "specific districts" trips the district-required error (page.tsx:89). - Qualifications aren't editable:
nurse/profile/page.tsx:58–60silently round-tripseducationLevel/educationField/specializationsJson— yet the server'sUpsertNurseProfileCommandaccepts and persists all three (verified). Pure frontend gap, no REQ.
What already exists (do not rebuild):
- The
services/verificationseam — ONE cacheduseVerificationStatus()query feeding B3+B6, the data-driven step catalog (verificationSteps.ts), the dev-only mock admin sim (verification/page.tsx:87). Restructure the presentation, keep the architecture. DocumentUpload's idle→uploading(%)→success/error state machine + rejected variant — fix the branch precedence, don't rewrite the machine.BankStatusPanel's three states with maskeddir="ltr"IBAN; the pending ownership poll inservices/nurse.- Tested shared components:
VariantCard,PriceDisplay(BigInt-safe, Toman-at-the-boundary),CategoryTile,CascadingRegionSelect,TrustBadge,StatusChip. - Phase 0/1 foundations (themed
StepperHeader, Jalali date picker, EmptyState/ErrorState kit, PageHeader, card kit) and phase 4's verification-explainer / trust-dossier components — consume, never fork. - Real backends: profiles, catalog, serviceAreas, nurse-bank run
USE_*_MOCK = false; verification remains mock-primary (USE_VERIFICATION_MOCK = true) — leave the flags as they are.
2. Required reading (do this first)
- audit/nurse-workspace.md and audit/nurse-trust-ops.md — the full evidence + keep-lists.
- .claude/skills/frontend-designer/SKILL.md — the design contract (invoke the skill, don't just read it).
product/business/02-nurse-verification.md(automated vs manual checks, honesty constraints);product/business/03-service-catalog-and-pricing.md+04-search-and-matching.md(searchability).- Code: the nurse route tree (
profile,services,coverage,bank,verification/**),components/DocumentUpload/,components/geography/CascadingRegionSelect.tsx,services/verification/types.ts(nurse-facingVerificationStatushas nosubmittedAt;NurseCredentialnever carriescredentialNumber),services/profiles/types.ts(NurseProfileDto.isAcceptingBookingsis read-only today;UpsertNurseProfileInputlacks it). - Phase 7's report (
../../shared-working-context/reports/ui-phase-7-report.md) — where the nurse dashboard left its setup-status slot — and the REQ tracker for-backend.md (REQ-001…038 taken; confirm the high-water mark before filing).
3. Scope — build this
3.1 Activation checklist («راهاندازی») + an honest, real go-live gate
- Build
src/components/ActivationChecklist/(shared, tested): one tracker composing the five scattered states from already-cached queries — تأیید هویت و مدارک ✓ (useVerificationStatus), تکمیل نمایه ✓ (bio + avatar), حداقل یک خدمت فعال ✓ (useMyVariants), محدودهٔ پوشش ✓ (useServiceAreas), شبای تأییدشده ✓ (useNurseBankAccounts) — each row a StatusChip-style state + a deep link to its fix. Distinguish the tiers honestly: the first four drive search visibility; bank drives getting paid (not part ofis_searchable— label it «برای دریافت درآمد»). - Mount it on the services page (above
MyServicesList) and in the phase-7 dashboard's setup slot (replace any placeholder card phase 7 left there — one shared component, not a fork). Collapse it to a compact «فعال در جستجو» state once every row passes and accepting-bookings is on. - Replace PublishGate's fake success with the real switch. Wire the existing, unwired
POST nurse_profiles/set_accepting_bookingsthrough the profiles seam (types + clientApi + mockApi + auseSetAcceptingBookingshook that invalidates the profile query). The gate becomes state-driven: unmet conditions → guidance («برای نمایش در جستجو: …» listing exactly the unmet real conditions); met butisAcceptingBookings === false→ «شروع پذیرش رزرو» calling the real endpoint; live → the on state + «توقف موقت پذیرش». Success copy only after the mutation succeeds — never a no-op snackbar.
3.2 Unified verification journey (one spine, one metaphor)
- Rebuild the B3 hub (
verification/page.tsx+VerificationChecklist.tsx) as ONE vertical journey: grouped step cards — هویت (identity KYC + Shahkar + mobile), مدارک حرفهای (MoH license, INO membership, criminal record), بانک (IBAN-owner match) — on a single spine, each group folding the data-driven steps fromverificationSteps.ts(keep the catalog + synthetic-mobile-step architecture — do-not-regress; regroup presentation only). - Remove the competing 3-step
StepperHeaderfrom B4/B5/B6; those pages get a journey-context header (group name + «بازگشت به مسیر تأیید»). One progress answer everywhere. - The payoff: a live TrustBadge preview panel on the hub — «این نشان را خانوادهها میبینند» — that fills as groups pass, reusing phase 4's verification-explainer/trust-dossier components for the framing (consume, don't duplicate).
- B6 under-review: submitted timestamp (Shamsi) + a what-happens-next timeline (بررسی توسط کارشناس →
نتیجه در ۲۴–۴۸ ساعت → فعالسازی نشان) + the same journey header as siblings.
VerificationStatuscarries nosubmittedAttoday → file the REQ (§4); omit the line when absent, never fake a time.
3.3 DocumentUpload fixes + capture guidance
- Fix the precedence bug:
state === 'uploading'(and the success flash) must win over therejectedprop so a re-upload shows live progress. Keep the rejection reason visible above the progress UI during re-upload, and disable the re-upload button while in flight. Update the co-located test to cover rejected→re-upload→progress→success. - Capture guidance where cheap: a frame-overlay illustration for the ID-card/selfie local-capture mode (B4) + static hints («نور کافی، بدون تاری، چهار گوشهٔ کارت داخل کادر»). Client-side too-dark/blurry heuristics only if trivially cheap; no new dependencies.
3.4 Credentials form (B5) — survives re-entry, Persian dates
- Hydrate from server state: steps already
in_review/passedrender as submitted summaries (fromuseVerificationStatus), not blank inputs; derive the submit gate from server + session state so a returning nurse never sees a dead disabled button with no explanation. The raw INO/credential number is never returned by design (encrypted server-side) — render a submitted state («شمارهٔ نظام ثبت شد»), never re-prompt as if lost. If no nurse-facing read-back of the structured details (authority/dates/specialties) exists, file the REQ (§4) and hydrate mock-tolerantly. - Replace both native
type="date"fields with the phase-1 Jalali picker — license issue/expiry feed the credential-expiry sweep; wrong dates are a correctness risk, not a style nit. - Specialty chips polish: selected/unselected states off tokens, custom-specialty entry kept.
3.5 Services & variant builder
- Step 3 live preview: render the real
VariantCardas «اینگونه در جستجو دیده میشوید» composing the entered name/category/price — the nurse is composing a listing; show the listing. - Replace the wrapped
ToggleButtonGroupoption values (VariantBuilder.tsx:424sx={{ flexWrap: 'wrap' }}— grouped-button borders/corners break on wrap) with a chip group. - Fix the duplicate-warning contrast:
VariantBuilder.tsx:263sets body text tovar(--bal-warning)(amber on paper fails light-mode contrast) — restyle as an accent-edge panel withtext.primarybody, warning reserved for the edge/icon; add an "edit the existing listing" affordance on the 409 duplicate. The themedStepperHeaderarrives free from phase 0/1 — just consume it.
3.6 Coverage — one control owns whole-city (+ cheap map viz)
- Collapse the double encoding: exactly one control owns the whole-city/districts choice. Preferred:
drop the separate scope toggle and let
CascadingRegionSelect's district level own it (its «کل شهر» empty option is the choice —districtId=null= whole city both ways, matching the serviceAreas contract); alternatively keep the toggle and add a prop suppressing the select's own whole-city item. Either way the district-required error can no longer be triggered by a choice the UI itself offered. OtherCascadingRegionSelectconsumers (addresses, search) must be unaffected — prop-gate any change; run its test. - Optional static map visualization of covered areas consuming
components/geography— keep it cheap. Real tile rendering (DEFERRED → Phase 9's map picker; reuse what it lands). - Give
removeArea.mutateanonErrortoast (currently silent).
3.7 Bank — an accounts section, not a one-shot form
- Restructure
nurse/bank/page.tsxas an accounts section with a persistent «افزودن حساب دیگر» CTA (evidence §1.6). Change-IBAN path: add new → pending inquiry → verified → make primary → old account remains listed. No delete affordance unless the seam supports it (it doesn't — don't invent). - Surface the pending ownership poll explicitly: «در حال استعلام صحت شبا، معمولاً چند دقیقه طول میکشد…» instead of a silent pending chip.
- Fix the error→false-empty hazard: a failed
useNurseBankAccountsquery renders the phase-1 ErrorState with retry — never the "no account yet" empty state + open form (which invites a duplicate-IBAN submission). - Keep the three-state
BankStatusPaneldesign exactly as is (do-not-regress).
3.8 Profile — qualifications editable + public preview
- Make education level/field and specializations real form fields (select + chips), submitted through the existing upsert — the server accepts them today (§1.8); no REQ, no server change.
- Avatar upload + profile save get the phase-1 mutation-error convention (
onErrortoasts — both are silent today), and warn before navigating away with a staged-but-unsaved avatar. - «نمایهٔ عمومی من»: a preview screen (
/nurse/profile/preview) composing phase 4's C3 trust-dossier pieces (TrustBadge, attribute chips,ServicePriceRowlist, coverage chips) from the nurse's own data (own profile +useMyVariants+useServiceAreas+ own badge) — no dependency on the search index, so it works pre-publish. Link it from profile and services pages: it is the strongest motivator to complete bio/photo/credentials.
4. Mocks & seams in this phase
No new mocks or seams. All work stays behind the existing services/{domain} seams; do not flip
any USE_*_MOCK flag (verification is deliberately still mock-primary). The one seam extension is
adding setAcceptingBookings to the profiles seam (types + clientApi + mockApi in lockstep — the
endpoint is real; the mock mirrors the flip).
Backend gaps become REQ entries appended to for-backend.md — REQ-001…038 are taken; check the tracker's high-water mark (other UI phases may have filed more) and number onward. Expected filings, both rendered mock-tolerantly (present → render, absent → degrade gracefully):
submittedAton the nurse-facingVerificationStatusDto(B6 timestamp — the data exists; the admin queue DTO already serves it).- Nurse-facing read-back of submitted credential details (issuing authority, dates, specialties;
masked/type-only for the number — never the raw encrypted
credentialNumber) so B5 hydrates on re-entry. Verify against Swagger first — file only if it truly doesn't exist.
5. Critical rules you must not get wrong
- Verification status is server truth. The client NEVER flips
is_verified, never fakes a step result, never derives "verified" from anything but the aggregate. The mock admin sim stays dev-only. is_searchableconditions are server-side. The activation checklist reflects them; the search index flips only via server writes (the accepting-bookings endpoint reindexes in-transaction). Never claim "you are now visible" from a client-side condition check alone.- No fake success — anywhere. A CTA either performs a real mutation or is guidance. This is the bug this phase exists to kill.
- Honest-automation copy stays: only genuinely automated checks say «استعلام خودکار»; manual-review
steps never claim an authority check. TrustBadge
verifiedrenders only from the approved aggregate;expiredstays visually distinct from never-verified. - Masked IBAN +
dir="ltr"stays on every bank/IBAN render; national-ID and price inputs keep their LTR-pinnedtextAlign:'start'treatment. - Keep-lists from both audits: token discipline (zero hexes),
borderInlineStartaccents + logical props, money viaPriceDisplay/BigInt (never a total from rate alone), soft-deactivate-onlyVariantCard, edit-mode locking of variant identity fields,CascadingRegionSelect's cached geo queries + prefill guard, belt-and-braces duplicate coverage handling, locale digits + Shamsi dates. - Design contract: i18n in both catalogs, dark mode via tokens, MUI v9 API only (no
flexWrapas aStackprop),App*wrappers + icon registry (new icons inAppIcon/config.ts, lowercase), co-located tests for every shared component touched (DocumentUpload,CascadingRegionSelect, newActivationChecklist), fetch/cookies rules untouched (clientFetchvia the seam, never rawfetch).
6. Definition of Done
On top of the shared definition-of-done.md:
npm run checkgreen;npm run test:cigreen including updatedDocumentUploadtests (rejected→re-upload→progress) and newActivationChecklisttests.en.json/fa.jsonin sync for every new key; no hard-coded strings.- The publish CTA performs a real
set_accepting_bookingsmutation; grep proves thepublish_done-snackbar-with-no-effect pattern is gone. - Exactly ONE progress metaphor across B3–B6;
verificationSteps.tsstill drives rendering (no hard-coded step lists). - Re-uploading a rejected document shows live progress with the rejection reason still visible.
- Returning to the credentials page with steps
in_reviewshows submitted state — not blank fields with a dead submit button; license dates are Jalali inputs. - A verified-account nurse can add another bank account; a failed accounts query shows an error state, never the empty-state form.
- The whole-city choice is owned by exactly one control; the district-required error can no longer be triggered by picking a UI-offered option.
- Education/specializations round-trip through the real upsert and re-render after reload.
- Visual verification on all four axes (
/fa+/en× light + dark), mobile + desktop, for every touched screen —/fafirst.
7. How to test (what a human can verify after this phase)
- As the seeded unverified nurse →
/nurse/services: the activation checklist shows unmet rows, each deep-linking to its page; the go-live CTA is guidance, not a button that toasts success. - Complete verification via the dev admin sim → rows flip; the CTA becomes «شروع پذیرش رزرو»; click
it → Network tab shows
POST nurse_profiles/set_accepting_bookings; the panel shows the live/pause state. Searching as a customer now finds the nurse (server-side flip). /nurse/verification: one vertical journey with grouped cards (هویت / مدارک حرفهای / بانک) and a TrustBadge payoff preview; B4/B5 show no 3-step Stepper anywhere; B6 shows what-happens-next (+ timestamp once the REQ lands).- In B5, upload a doc, have it rejected (mock), re-upload → the progress bar animates while the rejection reason stays visible; the re-upload button is disabled mid-flight. Leave and return → submitted steps render as summaries; submit is not silently dead; dates open a Jalali picker.
- New variant: step-2 options render as chips (no broken grouped borders); step 3 shows the live
VariantCardpreview; a duplicate yields a readable warning + "edit existing" path. /nurse/coverage: no second whole-city affordance that errors; whole-city adds via the single control; (if built) the map shows covered areas./nurse/bankwith a verified account → «افزودن حساب دیگر» opens the form; pending shows the explicit inquiry copy; kill the API and reload → error state with retry, not the empty form./nurse/profile: edit education + specializations, save, reload → values persist; navigate away with an unsaved avatar → warning. «نمایهٔ عمومی من» renders the own-data listing with TrustBadge + prices + coverage.- Repeat the key screens on
/en, dark mode, and a ~390px viewport.
8. Hand off & document (close the phase)
- Update
client/CLAUDE.md(Project Structure) in the same change: the/nurse/profile/previewroute,components/ActivationChecklist/, the profiles-seamsetAcceptingBookingsaddition, and the reshaped verification hub. - Write
dev/shared-working-context/reports/ui-phase-8-report.md: what shipped per §3 subsection, the REQ numbers actually filed, the PublishGate→real-toggle decision with its server evidence, and any foundation files you extended (per the README ownership rules — minimally, noted, never forked). - File the REQs in the tracker (§4) with
filed by ui-phase-8attribution. - Save a memory note per operating-rules §8: the activation checklist's two-tier honesty (search visibility vs getting paid), the accepting-bookings wiring, the unified-journey decision, the DocumentUpload precedence fix.