feat: add health data management workspace

This commit is contained in:
2026-07-03 00:25:11 -07:00
parent dc034b6d85
commit 41807ff557
53 changed files with 7367 additions and 3 deletions

View File

@@ -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)));
```