create mvp path
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# 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](../../product/business/01-actors-and-onboarding.md) ·
|
||||
data model: [product/data-model/10-reviews-and-records.md](../../product/data-model/10-reviews-and-records.md)
|
||||
**Integration:** [docs/integration/domains/patients.md](../integration/domains/patients.md) ·
|
||||
[patient-records.md](../integration/domains/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`](../../client/src/services/patientRecords/constants.ts)) — even though
|
||||
> all five server endpoints exist and return `200`. Nothing you see on `/record` comes from the database.
|
||||
|
||||
## Screens
|
||||
|
||||
| Step | Route | Component / notes |
|
||||
| --- | --- | --- |
|
||||
| E1 — the care circle | `/fa/patients` | [`patients/page.tsx`](../../client/src/app/%5Blocale%5D/(private-routes)/(customer)/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`](../../client/src/app/%5Blocale%5D/(private-routes)/(customer)/patients/%5Bid%5D/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`](../../client/src/services/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](../../product/business/01-actors-and-onboarding.md) |
|
||||
| **`gender` is load-bearing** — it drives same-gender caregiver matching. Never defaulted or inferred | [integration/patients.md](../integration/domains/patients.md) |
|
||||
| **Archive, never delete.** A patient referenced by a booking must stay resolvable | [integration/patients.md](../integration/domains/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](../integration/domains/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](../integration/api-contract.md#status-codes) |
|
||||
| `initialMedicalNotes` is encrypted PII and is **not** the care record | [integration/patients.md](../integration/domains/patients.md) |
|
||||
|
||||
## How to test
|
||||
|
||||
1. Log in as `09120000010` (سارا محمدی, customer) — see [testing-setup.md](testing-setup.md).
|
||||
2. Open `/fa/patients`.
|
||||
**Expect:** two cards — «فاطمه محمدی» and «حسن محمدی». Header reads «حلقهٔ مراقبت». Verified live:
|
||||
`GET /patients/list?page=1&pageSize=10` → `total: 2`.
|
||||
**Note:** both seeded rows carry `relation: null` and `conditions: []`, so the relation chip and the
|
||||
condition chips render nothing. That is seed data, not a UI bug.
|
||||
3. Tap «افزودن» and save a new person.
|
||||
**Expect:** the card appears immediately (the list is invalidated on mutation) and a green «ذخیره شد».
|
||||
A new `GET /patients/list` now returns `total: 3` — this writes to the **shared remote DB**.
|
||||
4. 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.
|
||||
5. 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 داروها / روتین / وظایف is
|
||||
`patientRecords/apis/mockApi.ts` fixtures, 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).
|
||||
6. 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 — and
|
||||
`taskResults` entries whose `label` is `null` (see gaps).
|
||||
7. Access control, verified live and **only** observable via curl today:
|
||||
|
||||
| Caller | `record_access` for patient 1 | Result |
|
||||
| --- | --- | --- |
|
||||
| `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_record` for the same caller returns `403` |
|
||||
|
||||
In 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 return `200`. 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 bare `unwrap()` with no mapper, and the wire shape does not match. Server `MedicationDto(long Id, string Name, string? Dosage, string Frequency, string? TimingNote)` vs client `Medication { id: string, doseAmount, doseUnit, frequencyCode, frequencyText, timeOfDay: TimeOfDayCode[], timingNote }` — dose/frequency/time-of-day all render blank.
|
||||
- Same flip **crashes the روتین tab**: server `RoutineItemDto.TimeOfDay` is a single `string?`; the client types it `TimeOfDayCode[]` and calls `.map()` on it (`record/page.tsx:616`).
|
||||
- Care-plan `id` type mismatch (hardening H-17, confirmed): server `long`, client `string`. `updateFamilyRecord` `PUT`s the whole record including client temp ids like `new-1754…` (`record/page.tsx:277`), so the write is unsafe against the real endpoint.
|
||||
- **Server defect — `taskResults` labels are lost.** `GetPatientHistoryQuery.Handler.cs:78` deserializes `TaskResultsJson` with default (case-**sensitive**) `JsonSerializer` options, but `DemoLifecycleSeeder.Social.cs:324` seeds 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`) hardcodes `taskResults: []` although `CareRecordDto.taskResults` is on the wire. The «X از Y» task-summary chip on `VisitNoteCard` can never render on the real path.
|
||||
- `deniedReason` enum mismatch: the server returns `"not_authorized"` (`PatientAccess.cs:20`), while `patientRecords/types.ts:117` and `docs/integration/domains/patient-records.md` both declare `no_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-12` and `apis/clientApi.ts:62-64` all say the family record and access check have "**no backend**" / are REQ-027 gaps. All three routes exist, are `[Authorize]`d, and were probed `200`. The integration doc is right and the code comments are wrong.
|
||||
- `patients/constants.ts:1-7` still explains the domain "demos behind the mock" because REQ-005 is missing — REQ-005 is delivered and the flag is already `false`.
|
||||
- **Editing a patient destroys their birth date.** `PatientForm.tsx:94` always submits `ageToBirthDate(age)` = `YYYY-01-01`. Saving seeded patient 1 (`birthDate: 1948-03-15`) with no changes rewrites it to `1948-01-01`. Age display survives; the date does not.
|
||||
- Seeded care plans are **empty** — `GET /patients/1/care_record` returns `{ medications: [], routine: [], tasks: [] }`, so three of the four record tabs would be blank the moment the flag flips. `DemoLifecycleSeeder` never seeds a `PatientCarePlan`.
|
||||
- Seeded patients have `relation: null` and `conditions: []`, so the relation/condition chips the UI is built around never appear on demo data.
|
||||
- REQ-057 open: `PatientDto` has no `lastVisitAt`/`visitCount` and `BookingListItemDto` has no `patientId`, 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. Only `product/data-model/10-reviews-and-records.md` mentions 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`).
|
||||
Reference in New Issue
Block a user