# 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](ui-phase-2-shells-and-navigation.md);
> [Phase 4](ui-phase-4-customer-storefront.md) (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](../../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):

1. **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:248` `NurseBookable`) — so ≥1 coverage area is structurally required
   too. **And `IsAcceptingBookings` defaults to `false`** (`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.
2. **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.
3. **Two competing progress metaphors:** the B3 hub counts 7 checklist steps («X از Y» +
   `LinearProgress`, inflated by the synthetic mobile step — `verificationSteps.ts:17` `MOBILE_STEP`)
   while B4/B5/B6 show an unrelated bare 3-step `StepperHeader` (`verification/review/page.tsx:28`).
4. **Rejected-upload recovery shows no feedback:** in `components/DocumentUpload/DocumentUpload.tsx`
   the `{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.
5. **The credentials form doesn't survive re-entry:** `verification/credentials/page.tsx` initialises
   every field to `''`/`[]` (lines 37–44); submit is `disabled={... || !anyUploaded}` (line 255) where
   `anyUploaded` reads only this-session local state (line 106); license dates are native Gregorian
   `type="date"` inputs (lines 221–236) on a trust-critical Persian form.
6. **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.
7. **Whole-city is encoded twice:** the coverage scope toggle (`coverage/page.tsx:198–210`) vs
   `CascadingRegionSelect.tsx:152`'s own `<MenuItem value="">{t('whole_city')}</MenuItem>` — picking
   the latter under "specific districts" trips the district-required error (`page.tsx:89`).
8. **Qualifications aren't editable:** `nurse/profile/page.tsx:58–60` silently round-trips
   `educationLevel`/`educationField`/`specializationsJson` — yet the server's
   `UpsertNurseProfileCommand` accepts and persists all three (verified). Pure frontend gap, no REQ.

**What already exists (do not rebuild):**

- The `services/verification` seam — ONE cached `useVerificationStatus()` 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 masked
  `dir="ltr"` IBAN; the pending ownership poll in `services/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](audit/nurse-workspace.md) and
  [audit/nurse-trust-ops.md](audit/nurse-trust-ops.md) — the full evidence + keep-lists.
- [.claude/skills/frontend-designer/SKILL.md](../../../.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-facing `VerificationStatus` has **no** `submittedAt`;
  `NurseCredential` **never** carries `credentialNumber`), `services/profiles/types.ts`
  (`NurseProfileDto.isAcceptingBookings` is read-only today; `UpsertNurseProfileInput` lacks 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](../../shared-working-context/frontend/requests/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 of `is_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_bookings` through the profiles seam (types + clientApi + mockApi +
  a `useSetAcceptingBookings` hook that invalidates the profile query). The gate becomes state-driven:
  unmet conditions → guidance («برای نمایش در جستجو: …» listing exactly the unmet real conditions);
  met but `isAcceptingBookings === 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 from `verificationSteps.ts` (keep the catalog + synthetic-mobile-step architecture
  — do-not-regress; regroup presentation only).
- Remove the competing 3-step `StepperHeader` from 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. `VerificationStatus`
  carries no `submittedAt` today → 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 the
  `rejected` prop 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`/`passed` render as submitted summaries (from
  `useVerificationStatus`), 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 `VariantCard` as «این‌گونه در جستجو دیده می‌شوید» composing the
  entered name/category/price — the nurse is composing a listing; show the listing.
- Replace the wrapped `ToggleButtonGroup` option values (`VariantBuilder.tsx:424`
  `sx={{ flexWrap: 'wrap' }}` — grouped-button borders/corners break on wrap) with a chip group.
- Fix the duplicate-warning contrast: `VariantBuilder.tsx:263` sets body text to `var(--bal-warning)`
  (amber on paper fails light-mode contrast) — restyle as an accent-edge panel with `text.primary`
  body, warning reserved for the edge/icon; add an "edit the existing listing" affordance on the 409
  duplicate. The themed `StepperHeader` arrives 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. Other `CascadingRegionSelect` consumers (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](ui-phase-9-customer-account-and-care-circle.md)'s map
  picker; reuse what it lands).
- Give `removeArea.mutate` an `onError` toast (currently silent).

### 3.7 Bank — an accounts section, not a one-shot form

- Restructure `nurse/bank/page.tsx` as 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 `useNurseBankAccounts` query 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 `BankStatusPanel` design 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 (`onError` toasts — 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, `ServicePriceRow` list, 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](../../shared-working-context/frontend/requests/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):

- **`submittedAt` on the nurse-facing `VerificationStatusDto`** (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_searchable` conditions 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 `verified` renders only from the approved
  aggregate; `expired` stays visually distinct from never-verified.
- **Masked IBAN + `dir="ltr"` stays** on every bank/IBAN render; national-ID and price inputs keep
  their LTR-pinned `textAlign:'start'` treatment.
- **Keep-lists from both audits:** token discipline (zero hexes), `borderInlineStart` accents +
  logical props, money via `PriceDisplay`/BigInt (never a total from rate alone), soft-deactivate-only
  `VariantCard`, 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 `flexWrap` as
  a `Stack` prop), `App*` wrappers + icon registry (new icons in `AppIcon/config.ts`, lowercase),
  co-located tests for every shared component touched (`DocumentUpload`, `CascadingRegionSelect`, new
  `ActivationChecklist`), fetch/cookies rules untouched (`clientFetch` via the seam, never raw `fetch`).

## 6. Definition of Done

On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):

- [ ] `npm run check` green; `npm run test:ci` green including updated `DocumentUpload` tests
      (rejected→re-upload→progress) and new `ActivationChecklist` tests.
- [ ] `en.json`/`fa.json` in sync for every new key; no hard-coded strings.
- [ ] The publish CTA performs a real `set_accepting_bookings` mutation; grep proves the
      `publish_done`-snackbar-with-no-effect pattern is gone.
- [ ] Exactly ONE progress metaphor across B3–B6; `verificationSteps.ts` still 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_review` shows 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 — `/fa` first.

## 7. How to test (what a human can verify after this phase)

1. 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.
2. 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).
3. `/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).
4. 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.
5. New variant: step-2 options render as chips (no broken grouped borders); step 3 shows the live
   `VariantCard` preview; a duplicate yields a readable warning + "edit existing" path.
6. `/nurse/coverage`: no second whole-city affordance that errors; whole-city adds via the single
   control; (if built) the map shows covered areas.
7. `/nurse/bank` with 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.
8. `/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.
9. 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/preview`
  route, `components/ActivationChecklist/`, the profiles-seam `setAcceptingBookings` addition, 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-8` attribution.
- 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.
