# 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**.