Files
baya-monorepo/dev/shared-working-context/reports/ui-phase-8-report.md
T
2026-07-19 13:57:11 +03:30

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 new useSetAcceptingBookings hook) — wiring the real, previously-unwired POST nurse_profiles/set_accepting_bookings endpoint the audit found. Invalidates the nurse-profile query on success; the mock mirrors the flip (never touching isVerified).
  • ActivationChecklist folds 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, inside MyServicesList) and in DashboardActivationSlot (replacing ui-phase-7's single-row placeholder) — one shared component.
  • PublishGate rewritten from scratch: consumes the same useActivationChecklist state (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 old enqueueSnackbar('published') no-op is gone (grep confirms no reachable publish_done/ publish_cta string remains).

Unified vertical verification journey (nurse/verification/):

  • verificationSteps.ts gained 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.tsx rebuilt 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 live TrustBadge (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-step StepperHeader those 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): added CaptureGuideFrame — a cheap, dependency-free CSS viewfinder (corner brackets for the ID card, an oval for the selfie) + a static hint line above each DocumentUpload, 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-tolerant status.credentialSubmission field (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's CredentialDetailsInput.inoNumber is required, so re-submitting without re-entering it isn't possible). The two native type="date" fields are now JalaliDateFields. 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-tolerant status.submittedAt field (REQ-055, filed) + a StatusTimeline what-happens-next (مدارک ثبت شد → بررسی توسط کارشناس → فعال‌سازی نشان) + the same VerificationJourneyHeader as its siblings.
  • Verification + B4/B5/B6 pages adopted the ui-phase-7 CONTENT_MAX_WIDTH + mx:'auto' + PageHeader convention (closing the trust-ops audit's "verification (620), no mx: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-true rejected prop, 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/):

  • VariantCard gained an interactive prop (default true; false drops the Edit/Deactivate row) — used for read-only preview contexts without forking the component.
  • VariantBuilder step 3 now renders the real VariantCard (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 inside VariantCard — 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") with text.primary body 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 via optionSetSignature against the already-cached useMyVariants() list and jumps straight into editing it (onEditExisting, wired through NurseServicesPage).

Coverage — one control owns whole-city (nurse/coverage/page.tsx):

  • Dropped the separate whole-city/districts scope ToggleButtonGroup entirely. CascadingRegionSelect's own district level (its «کل شهر» empty option) is now the only control — districtId = null is 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. CascadingRegionSelect itself is untouched — no prop-gating was needed, so addresses/search are provably unaffected.
  • removeArea.mutate gained an onError toast (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===0 gate) with a cancel affordance on the now-optional form. A failed useNurseBankAccounts query now renders ErrorState + retry — never the empty-state form (which invited a duplicate-IBAN submission blind). The pending-inquiry copy is now explicit about the wait («در حال استعلام صحت شبا؛ معمولاً چند دقیقه طول می‌کشد…»). setPrimary gained an onError toast. BankStatusPanel's three-state design is untouched.

Profile — qualifications editable + public preview (nurse/profile/):

  • Education level/field are now real select fields (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 shared SPECIALTY_PRESETS vocabulary (same codes B5 uses).
  • Avatar upload + profile save already had onError toasts (the audit's citation was stale — confirmed by reading the current code before touching it); added a beforeunload guard 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 via useNurseTrustBadge(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)

  1. As the seeded unverified nurse → /nurse/services: ActivationChecklist shows the unmet rows (identity/profile/services/coverage) each with a «تکمیل» deep link, plus the separately-labelled bank row; PublishGate below it shows the blocked guidance naming the same unmet items — no button that fakes success.
  2. Complete verification via the dev admin sim (__mockApproveAll, B3's mock controls) → the identity row flips; once profile/services/coverage are also done, PublishGate shows «شروع پذیرش رزرو» → click → Network tab shows POST nurse_profiles/set_accepting_bookings → the panel flips to the live state + «توقف موقت پذیرش»; add a verified bank account too → ActivationChecklist collapses to «فعال در جستجو».
  3. /nurse/verification: one vertical spine (هویت / مدارک حرفه‌ای / بانک cards) + the TrustBadgePreviewPanel above it. Open B4/B5 → no 3-step StepperHeader anywhere, 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.
  4. 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.
  5. /nurse/services → add a variant: step 2 renders chip groups (no broken borders on wrap); step 3 shows the live VariantCard preview; 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.
  6. /nurse/coverage: only one control (the district select) offers whole-city; picking «کل شهر» there and submitting never errors. Kill the removeArea mutation (e.g. force a network error) → a toast appears instead of silent failure.
  7. /nurse/bank with 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.
  8. /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/profile and /nurse/services) → the own-data listing renders with TrustBadge + VerificationPanel + priced services + coverage chips, reachable even before the nurse is search-visible.
  9. 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/profilesreal by default (USE_PROFILES_MOCK=false); setAcceptingBookings targets the real, already-live POST nurse_profiles/set_accepting_bookings route — 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 with USE_PROFILES_MOCK=true keeps working. See the updated mocks-registry.md row.
  • services/verificationmock-primary (USE_VERIFICATION_MOCK=true, unchanged, per the phase doc's explicit instruction not to flip it). Two new mock-tolerant fields added to VerificationStatus: submittedAt (REQ-055) and credentialSubmission (REQ-056) — both undefined on the real path, degrading gracefully (B6 omits the timestamp; B5 falls back to blank fields, same as before this phase). See the updated mocks-registry.md row.
  • 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_bookings was already documented server-side, just unwired client-side).
  • Filed to for-backend.md: REQ-055 (submittedAt on the nurse-facing VerificationStatusDto), 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, the verification/ subtree's new TrustBadgePreviewPanel.tsx/VerificationJourneyHeader.tsx + regrouped VerificationChecklist.tsx/verificationSteps.ts), the shared component tree (ActivationChecklist/, VariantCard's interactive prop), the services/profiles entry (setAcceptingBookings), and the i18n namespace list (nurseProfile/bank/coverage/services/verification deltas + the new activation namespace).
  • dev/shared-working-context/reports/mocks-registry.md: ProfilesApi and VerificationApi rows updated in place with this phase's additions.
  • dev/constants/routes.ts (client): added ROUTES.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 beforeunload guard (covers reload/tab-close, the highest-value real risk). Intercepting an in-app Link navigation 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/educationField are 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's visibilityon/visibilityoff deactivate/reactivate icons are not registered in AppIcon/config.ts (pre-existing gap predating this phase, confirmed via git 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-line AppIcon/config.ts registration whenever someone's next in that file.