frontend phase 13

This commit is contained in:
hamid
2026-07-10 16:58:15 +03:30
parent 6186f54294
commit 85488bc25b
57 changed files with 3283 additions and 105 deletions
@@ -0,0 +1,156 @@
# Frontend Phase 13 (b14) — Reviews & Patient Care Records — report
**Track:** frontend · **Consumes:** [`reviews-records.md`](../../contracts/domains/reviews-records.md) (b14) +
`openapi/swagger.v1.json` · **Status:** complete, gate green.
The last feature-domain frontend phase: the moderated **review** loop and the **patient care-record** viewer/
authoring. Two new `services/{domain}` domains, four screens (+ a tab added to C3 + a CTA on the customer
booking detail), four new shared composites.
---
## 1. What was built
### Services
- **`services/reviews`** (`types`/`keys`/`constants`/`apis{index,clientApi,mockApi}`/`hooks`/`index`) — the
moderated-review domain. Hooks: `useNurseReviews` (infinite, published-only aggregate + list),
`useReviewEligibility(bookingId)`, `useMyReviewForBooking(bookingId)`, `useCreateReview` (invalidates
eligibility + my-review, **never** the public list/aggregate).
- **`services/patientRecords`** (same shape) — the continuity-of-care domain, **patient-scoped**. Hooks:
`usePatientCareRecord` (family record), `useRecordAccess` (gates before any clinical fetch),
`usePatientHistory` (paged visit-note history), `useUpdateCareRecord` (**customer-only** edit → `setQueryData`),
`useCreateVisitNote` (**nurse-only** append → invalidates history).
- **`services/patients`** — added `usePatient(id)` (E2 identity header; the seam already had `get(id)`).
### Screens & flows
- **Leave-a-review** — `/bookings/[id]/review` (`RatingInput` + body + `ReviewTagSelector`), gated on
completed/closed + server `can_review` + 1:1; on submit → persistent **"under review"** state (never public
here); already-reviewed shows the review's moderation state, never a second form. Entry: **`LeaveReviewCta`**
on the customer booking detail (page-only sibling, reads the cached booking + my-review).
- **Nurse-profile reviews tab (C3)** — added a «خدمات» / «نظرات» tab strip to the existing profile; the reviews
panel renders the **published-only** aggregate (rating + count) + an infinite list. The old single-review
snippet is subsumed.
- **E2 patient record viewer** — `/patients/[id]/record`: reused `PatientHeader` + ownership banner + four tabs
(داروها/روتین/سوابق/وظایف). The customer edits medications/routine/tasks; **سوابق** is the read-only nurse
visit-note history; a first-class, non-leaking **access-denied** card gates before any clinical fetch. Tapping
a `PatientCard` opens it.
- **Nurse visit-note authoring (E3)** — `NurseVisitNotesPanel` (co-located at `nurse/visits/[id]/`), mounted
**below** the f8 EVV banner: today's task checklist + a free-text note composer + the read-only continuity
history. **Append-only** — it exposes no record editing and never wires `useUpdateCareRecord`.
- **Longitudinal history** — patient-scoped, paged (Prev/Next when >1 page), read-only, newest-first, rendered
via the shared `VisitNoteCard` in both the E2 سوابق tab and the nurse continuity view; persists across nurse
changes (the mock seeds a multi-nurse default).
### Shared composites (each with a co-located `*.test.tsx`)
- **`RatingInput`** — 15 star input/display (custom, on the registered `star` `AppIcon`; filled
`var(--bal-warning)` / empty `var(--bal-divider)`; interactive = radiogroup, readOnly = static).
- **`ReviewTagSelector`** — multi-select review-tag chip group (i18n-free; caller passes `labelFor(code)`).
- **`VisitNoteCard`** — one read-only visit note (nurse name + Shamsi date + body + done/not-done task chips).
- **`PatientHeader`** — extracted from `PatientCard` so E1 + E2 share the identity header; `PatientCard` now
composes it + gained an optional `onOpen` tap handler.
- New icons registered: `notes`, `routine`, `tasks`, `history`, `family`.
- i18n: new top-level `reviews` + `records` namespaces (both locales, in sync) + `search.tab_{services,reviews}`
+ `patients.open_record`.
---
## 2. What is testable and exactly how (per phase §7)
Run `npm run dev` (with `NEXT_PUBLIC_API_URL` set; the b14 backend reachable or the seam mocks active — both
default on).
1. **Leave a review on a completed booking → pending → appears after moderation.** As a customer, open the
completed **booking 5005** (seeded in the f8 mock) → its detail shows **«ثبت نظر»**. Open it, give 4 stars +
body + a tag chip, submit → the screen shows **«در حال بررسی / under review»** and the CTA no longer offers a
second review. The review does **not** appear on the nurse's profile. To watch it publish (the f15 admin path
is deferred), in the browser console call the reviews mock's dev helper
`__mockPublishSubmittedReview(5005)` (exported from `services/reviews/apis/mockApi.ts`) → it appears on the
**nurse 1** profile reviews tab and the aggregate updates on the next fetch. A **cancelled** booking (5004)
shows no review CTA (not completed).
2. **View a patient record with tabs + ownership banner.** Patients tab → tap a patient → E2 shows the four
tabs, medication cards under داروها, the **«این پرونده متعلق به خانواده است …»** banner, and the customer can
edit a medication/routine/task (**ویرایش** → change → **ذخیره**) and see it persist (cache updates via
`setQueryData`, no reload). Navigating to **`/patients/8888/record`** (the `MOCK_FOREIGN_PATIENT_ID`) shows
the **access-denied** card, not a crash.
3. **A nurse appends a visit note (cannot edit the record).** As the nurse, open a visit (`/nurse/visits/5005`)
→ below the EVV banner, tick the task checklist, write a note, **«ثبت یادداشت»** → the note saves and shows in
the history. There is **no** medication/routine/task edit control anywhere in the nurse view.
4. **History persists across nurse changes.** The سوابق tab (customer) and the nurse continuity view show the
patient-scoped, newest-first timeline — including seeded notes from a **different** nurse (سارا محمدی) —
paginated.
5. **Gate checks:** `npm run check` green; `npm run test:ci` green (all suites); the reviews tab/list + record
edit show query caching + invalidation in React Query Devtools (no needless refetch).
---
## 3. What is mocked client-side + how it swaps
Both domains are **mock-primary** behind their `services/{domain}` seams (`USE_REVIEWS_MOCK`,
`USE_PATIENT_RECORDS_MOCK`, default `true`). See [`mocks-registry.md`](./mocks-registry.md) for the full rows.
- **Reviews:** `reviewsClientApi.getNurseReviews`/`createReview` map the live b14 routes **1:1**; the mock adds
what b14 doesn't serve — review-eligibility + my-review-for-booking (**REQ-026**) — and stands in for the
admin-only moderation (f15) via `__mockPublishSubmittedReview`. Swap = flip `USE_REVIEWS_MOCK` once REQ-026 lands.
- **Patient records:** `patientRecordsClientApi.getPatientHistory`/`createVisitNote` map the **real** b14
`care_records` GET/POST 1:1; the mock adds the family-owned record + access check (**REQ-027**, no backend
entity exists). Swap = flip `USE_PATIENT_RECORDS_MOCK` once REQ-027 lands (only the family-record/access
methods change).
- **f8 bookings mock (non-seam):** added **booking 5005** (`completed`) so a review is demoable + the cross-mock
read helper `mockGetBookingForReview` (one-way edge into bookings, no cycle).
---
## 4. Contracts consumed / requests filed
- **Consumed (real, mapped 1:1):** `POST bookings/{id}/review`, `GET nurses/{id}/reviews`,
`GET`/`POST patients/{id}/care_records`.
- **Filed** to [`for-backend.md`](../frontend/requests/for-backend.md):
- **REQ-026** — review-eligibility read + my-review-for-booking read + confirm the masked-author omission on
`ReviewListItemDto`.
- **REQ-027** — the family-owned `care_record` (medications/routine/tasks) GET/PUT + a `record_access` read
(or derive from the 403) + structured `taskResults` on a visit note. Flags that the wireframe's four-tab E2
record has **no data-model entity** — needs a product decision.
---
## 5. Follow-ups for later phases
- **⚠ Discovered pre-existing infra defect (repo-wide, NOT in the f13 diff): the ESLint unused-vars gate is a
no-op.** `client/eslint.config.mjs` tries to raise `@typescript-eslint/no-unused-vars` to `error` by
`.map()`-patching `eslint-config-next`'s default export — but that export never carries the rule (it lives in
the `eslint-config-next/typescript` subpath, which the config doesn't spread), so the patch matches nothing
and the rule is **never enabled** (verified via `eslint --print-config` — zero active `@typescript-eslint`
rules). `tsconfig.json` also lacks `noUnusedLocals`. Net effect: **unused imports/vars pass `npm run check`
silently**, contradicting `client/CLAUDE.md` golden rule #11 and the config's own comment. This is why the
adversarial review (not the gate) caught three dead imports in this diff — now removed; **the f13 diff is
verified 0 unused via `tsc --noEmit --noUnusedLocals --noUnusedParameters`.** The fix (spread
`eslint-config-next/typescript`, or re-register the rule in an explicit `**/*.{ts,tsx}` override, + fix the
false CLAUDE.md/config comments, + optionally add `noUnusedLocals`) is **deliberately deferred** here: it
surfaces ~6 pre-existing violations in earlier-phase files, so it wants a focused repo-wide cleanup + a
team decision, not a f13-scoped change. **Recommend a dedicated infra task.**
- **Review moderation UI** (admin approve/hide/reject queue) is **DEFERRED → f15** (`frontend-phase-15-b15`).
The client is publish-only; the dev helper stands in until then.
- **Tag-aggregation dashboards** ("% punctual") are deferred (the `GET nurses/{id}/review_tags` endpoint exists
and could back them). This phase renders per-review tag chips only.
- The **in-app "raise a concern" flag + emergency banner**`frontend-phase-14-b15`.
- Once REQ-026/027 land, flip the two mock flags; no hook/component change.
---
## 6. Gate
`npm run check` green (tsc + eslint). `npm run test:ci` green (257 tests, 58 suites; +17 new across
`RatingInput`/`ReviewTagSelector`/`VisitNoteCard`/`PatientHeader`/`PatientCard`). `npm run build` green with
`NEXT_PUBLIC_API_URL` set (all new routes compile; the env-required build failure without it is pre-existing and
unrelated). The f13 diff is verified **0 unused** via `tsc --noEmit --noUnusedLocals --noUnusedParameters`.
A **6-dimension adversarial review** (contract-fidelity, caching, security/access, i18n/RTL/tokens,
React-correctness, flow-integrity) ran with per-finding adversarial verification. Outcome:
- **Confirmed & fixed:** 3 dead imports (`Box` + `RECORD_HISTORY_PAGE_SIZE` in the E2 page,
`mockListBookingsForReview` in the reviews mock — the latter's orphaned bookings-mock export also removed);
the now-inaccurate history-only invalidation doc comment; the orphaned `search` i18n keys my C3 refactor left;
an unreachable task-checklist skeleton branch.
- **Confirmed but deferred:** the pre-existing ESLint-gate no-op (§5, repo-wide infra).
- **Verified false-positive (no change):** the `409`-for-not-completed (the contract specifies no status for
that case, only for the 1:1 `409`); the security/access dimension raised **nothing** (published-only,
append-only, and the non-leaking access gate all hold); the caching dimension raised **nothing**.
@@ -77,3 +77,6 @@ the frontend can build before the backend phase merges, and swap to the real HTT
| `BnplApi` | `client/src/services/bnpl/apis/mockApi.ts` | **The f11 BNPL installment checkout (D1D5)** b12 doesn't serve client-side (b12 is order-centric — eligibility/initiate/status/webhook — and **explicitly does not model the repayment schedule**; no provider/plan options, no wallet installment status → REQ-022/023/024). Reads the frozen request gross from the shared **f7 store** and plays the provider: `getBnplOptions` builds the provider set as **data** (دیجی‌پی 3/6/12 · اسنپ‌پی ۴ · اقساط بالین‌یار; per-plan monthly/down-payment/total via **integer parts-per-10000 BigInt math**, never a hardcoded fee in the UI); `checkEligibility` returns `eligible` unless the national-id last digit is `0` (→`not_eligible`) or the order exceeds `MOCK_CREDIT_CEILING_IRR` (→`ceiling_exceeded`) so both declined paths demo; `getBnplSchedule` serves the down-payment + N-installment rows (last absorbs the remainder → rows sum to total); `issueBnplToken` enforces b12 idempotency (same key → same token; repeat after settle / lapsed window → **`409`**) + a `redirectUrl` into the local provider-handoff harness; `acceptBnplSchedule` on success is the **settle stand-in and reuses the f9 conversion bridge** — flips the request `converted` (`mockMarkBookingRequestConverted`), inserts a **confirmed** booking (`mockInsertConvertedBooking`; a settled BNPL order = a card payment net-of-fee, payout invariant to method), and **seeds a provider-reported Wallet plan**; `getWalletInstallments` serves D5 (seeded active دیجی‌پی ۶-ماهه with paid/due-soon/upcoming rows + each settled checkout's plan). Money = served IRR digit-strings end-to-end (components only format) | `USE_BNPL_MOCK` (`services/bnpl/constants.ts`, default `true`) | Deliver **REQ-022** (options + schedule — real `bnplClientApi` targets `checkout_bnpl/options/{id}` + `checkout_bnpl/schedule/{id}`), **REQ-023** (eligibility accepts the D3 national-id/mobile/consent), **REQ-024** (`checkout_bnpl/wallet_installments` provider-reported status + a customer `bookingId` on the settled order), and make the upstream `bookingRequests` flow real, then set flag `false``checkEligibility`/`issueBnplToken`(`Idempotency-Key`)/`getBnplOrder` already map the live b12 routes 1:1; the settle-on-return reads the order (the real settle is the provider webhook). No hook/component change | 🟡 |
| BNPL provider-handoff harness (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the provider's hosted BNPL page so the initiate → redirect → return round-trip is exercisable without a provider: the mock `redirectUrl` points here, and its pay/cancel buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «در حال انتقال به ارائه‌دهنده», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b12's `redirectUrl` is the provider's **absolute** URL (the wizard does a full `window.location.assign` for `http(s)`), so this page is never linked; delete it when `USE_BNPL_MOCK` retires. The provider's return deep-link into `/bookings/checkout/bnpl/return` is backend/provider config | 🟡 |
| `PayoutsApi` | `client/src/services/payouts/apis/mockApi.ts` | **The f12 nurse earnings surface** b13 doesn't serve read-side for a nurse (b13's only nurse route is `GET nurse_payouts/history`; the four-bucket **earnings summary**, the per-booking **earnings list + money-state**, and a **nurse-readable payout detail** with batch context + booking links are gaps → **REQ-025**). Self-contained, money-correct fixtures exercising **every** UI state: all four earnings states (`pending`/`eligible`/`paid`/`clawback_applied`; booking ids 50015004 align with the f8 bookings-store seeds so "view booking" deep-links land), all four `PayoutStatus` values in history (`pending`/`submitted`/`paid`/`failed`, incl. a `failed` payout with `failureReason: 'invalid_sheba'` for the read-only failure banner), payout **details that reconcile** (`gross clawback = net = amount`, Σ booking-link amounts = `grossEarnings`), and a **signed net balance** computed with BigInt via a `MOCK_SCENARIO` toggle (`standard` = positive; **`clawback_heavy` = negative "owed back"** for phase §7 step 3). Timestamps are relative to `now` so the pending dispute-window countdown always ticks; money stays IRR digit-strings end-to-end (components only format). `getNurseEarnings` filters by `state` + paginates | `USE_PAYOUTS_MOCK` (`services/payouts/constants.ts`, default `true`) + `MOCK_SCENARIO` in `constants.ts` | Deliver **REQ-025** (earnings_balance + earnings list + nurse `nurse_payouts/{id}` detail + `failureReason` on the history DTO), then set flag `false``payoutsClientApi` already maps the live `GET nurse_payouts/history` 1:1 and targets the proposed slugs for the other three. No hook/component change | 🟡 |
| `ReviewsApi` | `client/src/services/reviews/apis/mockApi.ts` | **The f13 moderated-review trust loop.** b14 serves the review **submit** (`POST bookings/{id}/review`), the public **nurse reviews** page (`GET nurses/{id}/reviews`), and the tag rollup — those are mapped 1:1 in `reviewsClientApi`. But there is **no review-eligibility read** and **no my-review-for-booking read** (**REQ-026**), and the whole moderation transition (`pending_moderation → published`) is **admin-only (f15)**. The mock reads a booking from the shared **f8 bookings store** (`mockGetBookingForReview`) to gate eligibility on a **completed/closed** booking (aligns with the new completed seed 5005 / nurse 1 / patient 905), tracks the customer's submission as `pending_moderation` so eligibility flips `already_reviewed` + `getMyReviewForBooking` returns the persistent "under review" state, and seeds a **published list per nurse** (nurse 1 has 7 → the profile tab paginates; nurses 5/6 empty → empty state). The aggregate is **recomputed from the published list** (never a stored sum). A submitted review **never** enters any public list. Dev-only `__mockPublishSubmittedReview(bookingId)` stands in for the deferred (f15) admin queue so a human can watch a review appear on the profile. Money-free | `USE_REVIEWS_MOCK` (`services/reviews/constants.ts`, default `true`) | Deliver **REQ-026** (`review_eligibility` + `my_review` reads; confirm masked-author omission), then set flag `false``reviewsClientApi.getNurseReviews`/`createReview` already map the live b14 routes 1:1 and target the two proposed slugs for the gaps. Moderation UI itself is **f15** (admin). No hook/component change | 🟡 |
| `PatientRecordsApi` | `client/src/services/patientRecords/apis/mockApi.ts` | **The f13 continuity-of-care surface.** Two very different things: (1) the **nurse-authored visit-note history** (`getPatientHistory`/`createVisitNote`) is **REAL b14** (`GET`/`POST patients/{id}/care_records`), mapped 1:1 in `patientRecordsClientApi` (the append composes the ticked task checklist into the note `body` since the wire has no structured task field); (2) the **family-owned editable record** (medications/routine/tasks — the داروها/روتین/وظایف tabs) and the **access check** have **NO backend at all** (neither the b14 contract nor `data-model/10-reviews-and-records.md` model them → **REQ-027**). The mock is **patient-scoped** and lazily seeds a coherent default per patient: a default family record (customer edits it), a **multi-nurse continuity history** (two prior notes from *different* nurses, proving the history persists across nurse changes; a nurse append prepends to the same patient's history), and a **foreign-patient access-denied** path (`MOCK_FOREIGN_PATIENT_ID = 8888``canView:false` + a `403` on every read) so the non-leaking access-denied card is demoable. Clinical text is fixture data (never logged) | `USE_PATIENT_RECORDS_MOCK` (`services/patientRecords/constants.ts`, default `true`) | Deliver **REQ-027** (family-owned `care_record` GET/PUT + `record_access` + structured `taskResults`), then set flag `false` — the history/append methods already map the real b14 routes; only the family-record/access methods flip. Confirm whether the family-owned record is a real MVP entity | 🟡 |
| f8 bookings mock — completed-booking seed 5005 + f13 cross-mock reads | `client/src/services/bookings/apis/mockApi.ts` | **Non-seam additions (mirrors the f10 refunds precedent).** The f8 seeds had **no `completed` booking** (only `confirmed`/`in_progress`/`cancelled`), so f13's review flow needs one: added **booking 5005** (`status: 'completed'`, nurse 1, patient 905, one completed EVV session) so the customer can open a completed booking and leave a review. Also added a **cross-mock read helper**`mockGetBookingForReview(id)` (single booking, clone) — imported by the reviews mock to gate eligibility and read the patient/nurse snapshot for a submission (the `listBookings` seam row omits `patientId`/`nurseId`). One-way edge INTO bookings (the bookings mock never imports f13), so no cycle | — (part of `USE_BOOKINGS_MOCK`) | When the bookings flow goes real (b9/b10 conversion live), 5005 stops being a static seed and the cross-mock helpers retire with the reviews/records mocks | 🟡 |