Files
2026-06-28 21:59:59 +03:30

327 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 15 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` 15,
`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 **15 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`.