feat: add health data management workspace
This commit is contained in:
@@ -180,3 +180,93 @@ if (!floor) {
|
||||
}
|
||||
await database.insert(rooms).values({ organizationId, floorId: floor.id, name, code });
|
||||
```
|
||||
|
||||
## Scenario: Health Admin Data Management
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: adding or changing health profile, vital record, chronic condition, or health anomaly review behavior.
|
||||
- Applies because health data is persisted in Drizzle tables and rendered through a settings management page, not fabricated in the operational `/app/health` placeholder.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- `GET /api/health/admin`
|
||||
- `PUT /api/health/profiles/[elderId]`
|
||||
- `POST /api/health/vitals`
|
||||
- `POST /api/health/chronic-conditions`
|
||||
- `PATCH /api/health/reviews/[id]`
|
||||
- `listHealthAdminData(organizationId: string): Promise<HealthAdminData>`
|
||||
- Mutations return either the domain DTO or `{ success: false; reason: string; status: number }`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Health APIs must call `requirePermission` before reading or mutating:
|
||||
- reads use `health:read`
|
||||
- mutations use `health:manage`
|
||||
- All health rows are scoped by `organizationId`.
|
||||
- Elder-owned health mutations must validate the elder exists in the active organization.
|
||||
- `GET /api/health/admin` returns `{ success: true; reason: string; data: HealthAdminData }`; client refresh code must read `result.data`.
|
||||
- Mutation success responses follow the standard shape:
|
||||
- profile: `{ success: true; reason: string; profile }`
|
||||
- vital: `{ success: true; reason: string; vital; review? }`
|
||||
- chronic condition: `{ success: true; reason: string; condition }`
|
||||
- review: `{ success: true; reason: string; review }`
|
||||
- Mutations must write audit logs after successful persistence.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Missing active organization -> `400` with a Chinese reason instructing the user to select an organization.
|
||||
- Missing `elderId` for vital/condition -> `400`.
|
||||
- Invalid date/source/status/numeric field -> `400`.
|
||||
- Elder not found or belongs to another organization -> `404` / `老人档案不存在`.
|
||||
- Review not found or belongs to another organization -> `404` / `异常复核记录不存在`.
|
||||
- Missing permission -> `403` from `requirePermission`; route must not call domain helpers.
|
||||
- Insert/update returning no row -> `500` mutation failure.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: keep `/app/settings/health` as the backend management surface and leave `/app/health` operational until real operational data exists.
|
||||
- Good: return the admin payload under a `data` key so route output and client refresh types match.
|
||||
- Good: seed demo health rows only inside the default workspace seeding transaction and tie them to real seeded elders.
|
||||
- Base: anomaly review rows may be created automatically from MVP vital thresholds.
|
||||
- Bad: render UI-only health records or counters on static module pages.
|
||||
- Bad: hide mutation buttons in the UI without enforcing `health:manage` in the Route Handler.
|
||||
- Bad: update reviews by `id` alone without also filtering by active `organizationId`.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- `pnpm test` with API assertions for each health route.
|
||||
- Route tests must cover at least: happy path, permission denial, missing active organization, invalid input, and missing/cross-organization IDs.
|
||||
- `pnpm db:generate` after schema changes, then review the generated SQL for only additive health enum/table/index/FK changes.
|
||||
- `pnpm lint`, `pnpm type-check`, and `pnpm build`.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```ts
|
||||
const data = await listHealthAdminData(organizationId);
|
||||
return jsonSuccess("健康数据已加载", data);
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```ts
|
||||
const data = await listHealthAdminData(organizationId);
|
||||
return jsonSuccess("健康数据已加载", { data });
|
||||
```
|
||||
|
||||
#### Wrong
|
||||
|
||||
```ts
|
||||
await database.update(healthAnomalyReviews).set({ status }).where(eq(healthAnomalyReviews.id, id));
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```ts
|
||||
await database
|
||||
.update(healthAnomalyReviews)
|
||||
.set({ status, updatedAt: new Date() })
|
||||
.where(and(eq(healthAnomalyReviews.id, id), eq(healthAnomalyReviews.organizationId, organizationId)));
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user