Files
baya-monorepo/archive/post-phase/ui/ui-phase-8-nurse-business-and-verification.md
T
2026-08-02 18:48:32 +03:30

22 KiB
Raw Blame History

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; 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 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:64onClick={() => 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:31showFormNow = !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 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-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 (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'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 — 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:

  • 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.