11 KiB
Flow — care-circle-patients
Last verified: 2026-08-02 against commit
c841bde
Actor(s): customer · Status: partial Client: partial · Server: real Business source: product/business/01-actors-and-onboarding.md · data model: product/data-model/10-reviews-and-records.md Integration: docs/integration/domains/patients.md · patient-records.md
What it does
A customer keeps a list of the people they arrange care for — «حلقهٔ مراقبت», renamed from «بیماران» in ui-phase-9. Each one is a first-class row, not the account holder: the payer is usually an adult child and the care recipient an elderly parent or a newborn. Tapping a person opens their care record — the family's own medication/routine/task plan, plus the read-only history of visit notes nurses wrote for them.
The seam splits mid-flow. The list screen is real end to end and was probed live. The record screen is real UI on a mock service (
USE_PATIENT_RECORDS_MOCK = true,patientRecords/constants.ts:13) — even though all five server endpoints exist and return200. Nothing you see on/recordcomes from the database.
Screens
| Step | Route | Component / notes |
|---|---|---|
| E1 — the care circle | /fa/patients |
patients/page.tsx · PatientCard (uses InitialsAvatar) · add/edit in FormDialogShell + PatientForm · archive behind ConfirmDialog |
| — home teaser | /fa |
HomeScreen.tsx:87 calls the same usePatients() |
| E2 — care record | /fa/patients/[id]/record |
record/page.tsx · 4 tabs داروها / روتین / سوابق / وظایف · PatientHeader, VisitNoteCard, per-item bottom-sheet edit · EmptyState access-denied card gated before any clinical fetch |
| — nurse counterpart | /fa/nurse/visits/[id] |
NurseVisitNotesPanel.tsx:38-44 — same domain, append-only |
Age is derived client-side by patients/age.ts; the wire
carries birthDate.
API
| Call | Endpoint | Notes |
|---|---|---|
usePatients → patientsClientApi.list |
GET /api/v1/patients/list |
real · clientApi.ts:46 → PatientsController.List |
usePatient |
GET /patients/get/{id} |
real |
useCreatePatient / useUpdatePatient |
POST /patients/create · /update/{id} |
real · relation+conditions round-trip (REQ-005) |
useArchivePatient |
POST /patients/archive/{id} |
real · archive, never delete |
useRecordAccess |
GET /patients/{id}/record_access |
server real, client mocked |
usePatientCareRecord |
GET /patients/{id}/care_record |
server real, client mocked |
useUpdateCareRecord |
PUT /patients/{id}/care_record |
server real, client mocked — the only PUT in the API |
usePatientHistory |
GET /patients/{id}/care_records |
server real, client mocked |
useCreateVisitNote (nurse) |
POST /patients/{id}/care_records |
server real, client mocked |
Handlers: PatientsController.cs (Features/Identity/{Commands,Queries}) and
PatientCareRecordsController.cs → Features/PatientCareRecords/**. Request/response shapes live in the
integration docs linked above — not here.
Rules that must hold
| Rule | Source |
|---|---|
A patient is a first-class row distinct from the customer; self still creates its own row. Customer→patient is 1:N |
business/01 |
gender is load-bearing — it drives same-gender caregiver matching. Never defaulted or inferred |
integration/patients.md |
| Archive, never delete. A patient referenced by a booking must stay resolvable | integration/patients.md |
| The plan is family-owned; visit records are nurse-owned and append-only. No edit, no delete endpoint exists and none should | integration/patient-records.md |
| Two-stage clinical disclosure — clinical bodies encrypted at rest, decrypted only after the access check; nurse read is scoped by an active booking | INV-6, server/CLAUDE.md rule 18 |
| Tenancy mismatch is a 404, never a 403 | api-contract.md |
initialMedicalNotes is encrypted PII and is not the care record |
integration/patients.md |
How to test
-
Log in as
09120000010(سارا محمدی, customer) — see testing-setup.md. -
Open
/fa/patients. Expect: two cards — «فاطمه محمدی» and «حسن محمدی». Header reads «حلقهٔ مراقبت». Verified live:GET /patients/list?page=1&pageSize=10→total: 2. Note: both seeded rows carryrelation: nullandconditions: [], so the relation chip and the condition chips render nothing. That is seed data, not a UI bug. -
Tap «افزودن» and save a new person. Expect: the card appears immediately (the list is invalidated on mutation) and a green «ذخیره شد». A new
GET /patients/listnow returnstotal: 3— this writes to the shared remote DB. -
Tap a card's archive action and confirm. Expect: the card disappears, toast «بایگانی شد». The row is still resolvable server-side (
isActive: false), so any booking that references it keeps working. -
Tap a card body to open
/fa/patients/1/record. Expect: the ownership banner «این پرونده متعلق به خانواده است…», then four tabs. This screen is mock-served. What you see under داروها / روتین / وظایف ispatientRecords/apis/mockApi.tsfixtures, and سوابق shows mock notes — not the two real seeded care records. Every edit you make is lost on reload (the mock store is module-level). -
To see the real data the screen is hiding, call it directly:
curl --noproxy '*' "http://localhost:5002/api/v1/patients/1/care_records?page=1&pageSize=20" -H "Authorization: Bearer $T_09120000010". Expect:total: 2, two Persian notes by «زهرا عزیزی» dated 2026-07-16 and 2026-07-25 — andtaskResultsentries whoselabelisnull(see gaps). -
Access control, verified live and only observable via curl today:
Caller record_accessfor patient 1Result 09120000010(owner)canView: true, canEdit: true, canAppendNote: false✅ 09120000001(nurse 1, has bookings for patient 1)canView: true, canEdit: false, canAppendNote: true✅ 09120000011(other customer)canView: false, deniedReason: "not_authorized"✅ denies; GET care_recordfor the same caller returns403In the browser the mock's denial path is only reachable via
MOCK_FOREIGN_PATIENT_ID = 8888→/fa/patients/8888/record.
The seeded world still supports every step of this flow — it does not depend on booking-request freshness.
Known gaps
USE_PATIENT_RECORDS_MOCK = true(client/src/services/patientRecords/constants.ts:13) while all five server endpoints are live and return200. The customer's care record and the nurse's visit-note panel both render fixtures instead of the database. The single highest-value flag flip in this flow.- Flipping that flag today breaks the record screen:
getFamilyRecord(patientRecords/apis/clientApi.ts:68-69) is a bareunwrap()with no mapper, and the wire shape does not match. ServerMedicationDto(long Id, string Name, string? Dosage, string Frequency, string? TimingNote)vs clientMedication { id: string, doseAmount, doseUnit, frequencyCode, frequencyText, timeOfDay: TimeOfDayCode[], timingNote }— dose/frequency/time-of-day all render blank. - Same flip crashes the روتین tab: server
RoutineItemDto.TimeOfDayis a singlestring?; the client types itTimeOfDayCode[]and calls.map()on it (record/page.tsx:616). - Care-plan
idtype mismatch (hardening H-17, confirmed): serverlong, clientstring.updateFamilyRecordPUTs the whole record including client temp ids likenew-1754…(record/page.tsx:277), so the write is unsafe against the real endpoint. - Server defect —
taskResultslabels are lost.GetPatientHistoryQuery.Handler.cs:78deserializesTaskResultsJsonwith default (case-sensitive)JsonSerializeroptions, butDemoLifecycleSeeder.Social.cs:324seeds camelCase{"label":…,"done":…}. Live result for patient 1: three entries of{"label": null, "done": false}. Any externally-written JSON in that column silently degrades to blank labels. - Client discards a field the server does serve.
toVisitNote(patientRecords/apis/clientApi.ts:37) hardcodestaskResults: []althoughCareRecordDto.taskResultsis on the wire. The «X از Y» task-summary chip onVisitNoteCardcan never render on the real path. deniedReasonenum mismatch: the server returns"not_authorized"(PatientAccess.cs:20), whilepatientRecords/types.ts:117anddocs/integration/domains/patient-records.mdboth declareno_access | not_found. Latent today (the UI renders a generic card), a bug the moment anyone branches on it.- Stale code comments assert the opposite of reality.
patientRecords/types.ts:11-13,constants.ts:1-12andapis/clientApi.ts:62-64all say the family record and access check have "no backend" / are REQ-027 gaps. All three routes exist, are[Authorize]d, and were probed200. The integration doc is right and the code comments are wrong. patients/constants.ts:1-7still explains the domain "demos behind the mock" because REQ-005 is missing — REQ-005 is delivered and the flag is alreadyfalse.- Editing a patient destroys their birth date.
PatientForm.tsx:94always submitsageToBirthDate(age)=YYYY-01-01. Saving seeded patient 1 (birthDate: 1948-03-15) with no changes rewrites it to1948-01-01. Age display survives; the date does not. - Seeded care plans are empty —
GET /patients/1/care_recordreturns{ medications: [], routine: [], tasks: [] }, so three of the four record tabs would be blank the moment the flag flips.DemoLifecycleSeedernever seeds aPatientCarePlan. - Seeded patients have
relation: nullandconditions: [], so the relation/condition chips the UI is built around never appear on demo data. - REQ-057 open:
PatientDtohas nolastVisitAt/visitCountandBookingListItemDtohas nopatientId, so no booking card can show a care teaser. - No
product/business/file covers patient care records at all — no documented rule for the append-only constraint, the encryption, or the clinical-access gate. Onlyproduct/data-model/10-reviews-and-records.mdmentions the table. Largest product-doc hole touching this flow. - Archive is optimistic with no undo; a cross-tenant/stale id 404s and the card silently reappears with a generic «در دسترس نیست» toast (
patients/page.tsx:65-70).