cleanup phases 6
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# 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 `JalaliDateField`s. 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/profiles` — **real** 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/verification` — **mock-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.
|
||||
Reference in New Issue
Block a user