11 KiB
Frontend Phase 13 (b14) — Reviews & Patient Care Records — report
Track: frontend · Consumes: 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— addedusePatient(id)(E2 identity header; the seam already hadget(id)).
Screens & flows
- Leave-a-review —
/bookings/[id]/review(RatingInput+ body +ReviewTagSelector), gated on completed/closed + servercan_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:LeaveReviewCtaon 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: reusedPatientHeader+ 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 aPatientCardopens it. - Nurse visit-note authoring (E3) —
NurseVisitNotesPanel(co-located atnurse/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 wiresuseUpdateCareRecord. - Longitudinal history — patient-scoped, paged (Prev/Next when >1 page), read-only, newest-first, rendered
via the shared
VisitNoteCardin 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 registeredstarAppIcon; filledvar(--bal-warning)/ emptyvar(--bal-divider); interactive = radiogroup, readOnly = static).ReviewTagSelector— multi-select review-tag chip group (i18n-free; caller passeslabelFor(code)).VisitNoteCard— one read-only visit note (nurse name + Shamsi date + body + done/not-done task chips).PatientHeader— extracted fromPatientCardso E1 + E2 share the identity header;PatientCardnow composes it + gained an optionalonOpentap handler.- New icons registered:
notes,routine,tasks,history,family. - i18n: new top-level
reviews+recordsnamespaces (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).
- 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 fromservices/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). - 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(theMOCK_FOREIGN_PATIENT_ID) shows the access-denied card, not a crash. - 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. - 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.
- Gate checks:
npm run checkgreen;npm run test:cigreen (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 for the full rows.
- Reviews:
reviewsClientApi.getNurseReviews/createReviewmap 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 = flipUSE_REVIEWS_MOCKonce REQ-026 lands. - Patient records:
patientRecordsClientApi.getPatientHistory/createVisitNotemap the real b14care_recordsGET/POST 1:1; the mock adds the family-owned record + access check (REQ-027, no backend entity exists). Swap = flipUSE_PATIENT_RECORDS_MOCKonce 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 helpermockGetBookingForReview(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:- 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 + arecord_accessread (or derive from the 403) + structuredtaskResultson a visit note. Flags that the wireframe's four-tab E2 record has no data-model entity — needs a product decision.
- REQ-026 — review-eligibility read + my-review-for-booking read + confirm the masked-author omission on
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.mjstries to raise@typescript-eslint/no-unused-varstoerrorby.map()-patchingeslint-config-next's default export — but that export never carries the rule (it lives in theeslint-config-next/typescriptsubpath, which the config doesn't spread), so the patch matches nothing and the rule is never enabled (verified viaeslint --print-config— zero active@typescript-eslintrules).tsconfig.jsonalso lacksnoUnusedLocals. Net effect: unused imports/vars passnpm run checksilently, contradictingclient/CLAUDE.mdgolden 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 viatsc --noEmit --noUnusedLocals --noUnusedParameters. The fix (spreadeslint-config-next/typescript, or re-register the rule in an explicit**/*.{ts,tsx}override, + fix the false CLAUDE.md/config comments, + optionally addnoUnusedLocals) 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_tagsendpoint 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_SIZEin the E2 page,mockListBookingsForReviewin the reviews mock — the latter's orphaned bookings-mock export also removed); the now-inaccurate history-only invalidation doc comment; the orphanedsearchi18n 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:1409); the security/access dimension raised nothing (published-only, append-only, and the non-leaking access gate all hold); the caching dimension raised nothing.