# Frontend Phase 13 (b14) — Reviews & patient care records

> **Mission:** close the trust loop and the continuity-of-care loop in the client. After a visit is
> completed, the family leaves **one moderated review** that surfaces on the nurse's public profile only
> once it clears moderation; and the **family-owned, patient-scoped care record** becomes a real screen —
> the customer reads/edits it (داروها/روتین/سوابق/وظایف) under an ownership banner, while the assigned
> nurse may only **append a visit note** (the EVV check-in/out itself already shipped in f8). This is the
> brand-survival surface: vulnerable patients are cared for unobserved at home, so we never render
> unmoderated content publicly, we never let a nurse edit the family's record, and we treat clinical
> fields as sensitive.
>
> **Track:** frontend · **Depends on:** [`frontend-phase-8-b9.md`](./frontend-phase-8-b9.md) (booking
> detail · sessions · EVV) + the **b14** contract [`reviews-records.md`](../../contracts/domains/reviews-records.md) ·
> **Unlocks:** (last vertical-feature frontend phase — the support/notification surfaces in
> [`frontend-phase-14-b15.md`](./frontend-phase-14-b15.md) reuse these patterns)
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.

---

## 1. Context — where this sits

This is the **last feature-domain frontend phase** before the support/admin consoles. The booking
lifecycle is fully built: a customer can search, request, get accepted, pay (escrow / BNPL), and the
nurse runs the visit with EVV. The two things still missing on the client are the **post-visit review**
(what makes the marketplace's rating signal real) and the **patient care record viewer/authoring**
(what makes continuity-of-care real across nurse changes). Both are described in the wireframe's
**Section E** (E1/E2 patient record, E3 visit note) and **Section C** (the review snippet on the nurse
profile C3). You implement the family-facing review + record screens and the nurse-facing append-only
visit-note part of E3.

**What already exists (do not rebuild) — confirmed in the codebase + prior handoffs:**
- **The whole frontend foundation** from [`frontend-phase-0.md`](./frontend-phase-0.md): the three actor
  shells (customer mobile + 5-tab bottom nav خانه/رزروها/بیماران/کیف‌پول/پروفایل, nurse, admin), the
  `services/{domain}` + TanStack Query caching pattern (copy the `auth` service shape:
  `types.ts`/`keys.ts`/`apis/clientApi.ts`/`hooks/use*.ts`/`index.ts`), the contracts→types pattern, the
  shared composite components (status chip, stepper, cards), the money/format util, and the i18n
  namespace baseline (a `reviews` namespace was reserved in f0 — fill it; add a `records` namespace).
- **Booking detail, sessions & EVV** from [`frontend-phase-8-b9.md`](./frontend-phase-8-b9.md): the
  booking-detail screen, the status timeline, the nurse EVV **check-in/check-out** banner and the
  post-confirmation care-instructions surface, and the booking status enum (the **completed/closed**
  state that gates a review). **Reuse the booking-detail screen and the booking `services` domain** — the
  "Leave a review" entry point hangs off a completed booking; the visit-note authoring is the **note +
  task-checklist** part of E3 that sits *below* the EVV banner f8 already built. Do **not** rebuild EVV.
- **The patients list & a patient's identity** from [`frontend-phase-2-b3.md`](./frontend-phase-2-b3.md):
  E1 (patients list, Patients tab) and the patient header (name, age/gender, conditions) already exist —
  the **record viewer E2 is a new screen reached from a patient**; reuse the patient header, don't
  re-fetch the patient identity from scratch.
- **The nurse public profile (C3)** from [`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md): it already
  renders avatar/badges/services and a *single* review snippet. This phase adds the **reviews tab** to
  that existing profile — extend it, don't fork it.
- The **contract** [`reviews-records.md`](../../contracts/domains/reviews-records.md) produced by
  backend phase b14 — the source of truth for every shape below. If it is not yet published, mock behind
  the seam (§4) and file the gap (§8).

## 2. Required reading (do this first)

- [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and
  [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) — how you
  work and the tick-list you are graded on.
- [`../../../client/CLAUDE.md`](../../../client/CLAUDE.md) in full — the RSC/client boundary, the
  `services/{domain}` + Query rules, i18n, theme/tokens, cookies. Non-negotiable.
- **Invoke the `frontend-designer` skill before any visual work.** It is the design/brand contract
  (palette: teal `#1d4a40`, terracotta `#d98c6a` for the nurse-view E3 accent, cream; tokens, typography,
  the `App*` library, layout shells, the hard UI rules). Every screen in this phase goes through it — the
  star input, the tag chips, the tabbed record viewer, the ownership banner, the visit-note composer.
- [`reviews-records.md`](../../contracts/domains/reviews-records.md) — **the b14 contract you consume.**
  Read it end-to-end for exact request/response shapes, routes, status codes, the `review_status` enum
  (`pending_moderation`/`published`/`hidden`/`rejected`), the care-record tab/section shape, and which
  clinical fields are masked vs. full. Derive your `types.ts` from this, not from guesses.
- [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) +
  [`money-and-types.md`](../../contracts/conventions/money-and-types.md) — envelope, `snake_case`
  routes/JSON, pagination (`page`/`page_size`, `items`+`total`), enums-as-codes (mirror as string-literal
  unions, **never** hardcode a label off a code), UTC + **Shamsi display is a client concern**, and the
  **PII/sensitive-field** rule (clinical notes are encrypted-at-rest, returned only to authorized callers,
  sometimes masked — the two-stage clinical-disclosure rule applies).
- [`../../../product/business/11-reviews-trust-and-safety.md`](../../../product/business/11-reviews-trust-and-safety.md)
  — the business rules: one review per completed booking, `pending_moderation` default, recompute-on-every-
  transition (a server concern, but it means a hidden review must *vanish* from the profile — your cache
  must invalidate), low-rating → support alert (server-side; you just render the "under review" state).
- [`../../../product/data-model/10-reviews-and-records.md`](../../../product/data-model/10-reviews-and-records.md)
  — `reviews` (1:1 booking, rating 1–5 CHECK, body, moderation status), `review_tags_master`/
  `review_tag_links` (the tag vocabulary), `patient_care_records` (nurse-authored, **patient-scoped not
  booking-scoped**, encrypted, strict access: owning customer + nurse with a confirmed booking for that
  patient + admin).
- [`../../../product/wireframes/index.html`](../../../product/wireframes/index.html) — **Section E** (E1
  patients list, **E2 patient record** with the four tabs + ownership banner "این پرونده متعلق به خانواده
  است …", **E3 visit note** in the terracotta "نمای پرستار" frame: EVV banner [already built] + today's
  task checklist [give متفورمین, measure blood pressure, short walk] + free-text visit-note field) and
  **Section C** (C3 nurse profile with the latest-review snippet you turn into a tab).
- The existing `client/src/services/auth/*` — the exact `services/{domain}` shape to copy, and the
  booking + patients services from f8 / f2 you will reuse.

## 3. Scope — build this

Two new `services/{domain}` domains (`reviews`, `patientRecords`), their hooks, and four wireframe
screens (+ one tab added to an existing screen). Every screen is RTL-first, `fa` default, both locales in
sync, colours from tokens, MUI v9 primitives reused, query-cached with deliberate keys and
invalidate-on-mutation. **Invoke the `frontend-designer` skill for each screen.**

### 3.1 `services/reviews` domain
Copy the `auth` service shape. Consume the b14 contract.
- `types.ts` — `Review` (`id`, `booking_id`, `nurse_id`, `customer_display_name`, `rating` 1–5,
  `body`, `status: 'pending_moderation' | 'published' | 'hidden' | 'rejected'`, `tag_codes: string[]`,
  `created_at`), `NurseReviewsResponse` (`items`, `total`, `aggregate_rating`, `review_count`),
  `ReviewTag` (`code`, plus the i18n key is the client's, **not** a label off the wire),
  `CreateReviewRequest` (`booking_id`, `rating`, `body`, `tag_codes`), `ReviewEligibility`
  (`can_review: boolean`, `reason?`). Mirror the **exact** wire shape/casing from `swagger.json`.
- `keys.ts` — a query-key factory: `reviews.nurse(nurseId, page)`, `reviews.eligibility(bookingId)`,
  `reviews.myReviewForBooking(bookingId)`.
- `apis/clientApi.ts` — wrap `clientFetch`: `getNurseReviews(nurseId, page)`
  (`GET .../get_nurse_reviews`, **published only** — the server already filters; never request or render
  other statuses publicly), `getReviewEligibility(bookingId)`, `createReview(body)`
  (`POST .../create_review`). A `serverApi.ts` only if the nurse-profile reviews tab is prefetched in the
  RSC (prefer it — removes a client round-trip on C3).
- `hooks/` (one per file): `useNurseReviews` (`useInfiniteQuery` or paged `useQuery` with `select` for
  the aggregate slice), `useReviewEligibility`, `useCreateReview` (`useMutation` → on success
  `invalidateQueries` for `reviews.eligibility(bookingId)` **and** `reviews.myReviewForBooking(bookingId)`
  so the booking-detail CTA flips to the "under review" state immediately; do **not** optimistically push
  the review into the public nurse list — it is `pending_moderation` and must not appear publicly).
- `index.ts` barrel.

### 3.2 `services/patientRecords` domain
Same shape. Consume the b14 contract. **Patient-scoped**, not booking-scoped.
- `types.ts` — `CareRecordTab = 'medications' | 'routine' | 'history' | 'tasks'`; `Medication`
  (`name`, `frequency`, `timing_note`), `RoutineItem`, `HistoryEntry`, `CareTask` (`label`, `done`),
  `VisitNote` (`id`, `booking_id`, `nurse_display_name`, `body`, `task_results`, `created_at` — **read-
  only/append-only** from the client's perspective), `PatientCareRecord` (the family-owned editable
  record: medications/routine/tasks the customer maintains), `RecordAccess`
  (`can_view`, `can_edit`, `can_append_note`, `denied_reason?`), `CreateVisitNoteRequest`
  (`booking_id`, `body`, `task_results`). Clinical fields are **sensitive** — treat masked/full per the
  contract; never log them.
- `keys.ts` — `records.patient(patientId)`, `records.history(patientId, page)`,
  `records.access(patientId)`.
- `apis/clientApi.ts` — `getPatientCareRecord(patientId)` (`GET .../get_patient_care_record`, the
  four-tab payload), `getPatientHistory(patientId, page)` (longitudinal visit-note history, paged),
  `updateCareRecord(patientId, body)` (customer edits — medications/routine/tasks),
  `createVisitNote(patientId, body)` (**nurse append** — `POST .../create_visit_note`). The access check
  rides on the read responses (403 from the envelope → render access-denied, don't crash).
- `hooks/` — `usePatientCareRecord`, `usePatientHistory` (paged/infinite), `useUpdateCareRecord`
  (customer mutation → invalidate `records.patient`), `useCreateVisitNote` (nurse mutation → invalidate
  `records.history` and `records.patient`; the nurse **cannot** call `updateCareRecord` — don't even wire
  that hook into the nurse view).
- `index.ts` barrel.

### 3.3 Screens & flows

**(a) Leave-a-review flow** (customer; entry from completed booking detail / completed-bookings list)
- A `<LeaveReviewSheet>` (or page) reached only when `useReviewEligibility(bookingId).can_review` is true
  **and** the booking status is completed/closed. Contains: a **1–5 star input** (a new shared
  `<RatingInput>` composite — see §3.4), a multiline **body** field, and **tag chips** (multi-select from
  the contract's tag vocabulary; chip labels are i18n keys keyed off `tag_codes`, never off the wire).
  Primary action "ثبت نظر".
- States: **not-eligible** → CTA hidden/disabled with a clear reason ("نظر فقط برای ویزیت‌های تکمیل‌شده
  امکان‌پذیر است"); **eligible** → the form; **submitting**; **submitted** → an **"در حال بررسی" / "under
  review"** banner (the review is `pending_moderation`, not yet public) and the CTA becomes a passive
  "نظر شما ثبت شد و در حال بررسی است"; **already-reviewed** (1:1) → show the existing pending/published
  state, never a second form; **error** → domain 4xx message, preserve the draft.

**(b) Nurse public-profile reviews tab** (customer; on the existing C3 nurse profile)
- Add a **reviews tab** to the existing nurse-profile screen. Render **only `published`** reviews via
  `useNurseReviews`, with the **aggregate rating + review count** header, paginated/infinite list, each
  row: stars, body, tag chips, masked customer display name, Shamsi date. States: **loading** (skeleton),
  **empty** ("هنوز نظری ثبت نشده"), **error**. Never render `pending_moderation`/`hidden`/`rejected` — if a
  review is hidden server-side, the next fetch simply omits it (the aggregate recompute is the server's
  job; the client just trusts the published list and its `aggregate_rating`).

**(c) Patient record viewer E2** (customer; reached from a patient in the Patients tab / E1)
- Header (reuse the f2 patient header: name, age/gender, condition chips, an **ویرایش/edit** affordance)
  + a **tabbed** body: **داروها (Medications)** [default], **روتین (Routine)**, **سوابق (History)**,
  **وظایف (Tasks)**. Medication cards (drug, frequency, timing/notes). The **سوابق** tab shows the
  longitudinal visit-note history (§(e)). A persistent **ownership banner**: "این پرونده متعلق به خانواده
  است" (the record belongs to the family). Customer can edit medications/routine/tasks
  (`useUpdateCareRecord`); the **سوابق** (nurse visit notes) are read-only to everyone.
- States: **loading** (skeleton per tab), **empty** per tab ("دارویی ثبت نشده" / "یادداشتی ثبت نشده"),
  **access-denied** (403 → a clear, non-leaking "شما به این پرونده دسترسی ندارید" card — never show
  partial clinical data), **error**.

**(d) Nurse visit-NOTE authoring E3 — the note + task-checklist part only** (nurse; terracotta
"نمای پرستار" frame, on the booking-visit screen f8 built)
- **Below the EVV check-in/out banner f8 already renders**, add: **today's task checklist** (from the
  patient's care tasks — render each `CareTask` as a checkbox the nurse ticks: give متفورمین, measure
  blood pressure, short walk) and a **free-text visit-note field**. Primary action "ثبت یادداشت"
  (`useCreateVisitNote`). The nurse view is **append-only**: it must **never** expose the customer's
  edit affordances (no medication/routine/tasks editing, no `updateCareRecord` hook wired) — the nurse
  can read prior history for continuity and append a note, nothing more.
- States: **append form** (default), **submitting**, **saved** (note appended → it appears in the
  longitudinal history), **error** (preserve draft). If the nurse lacks a confirmed booking for that
  patient (`access.can_append_note === false`), hide the composer.

**(e) Longitudinal patient history** (customer in the سوابق tab + nurse for continuity)
- A patient-scoped, paginated visit-note timeline (`usePatientHistory`): each entry = nurse display name,
  Shamsi date, note body, completed-task summary — ordered newest-first. It **persists across nurse
  changes** (patient-scoped, so a new nurse reads it before/at the visit). Read-only. States:
  loading/empty/error.

> **(DEFERRED)** — do **not** build in this phase: review *moderation* UI (the admin approve/hide/reject
> queue → [`frontend-phase-15-b15.md`](./frontend-phase-15-b15.md)); two-way (nurse-reviews-customer)
> reviews; structured tag *aggregation* dashboards ("% punctual") — render the tag chips, but the
> aggregate analytics are deferred per the product doc; the in-app "raise a concern" flag and emergency
> banner → [`frontend-phase-14-b15.md`](./frontend-phase-14-b15.md). Tag those entry points with a pointer
> if a placeholder is unavoidable; otherwise leave them out.

## 4. Mocks & seams in this phase

- **Reuse the `services/{domain}` seam pattern** from [`frontend-phase-0.md`](./frontend-phase-0.md): all
  data goes through `clientFetch`/`serverFetch` in `services/reviews` and `services/patientRecords`. No
  raw `fetch()`.
- If the **b14 contract** [`reviews-records.md`](../../contracts/domains/reviews-records.md) (or the
  `swagger.json` snapshot) is **not yet published** when you run, build a **mock `clientApi`** behind the
  same domain seam returning real-shaped fixtures (a completed-booking eligibility, a small published-
  review list with an aggregate, a four-tab care record, an append-able history) — selected by config,
  never an `if (mock)` in a component — and:
  1. append the missing/uncertain shapes to
     [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
     (per operating-rules §6), and
  2. record the mock in your phase report + the
     [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) so it swaps out cleanly
     once the real endpoint lands.
- No third-party client seam is introduced here (AI moderation `IReviewModerationService` and field
  encryption `IFieldEncryptor` are **server-side** b14 concerns — the client never sees plaintext-vs-
  ciphertext, only the authorized/masked payload).

## 5. Critical rules you must not get wrong

- **Review eligibility is gated on a completed/closed booking.** The "Leave a review" CTA is enabled
  **only** when the booking status is completed/closed (from f8's booking enum) **and** the server says
  `can_review`. Never offer a review for a cancelled/expired/other-customer booking, and enforce **one
  review per booking** (1:1) — if a review already exists, show its state, never a second form.
- **Never render `pending_moderation` (or `hidden`/`rejected`) content publicly.** The nurse-profile
  reviews tab requests and renders **published only**. After a user submits, show an **"under review"**
  state locally — do **not** optimistically inject the new review into any public list or aggregate.
  Trust the server's published list + `aggregate_rating`; when a review is hidden server-side, invalidate
  and re-fetch rather than mutating the count yourself.
- **The patient care record is FAMILY-OWNED and PATIENT-scoped.** The customer owns and edits it
  (medications/routine/tasks); the record persists **across nurse changes** because it is keyed to the
  **patient, not the booking**. Render the ownership banner "این پرونده متعلق به خانواده است" on E2.
- **The nurse can ONLY append a visit note — never edit the record.** The nurse view exposes the task
  checklist + a note composer and the read-only history; it must **not** wire `updateCareRecord` or any
  medication/routine/task editing. Append-only is a hard boundary, not just a hidden button.
- **Strict access; surface access-denied clearly.** Only the owning customer, a nurse with a **confirmed**
  booking for that patient, and admin may view a record. A `403` from the envelope → render a clear,
  non-leaking access-denied card (no partial clinical data), never a crash or a blank tab.
- **Clinical fields are sensitive.** Treat masked vs. full strictly per the contract (two-stage clinical
  disclosure spirit); never log clinical text, never persist it to `localStorage`, never put it in a
  query string.
- **RSC/client boundary, caching, re-renders, i18n, RTL, tokens.** No layout above `[locale]`; no
  `next/headers`/`next-intl/server` in client components. Set `queryKey`/`staleTime` deliberately and
  **invalidate on every mutation** (review create → eligibility/my-review; note append → history+record;
  record edit → record) so nothing over-fetches. Use `select` for the aggregate/tab slices to avoid
  needless re-renders. Every string is a key in **both** `en.json` and `fa.json`; `fa` default & RTL;
  colours from `tokens.css` (terracotta accent for the nurse E3 frame via tokens, never hardcoded);
  MUI v9 primitives reused; Shamsi date display is the client's job.

## 6. Definition of Done

The shared [definition-of-done.md](../_shared/definition-of-done.md), plus:
- [ ] `services/reviews` and `services/patientRecords` exist with the f0 shape (`types`/`keys`/`apis`/
      `hooks`/`index`); types are derived from the published b14 contract (or mocked behind the seam with a
      `for-backend.md` entry), never guessed.
- [ ] The leave-a-review flow enforces completed-booking eligibility + 1:1, shows the **under-review**
      state on submit, and never injects unmoderated content into a public list.
- [ ] The nurse-profile **reviews tab** renders published-only with aggregate rating + count, paginated,
      with loading/empty/error states.
- [ ] The **E2 patient record viewer** renders the four tabs (داروها/روتین/سوابق/وظایف), the ownership
      banner, customer edit of medications/routine/tasks, and a clear **access-denied** state on 403.
- [ ] The **nurse E3 visit-note** authoring (task checklist + note composer) is **append-only**, sits
      below the f8 EVV banner, exposes no record-editing affordances, and the appended note appears in the
      longitudinal history.
- [ ] The **longitudinal history** is patient-scoped, paginated, read-only, newest-first, and persists
      across nurse changes.
- [ ] New shared composites (`<RatingInput>`, the tag-chip selector, the tabbed record viewer if reused)
      live at the right shared level with co-located `*.test.tsx`; `npm run check` and (if a shared
      component changed) `npm run test:ci` are green; `en.json`/`fa.json` in sync (`reviews` + `records`
      namespaces).
- [ ] `client/CLAUDE.md` *Project Structure* updated for the two new service domains + any new shared
      component/route; the `frontend-designer` skill was invoked for the visual work.

## 7. How to test (what a human can verify after this phase)

Run `npm run dev` (and have the b14 backend reachable, or the seam mock active).
1. **Leave a review on a completed booking → pending → appears after moderation.** As a customer on a
   **completed** booking, open "ثبت نظر", give 4 stars + body + a tag chip, submit → the screen shows the
   **"در حال بررسی / under review"** state and the CTA does not offer a second review. The review does
   **not** appear on the nurse's profile yet. After the admin publishes it (b14/f15 path, or flip the
   mock to `published`), it appears on the **nurse profile reviews tab** and the aggregate rating/count
   updates on next fetch. Confirm a **cancelled** booking shows no review CTA.
2. **View a patient record with tabs + ownership banner.** From the Patients tab, open a patient → E2
   shows the four tabs, medication cards under داروها, the **"این پرونده متعلق به خانواده است"** banner,
   and the customer can edit a medication/routine/task and see it persist (cache invalidates, no full
   reload). Visiting a patient you don't own returns the **access-denied** card, not a crash.
3. **A nurse appends a visit note (cannot edit the record).** As the assigned nurse on today's visit
   (E3, terracotta frame), below the EVV banner: tick the task checklist, write a note, "ثبت یادداشت" →
   the note saves and shows in the history. Confirm 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's continuity view
   show the full patient-scoped, newest-first visit-note timeline — including notes from a *different*
   nurse — paginated.
5. **Gate checks:** `npm run check` green; `npm run test:ci` green for the new shared components; toggling
   locale flips `dir`/strings; the reviews tab/list and the record edit show query caching + invalidation
   in React Query Devtools (no needless refetch).

## 8. Hand off & document (close the phase)

- **Docs:** update the *Project Structure* tree in [`../../../client/CLAUDE.md`](../../../client/CLAUDE.md)
  for `services/reviews`, `services/patientRecords`, and any new shared component/route; note the
  `reviews`/`records` i18n namespaces. If you discovered a business-rule detail the product docs don't
  capture (e.g. an exact masking behaviour), record it in
  [`../../../product/business/11-reviews-trust-and-safety.md`](../../../product/business/11-reviews-trust-and-safety.md)
  or [`../../../product/data-model/10-reviews-and-records.md`](../../../product/data-model/10-reviews-and-records.md)
  — don't invent rules.
- **Contract:** **consume** [`reviews-records.md`](../../contracts/domains/reviews-records.md) (b14) as
  the source of truth for every shape. The frontend does **not** write contracts — if a shape is missing,
  wrong, or unmasked when it should be masked, append a request to
  [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
  and mock behind the `services/{domain}` seam meanwhile.
- **Handoff & report:** append your phase summary to
  [`../../shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md); write
  [`../../shared-working-context/reports/frontend-phase-13-report.md`](../../shared-working-context/reports/frontend-phase-13-report.md)
  (what was built, **what is testable and exactly how** per §7, what is mocked client-side + how it swaps,
  contracts consumed, follow-ups — e.g. the deferred moderation UI for f15); update
  [`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md)
  for any client-side mock you used.
- **Memory:** save a `project`-type memory note for the non-obvious decisions this phase locks in (review
  is published-only on the client and never optimistically injected; the patient record is family-owned
  and patient-scoped with the nurse strictly append-only; access-denied is a first-class state), with a
  one-line pointer in `MEMORY.md`.
