Files
baya-monorepo/dev/post-phase/ui/ui-phase-8-nurse-business-and-verification.md
T
2026-07-17 13:22:04 +03:30

294 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 02](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 B3B6 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 3744); 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 221236) 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:198210`) 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:5860` 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 B3B6; `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.