# 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`** — 1–5 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**.
