ui phase 8

This commit is contained in:
hamid
2026-07-19 13:57:11 +03:30
parent edc38543fd
commit 1ef4feb911
41 changed files with 2113 additions and 667 deletions
@@ -860,3 +860,39 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
- **Proposed shape:** n/a — filed to put the need on record, not to propose an endpoint shape yet.
- **Status:** deferred, non-blocking — build nothing for it this phase; the 15s poll remains the only
freshness mechanism.
## REQ-055 — `submittedAt` on the nurse-facing `VerificationStatusDto` — filed by ui-phase-8 — 2026-07-19
- **Need:** Add `submittedAt` (UTC ISO, nullable) to the nurse-facing `VerificationStatusDto`
(`GET nurse_verification/status`-equivalent) — the timestamp the nurse first left `not_started`.
- **Why:** The B6 unified-journey rebuild (phase §3.2) shows a Shamsi submitted timestamp above the
"what happens next" timeline. The data already exists — the **admin** queue DTO
(`AdminVerificationQueueItem.submittedAt`) serves it today — but the nurse's own status read doesn't.
The client's `VerificationStatus` type now carries `submittedAt` as an optional, mock-tolerant field
(`services/verification/types.ts`); the mock stamps it when the checklist is first seeded
(`services/verification/apis/mockApi.ts`); the real `verificationClientApi` leaves it `undefined` and
B6 simply omits the timestamp line rather than fake one.
- **Proposed shape:** `VerificationStatusDto { …, submittedAt: string | null }` (same semantics as the
admin queue's field — set once, on first leaving `not_started`, never updated after).
- **Status:** open — mock-only until served; the real path omits the B6 timestamp line.
## REQ-056 — Nurse-facing read-back of submitted credential details — filed by ui-phase-8 — 2026-07-19
- **Need:** A nurse-facing read of the structured B5 fields already accepted by REQ-011's
`submit_credential_details` write: whether an INO number is on file (boolean — **never** the number
itself, which stays encrypted server-side and is never re-served by design), `specialties: string[]`,
and the optional registry fields (`issuingAuthority`, `issuedAt`, `expiresAt`). Either add these to
`VerificationStatusDto` directly or a sibling nurse-facing read.
- **Why:** B5's rebuild (phase §3.4) fixes the "returning nurse sees blank fields and a dead disabled
submit button" defect by hydrating the form from the server — but there is **no nurse-facing read** of
what was actually submitted (REQ-011 delivered only the write; the admin `decide` endpoint is the only
place these fields are currently readable, and only post-decision). The client's `VerificationStatus`
type now carries an optional, mock-tolerant `credentialSubmission` field
(`{ inoNumberSubmitted: boolean, specialties: string[], issuingAuthority: string | null, issuedAt:
string | null, expiresAt: string | null }`); the mock persists what `submitCredentialDetails` receives
and serves it back; the real path leaves it `undefined` and B5 falls back to its pre-phase-8 blank-form
behavior (never a lie about what's on file, just less helpful).
- **Proposed shape:** `VerificationStatusDto { …, credentialSubmission?: { inoNumberSubmitted: boolean,
specialties: string[], issuingAuthority: string | null, issuedAt: string | null, expiresAt: string |
null } | null }`. Verify against Swagger first — file only if it truly doesn't exist (per the phase
doc's instruction); at authoring time no such read was found in the b6 contract or swagger snapshot.
- **Status:** open — mock-only until served; the real path degrades to the pre-phase-8 blank-form
behavior (documented, not a regression — just not yet as helpful as the mock demonstrates).
@@ -110,7 +110,7 @@ the frontend can build before the backend phase merges, and swap to the real HTT
| Seam (interface) | File | What it fakes | Config flag | Make it real → | Status |
| --- | --- | --- | --- | --- | --- |
| `PatientsApi` | `client/src/services/patients/apis/mockApi.ts` | In-memory patient CRUD (list/get/create/update/soft-archive), **seeded empty** so onboarding + the empty state both demo; persists the client-augmented `relation`/`conditions` the wire `PatientDto` lacks (REQ-005) | `USE_PATIENTS_MOCK` (`services/patients/constants.ts`, default `true`) | Deliver REQ-005 (relation/conditions on `PatientDto` + create/update), then set flag `false``patientsClientApi` is already wired to the b3 `patients/*` routes | 🟢 (real, refinement-phase-4) |
| `ProfilesApi` | `client/src/services/profiles/apis/mockApi.ts` | Customer + nurse profile get/upsert and **avatar upload** (echoes an object-URL). Keeps guarded read-only fields (`isVerified=false`, zero aggregates). Augments customer name/language (REQ-007) + nurse `avatarUrl` (REQ-006) the wire DTOs lack | `USE_PROFILES_MOCK` (`services/profiles/constants.ts`, default `true`) | b3 `customer_profiles/*` + `nurse_profiles/*` are live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag `false``profilesClientApi` is wired (its `uploadAvatar` throws `501` until REQ-006) | 🟢 (real, refinement-phase-4) |
| `ProfilesApi` | `client/src/services/profiles/apis/mockApi.ts` | Customer + nurse profile get/upsert and **avatar upload** (echoes an object-URL). Keeps guarded read-only fields (`isVerified=false`, zero aggregates). Augments customer name/language (REQ-007) + nurse `avatarUrl` (REQ-006) the wire DTOs lack. **ui-phase-8:** `setAcceptingBookings(accepting)` now mirrors the flip in-memory (`isAcceptingBookings`, never touching `isVerified`) — this is the **real** endpoint (`POST nurse_profiles/set_accepting_bookings`, previously unwired), not a mock-only gap; the mock exists only so `USE_PROFILES_MOCK=true` local dev still demos the go-live toggle | `USE_PROFILES_MOCK` (`services/profiles/constants.ts`, default `true`) | b3 `customer_profiles/*` + `nurse_profiles/*` + `nurse_profiles/set_accepting_bookings` are all live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag `false``profilesClientApi` is wired (its `uploadAvatar` throws `501` until REQ-006; `setAcceptingBookings` is fully wired already) | 🟢 (real, refinement-phase-4) |
| `NurseBankAccountsApi` | `client/src/services/nurse/apis/mockApi.ts` | Bank-account list/add/set-primary/verify-ownership. Drives the استعلام شبا **pending→verified/mismatch** transition over 2 list reads (so the poll shows it), single-primary enforcement, masked-IBAN (last-4); the configured mismatch IBAN (`IR000000000000000000000000`, matches backend default) resolves to `matchedNationalId=false` | `USE_NURSE_BANK_MOCK` (`services/nurse/constants.ts`, default `true`) | b3 `nurse_bank_accounts/*` are live (the real `add` resolves the inquiry synchronously — no client poll needed); set flag `false``nurseBankClientApi` is wired | 🟢 (real, refinement-phase-4) |
| `AuthApi` | `client/src/services/auth/apis/mockApi.ts` (`authMockApi`) | Phone-OTP login offline: `requestOtp``{otpSent,resendAvailableInSeconds:120}`; `verifyOtp` accepts dev code **`123456`** and locks after 3 wrong tries (`otp_locked`); `getMe`/`selectRole`/`refresh` from a `MOCK_SCENARIO` toggle (`customer`/`nurse_unverified`/`no_role`) to exercise all router branches | `USE_AUTH_MOCK` (`services/auth/constants.ts`, default **false** — b2 is live) + `MOCK_SCENARIO` in `mockApi.ts` | The real `authClientApi` is already wired to the live b2 routes; set `USE_AUTH_MOCK = false` (already the default) — no hook/screen change | 🟢 real by default, 🟡 mock available |
| `GeographyApi` | `client/src/services/geography/apis/mockApi.ts` (+ `apis/seed.ts`) | The province→city→district reference hierarchy — a faithful subset of the b4 seed: 8 provinces, Tehran (city 101) with its 22 مناطق (1001…1022), and the white-space cities Mashhad/Isfahan/Shiraz/Tabriz/Ahvaz/Qom/Karaj as whole-city-only. Active-only, `sortOrder`-ordered. `seed.ts` also resolves a saved `cityId`/`districtId` back to names for the addresses & serviceAreas mocks | `USE_GEOGRAPHY_MOCK` (`services/geography/constants.ts`, default `true`) | b4 `geo/{provinces,cities,districts}` are live; set flag `false``geographyClientApi` is wired to the snake_case-param lookups. No hook/component change | 🟢 (real, refinement-phase-4) |
@@ -118,7 +118,7 @@ the frontend can build before the backend phase merges, and swap to the real HTT
| `ServiceAreasApi` | `client/src/services/serviceAreas/apis/mockApi.ts` | Nurse coverage areas (list whole-city-first / add / remove). Enforces `UNIQUE(cityId, districtId)` exactly as the server — a duplicate (incl. a second whole-city row) throws the same **`409`** (`area_duplicate`) so the coverage editor's inline dup handling is demonstrable | `USE_SERVICE_AREAS_MOCK` (`services/serviceAreas/constants.ts`, default `true`) | b4 `nurse_service_areas/*` are live; set flag `false``serviceAreasClientApi` is wired (maps the server 409 to the same inline message). No hook/component change | 🟢 (real, refinement-phase-4) |
| `AddressMapPicker` (map stand-in) | `client/src/components/geography/AddressMapPicker.tsx` | **Not a real map** — a bounded, tappable/draggable marker canvas (CSS grid, no Neshan/Google tiles, no network) that maps the pointer position to `{ latitude, longitude }` around the chosen city's centroid (`CITY_CENTROIDS`/`IRAN_CENTROID` in `services/geography/constants.ts`). Emits real coordinates for the create/update request | _none (component boundary)_ | Replace the canvas internals with a real map widget (Neshan/Google, inlined per the client CSP) that emits the same `{ latitude, longitude }` via `onChange``AddressForm` and every caller stay unchanged | 🟡 |
| `CatalogApi` | `client/src/services/catalog/apis/mockApi.ts` (+ `apis/seed.ts`) | The catalog skeleton + nurse pricing layer. **Categories mirror the b5 seed exactly** (5 categories, ids 15, `sortOrder` 04). Seeds representative **option groups/values** the fresh backend does **not** (an admin authors them per category) — incl. required + optional groups and one **cross-category** (`serviceCategoryId=null`) group — so the builder's required-option gate + cross-category rendering demo. Enforces the server's create validation in-memory: `400` missing required dimension / bad price, and the `(nurse, category, option-set)` duplicate **`409`** (via `optionSetSignature`). Variant store seeded **empty** so the offerings empty-state demos; the nurse builds variants live (across price units). `create`/`update`/`set_active`/`list`(active-first, paginated)/`get`. Money stays an **IRR digit-string** end-to-end | `USE_CATALOG_MOCK` (`services/catalog/constants.ts`, default `true`) | b5 `catalog/*` + `nurse_variants/*` are live; set flag `false``catalogClientApi` is wired to the action-style routes (camelCase bodies, `pageSize` pagination per REQ-010, `category_id` snake_case filter). **When swapped, categories will have NO option groups until an admin authors them** (the mock's groups were illustrative). No hook/component change | 🟢 (real, refinement-phase-4) |
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false``verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveat:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet) — the document uploads it accompanies are contract-backed. No hook/component change | 🟡 |
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true. **ui-phase-8:** `VerificationStatus` gained two mock-tolerant fields the nurse-facing wire doesn't serve yet — `submittedAt` (stamped once when `start()` first seeds the steps, REQ-055) and `credentialSubmission` (stamped by `submitCredentialDetails``inoNumberSubmitted` boolean + specialties/registry fields, **never** the raw INO number, REQ-056) — so B6's timestamp and B5's hydrate-on-return both demo pre-REQ | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false``verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveats:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet — the document uploads it accompanies are contract-backed); `submittedAt` (REQ-055) and `credentialSubmission` (REQ-056) are both undefined until served, degrading gracefully (B6 omits the timestamp line; B5 falls back to blank fields). No hook/component change | 🟡 |
| `BookingsApi` | `client/src/services/bookings/apis/mockApi.ts` | The post-payment engagement (b9). Seeds **2 confirmed bookings** (one 3-session multi-day, one single-visit) + `booking_care_instructions` + a per-session **EVV state machine**`checkInVisit` flips the session→`in_progress`/`checked_in` (booking→`in_progress`) and computes the **advisory** `checkInAddressMatch` (haversine vs the seeded address ± `MOCK_EVV_TOLERANCE_METERS`, `null` when GPS was absent); `checkOutVisit` requires an open check-in (**`400 no_open_check_in`** otherwise), completes the session (stamps `payoutEligibleAt`), and completes the booking + opens the dispute window once **all** sessions settle. `getCareInstructions` **404s any viewer but the assigned nurse** (the two-stage-disclosure boundary; the UI `enabled` gate means the customer never even calls it). Money stays IRR digit-strings with `gross = commission + payout` and `Σ visitPayout = payout`. **ui-phase-7:** `forViewer` now unmasks `addressSnapshotJson` for the nurse once the booking is `confirmed`+ (simulating REQ-051, still delivered) instead of unconditionally nulling it — `addr5001` gained `latitude`/`longitude` matching the EVV reference point so the new address-card map link is demoable; `listTodaySessions` stamps a `variantLabel` off the booking's frozen variant snapshot (REQ-052) | `USE_BOOKINGS_MOCK` (`services/bookings/constants.ts`, default `false`) | b9 `bookings/*` + `booking_sessions/*` are live, but a booking only exists after `bookings/convert` runs on a **paid** request — both upstreams (`bookingRequests` mock, card capture b10) aren't real client-side yet. Once conversion is live, set flag `false``bookingsClientApi` maps the routes 1:1 (+ `bookingsServerApi` for the RSC prefetch). No hook/component change. Deliver **REQ-051** (nurse-view address post-confirmation) + **REQ-052** (today-feed `variantLabel`) | 🟢 (real, refinement-phase-4) |
| `ILocationProvider` | `client/src/services/bookings/evv/locationProvider.ts` | **EVV GPS capture** — the only client seam f8 introduces. `getCurrentPosition()` never rejects (denied/unavailable → `null`, so a GPS problem is **advisory, never a block**). The **real** provider wraps `navigator.geolocation.getCurrentPosition`; the **mock** returns canned coordinates per mode so the in-range / advisory-out-of-range / denied paths are all demoable without a device (the mock `BookingsApi` computes the match against the same seeded reference point) | `NEXT_PUBLIC_EVV_MOCK_GPS` = `in_range` \| `out_of_range` \| `denied` \| `off` (default `in_range` while `USE_BOOKINGS_MOCK`, else `off`) | Set `NEXT_PUBLIC_EVV_MOCK_GPS=off` (or flip `USE_BOOKINGS_MOCK`) → the real `navigator.geolocation` provider is selected. Real **address-match math** stays server-side (backend geocoding seam), not here — this seam only *captures* the position | 🟢 (real, refinement-phase-4) |
| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued. **ui-phase-6:** `initiatePayment`'s `redirectUrl` is now `null` (was a stale pointer to the deleted card-gateway harness page — a latent bug, since the harness itself was already removed in refinement-phase-4; the checkout page's `!redirectUrl` branch already reads the outcome directly, no behavior change); `getCheckoutSummary` adds mock `nurseAvatarUrl: null`/`nurseVerified: true` (REQ-046, the C6 identity moment); the capture path stamps `capturedAt`/`createdAt` on the transaction so `PaymentOutcomeDto` serves `trackingCode`/`paidAt` (REQ-046, the confirmation receipt) and the new `getPaymentHistory` reads the same transaction list (REQ-047, wallet «پرداخت‌ها»); invoice creation adds mock `paymentMethod: 'card'`/`transactionReference`/`sellerFiscalIdentity` (REQ-049, fiscal-grade invoice) | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture) + **REQ-046** (nurse identity + tracking code/paid-at) + **REQ-047** (payment history) + **REQ-049** (invoice fiscal fields), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟢 (real, refinement-phase-4) |
@@ -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.