- Add IbuHamil and Balita models to schema.prisma - Implement IbuHamil and Balita API modules (CRUD) - Implement /stats endpoint for aggregated health KPIs - Refactor ringkasan-kesehatan admin page to dashboard-style UI - Update sidebar with Ibu Hamil and Balita entries - Fix type errors and icon exports in admin UI - Bump version to 0.1.52
247 lines
8.1 KiB
Markdown
247 lines
8.1 KiB
Markdown
# Task: Refactor Ringkasan Kesehatan → Dashboard Auto-Derived
|
|
|
|
## Latar Belakang
|
|
|
|
Page `/admin/kesehatan/ringkasan-kesehatan` saat ini = form `NumberInput`
|
|
manual (7 angka diketik operator). Itu **salah konsep**: "Ringkasan" harus
|
|
**hasil agregasi** dari data sumber, bukan input bebas.
|
|
|
|
Solusi: bikin 2 entitas data sumber (`IbuHamil`, `Balita`), lalu derive
|
|
KPI + statistik kesehatan dari query agregat. Page ringkasan jadi
|
|
**dashboard read-only** + tombol "Kelola Ibu Hamil" / "Kelola Balita".
|
|
|
|
## Mapping Field → Sumber Data
|
|
|
|
| Field di Ringkasan | Sumber |
|
|
|---|---|
|
|
| `ibuHamilAktif` | `count(IbuHamil where status=AKTIF, isActive=true)` |
|
|
| `balitaTerdaftar` | `count(Balita where isActive=true)` |
|
|
| `alertStunting` | `count(Balita where statusStunting IN [ALERT, STUNTING])` |
|
|
| `imunisasiLengkapPct` | `count(imunisasiLengkap=true) / total balita * 100` |
|
|
| `pemeriksaanRutinPct` | `count(pemeriksaanRutin=true) / total balita * 100` |
|
|
| `giziBaikPct` | `count(giziBaik=true) / total balita * 100` |
|
|
| `targetStuntingPct` | tetap di `RingkasanKesehatanDesa` (policy target, bukan derived) |
|
|
|
|
## Kontrak Lama (jaga, AI-CONTRACT §10)
|
|
|
|
- `GET /api/kesehatan/ringkasankesehatan/find` — tetap return shape lama
|
|
- `PUT /api/kesehatan/ringkasankesehatan/update` — tetap menerima 7 field lama
|
|
- Tambah baru: `GET /api/kesehatan/ringkasankesehatan/stats` (agregat)
|
|
- Tambah baru: `/api/kesehatan/ibuhamil/*`, `/api/kesehatan/balita/*`
|
|
|
|
> Alasan: ada konsumer eksternal (mobile/landing) yang mungkin sudah
|
|
> baca shape lama. Refactor admin tidak boleh memutus kontrak existing.
|
|
> Field manual lama akan ditandai deprecated di komentar schema saat
|
|
> tidak lagi dipakai admin UI.
|
|
|
|
---
|
|
|
|
## Step A — Model `IbuHamil`
|
|
|
|
### Schema
|
|
|
|
```prisma
|
|
enum IbuHamilStatus {
|
|
AKTIF
|
|
MELAHIRKAN
|
|
KEGUGURAN
|
|
NONAKTIF
|
|
}
|
|
|
|
model IbuHamil {
|
|
id String @id @default(cuid())
|
|
nama String
|
|
nik String?
|
|
usiaKehamilan Int @default(0) // minggu
|
|
hpht DateTime? // hari pertama haid terakhir
|
|
taksiranLahir DateTime?
|
|
alamat String?
|
|
noHp String?
|
|
catatan String?
|
|
posyanduId String?
|
|
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
|
status IbuHamilStatus @default(AKTIF)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
isActive Boolean @default(true)
|
|
}
|
|
```
|
|
|
|
### API `src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/`
|
|
|
|
Pattern mirror `posyandu/`:
|
|
- `index.ts` — Elysia routes (prefix `/ibuhamil`)
|
|
- `create.ts` — POST `/create`
|
|
- `find-many.ts` — GET `/find-many?page=&limit=&search=&status=`
|
|
- `find-by-id.ts` — GET `/:id`
|
|
- `updt.ts` — PUT `/:id`
|
|
- `del.ts` — DELETE `/del/:id` (soft delete `isActive=false`)
|
|
|
|
Daftarkan di `kesehatan/index.ts`: `.use(IbuHamil)`
|
|
|
|
### Checklist
|
|
|
|
- [ ] 1. Tambah enum `IbuHamilStatus` + model `IbuHamil` ke `prisma/schema.prisma`
|
|
- [ ] 2. Tambah `@relation` reverse di model `Posyandu` (`ibuHamil IbuHamil[]`)
|
|
- [ ] 3. Generate migration `add_ibu_hamil` (manual SQL bila sandbox)
|
|
- [ ] 4. Buat 6 file API (index/create/find-many/find-by-id/updt/del)
|
|
- [ ] 5. Register di `kesehatan/index.ts`
|
|
- [ ] 6. State file `src/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil.ts`
|
|
pattern Valtio + zod (mirror `posyandu/posyandu.ts`)
|
|
- [ ] 7. Admin pages:
|
|
- `src/app/admin/(dashboard)/kesehatan/ibu-hamil/page.tsx` (list)
|
|
- `src/app/admin/(dashboard)/kesehatan/ibu-hamil/create/page.tsx`
|
|
- `src/app/admin/(dashboard)/kesehatan/ibu-hamil/edit/[id]/page.tsx`
|
|
- [ ] 8. Sidebar entry: tambah "Ibu Hamil" di group Kesehatan
|
|
- [ ] 9. `bun run build` exit 0
|
|
- [ ] 10. Bump version + commit + push
|
|
|
|
---
|
|
|
|
## Step B — Model `Balita`
|
|
|
|
### Schema
|
|
|
|
```prisma
|
|
enum JenisKelaminBalita {
|
|
L
|
|
P
|
|
}
|
|
|
|
enum StatusStunting {
|
|
NORMAL
|
|
ALERT
|
|
STUNTING
|
|
}
|
|
|
|
model Balita {
|
|
id String @id @default(cuid())
|
|
nama String
|
|
nik String?
|
|
tanggalLahir DateTime
|
|
jenisKelamin JenisKelaminBalita
|
|
beratBadanKg Float?
|
|
tinggiBadanCm Float?
|
|
namaOrtu String?
|
|
alamat String?
|
|
noHpOrtu String?
|
|
posyanduId String?
|
|
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
|
imunisasiLengkap Boolean @default(false)
|
|
giziBaik Boolean @default(true)
|
|
pemeriksaanRutin Boolean @default(true)
|
|
statusStunting StatusStunting @default(NORMAL)
|
|
catatan String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
isActive Boolean @default(true)
|
|
}
|
|
```
|
|
|
|
### API `src/app/api/[[...slugs]]/_lib/kesehatan/balita/`
|
|
|
|
Sama pattern dengan ibu-hamil (6 file). Body validator Elysia:
|
|
|
|
```ts
|
|
t.Object({
|
|
nama: t.String(),
|
|
nik: t.Optional(t.String()),
|
|
tanggalLahir: t.String(), // ISO string
|
|
jenisKelamin: t.Union([t.Literal('L'), t.Literal('P')]),
|
|
beratBadanKg: t.Optional(t.Number()),
|
|
tinggiBadanCm: t.Optional(t.Number()),
|
|
namaOrtu: t.Optional(t.String()),
|
|
alamat: t.Optional(t.String()),
|
|
noHpOrtu: t.Optional(t.String()),
|
|
posyanduId: t.Optional(t.String()),
|
|
imunisasiLengkap: t.Boolean(),
|
|
giziBaik: t.Boolean(),
|
|
pemeriksaanRutin: t.Boolean(),
|
|
statusStunting: t.Union([t.Literal('NORMAL'), t.Literal('ALERT'), t.Literal('STUNTING')]),
|
|
catatan: t.Optional(t.String()),
|
|
})
|
|
```
|
|
|
|
### Checklist
|
|
|
|
- [ ] 1. Tambah enum + model `Balita` ke schema
|
|
- [ ] 2. `@relation` reverse di Posyandu (`balita Balita[]`)
|
|
- [ ] 3. Migration `add_balita`
|
|
- [ ] 4. 6 file API
|
|
- [ ] 5. Register di `kesehatan/index.ts`
|
|
- [ ] 6. State file `_state/kesehatan/balita/balita.ts`
|
|
- [ ] 7. Admin pages list / create / edit (form dengan 4 toggle:
|
|
`imunisasiLengkap`, `giziBaik`, `pemeriksaanRutin` + Select
|
|
`statusStunting`)
|
|
- [ ] 8. Sidebar entry "Balita" di group Kesehatan
|
|
- [ ] 9. `bun run build` exit 0
|
|
- [ ] 10. Bump version + commit + push
|
|
|
|
---
|
|
|
|
## Step C — Endpoint Stats Agregat
|
|
|
|
### File baru `src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/stats.ts`
|
|
|
|
Return:
|
|
|
|
```ts
|
|
{
|
|
ibuHamilAktif: number,
|
|
balitaTerdaftar: number,
|
|
alertStunting: number,
|
|
imunisasiLengkapPct: number, // round
|
|
pemeriksaanRutinPct: number,
|
|
giziBaikPct: number,
|
|
targetStuntingPct: number, // dari RingkasanKesehatanDesa
|
|
}
|
|
```
|
|
|
|
Implementasi (Prisma):
|
|
- 1 `count` IbuHamil + 1 `groupBy` Balita (status flags)
|
|
- Hitung pct di JS: `Math.round((n / total) * 100)`
|
|
- Edge case `total=0` → semua pct = 0
|
|
|
|
### Checklist
|
|
|
|
- [ ] 1. Bikin `stats.ts` handler
|
|
- [ ] 2. Daftarkan route `GET /stats` di `ringkasan-kesehatan/index.ts`
|
|
- [ ] 3. Update state `ringkasanKesehatan.ts`:
|
|
- tambah `findStats: { data, loading, load() }`
|
|
- method lama `findUnique` + `update` → tetap (kontrak)
|
|
- [ ] 4. Refactor `page.tsx` ringkasan:
|
|
- section atas: 7 stat card read-only dari `findStats.data`
|
|
- section bawah: form kecil hanya `targetStuntingPct` (policy target)
|
|
- 2 tombol: "Kelola Ibu Hamil" → push `/admin/kesehatan/ibu-hamil`
|
|
"Kelola Balita" → push `/admin/kesehatan/balita`
|
|
- [ ] 5. `bun run build` exit 0
|
|
- [ ] 6. Bump version + commit + push
|
|
|
|
---
|
|
|
|
## Step D — Cleanup (opsional, tunggu user konfirmasi)
|
|
|
|
Field manual `ibuHamilAkh`, `balitaTerdaftar`, `alertStunting`,
|
|
`imunisasiLengkapPct`, `pemeriksaanRutinPct`, `giziBaikPct` di
|
|
`RingkasanKesehatanDesa` jadi **deprecated**.
|
|
|
|
- Opsi 1 (aman): tinggalkan, tandai `// DEPRECATED — derived from /stats`
|
|
- Opsi 2 (bersih): hapus + bump major version + update kontrak konsumer
|
|
|
|
**Tunggu input user** — jangan eksekusi tanpa izin.
|
|
|
|
---
|
|
|
|
## Pending Manual
|
|
|
|
- [ ] User jalankan `bunx prisma migrate deploy` di lokal setelah Step A & B
|
|
- [ ] User trigger publish + re-pull workflow bila mau deploy STG
|
|
|
|
## Prinsip
|
|
|
|
- **Additive**: kontrak `/find` + `/update` ringkasankesehatan tetap
|
|
- **Single source of truth**: KPI = derived dari IbuHamil + Balita
|
|
- **Soft delete** pakai `isActive=false`, jangan hard delete
|
|
- **YAGNI**: belum bikin chart/grafik tren; cukup angka snapshot
|
|
- **No breaking change**: konsumer landing/mobile aman selama migrasi
|