feat(kesehatan): refactor ringkasan kesehatan to auto-derived stats
- 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
This commit is contained in:
@@ -1,46 +1,246 @@
|
||||
# Task: Tambah Statistik Persentase ke RingkasanKesehatanDesa
|
||||
# Task: Refactor Ringkasan Kesehatan → Dashboard Auto-Derived
|
||||
|
||||
## Tujuan
|
||||
Lengkapi schema + API `RingkasanKesehatanDesa` dengan 4 field persentase
|
||||
agar dashboard Statistik Kesehatan (Imunisasi, Pemeriksaan Rutin, Gizi Baik,
|
||||
Target Stunting) punya backend yang bisa di-fetch.
|
||||
## Latar Belakang
|
||||
|
||||
## Field Baru
|
||||
Tambah ke model `RingkasanKesehatanDesa` (additive, tidak ubah field lama):
|
||||
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.
|
||||
|
||||
| Field | Tipe | Default | Keterangan |
|
||||
|---|---|---|---|
|
||||
| `imunisasiLengkapPct` | Int | 0 | Persentase imunisasi lengkap (0-100) |
|
||||
| `pemeriksaanRutinPct` | Int | 0 | Persentase pemeriksaan rutin (0-100) |
|
||||
| `giziBaikPct` | Int | 0 | Persentase gizi baik (0-100) |
|
||||
| `targetStuntingPct` | Int | 0 | Persentase target stunting (0-100) |
|
||||
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".
|
||||
|
||||
## Step A — Schema + API
|
||||
## Mapping Field → Sumber Data
|
||||
|
||||
- [x] 1. Edit `prisma/schema.prisma` — tambah 4 field
|
||||
- [x] 2. Buat migration manual `20260504000000_add_statistik_pct_ringkasan_kesehatan/migration.sql` (mode interaktif tidak tersedia di sandbox)
|
||||
- [x] 3. `findUnique.ts` tidak perlu diubah — `findFirst` tanpa `select` otomatis bawa field baru
|
||||
- [x] 4. `updt.ts` + `index.ts` — handler create/update + Elysia body validation (range 0-100)
|
||||
- [x] 5. `bun run build` — exit 0
|
||||
- [x] 6. Bump `0.1.48 → 0.1.49`
|
||||
- [x] 7. Commit `feat(kesehatan): tambah 4 field statistik pct...` + push branch `tasks/statistik-kesehatan-ringkasan/add-pct-fields/20260504` + merge ke `stg`
|
||||
| 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) |
|
||||
|
||||
## Step B — State File Admin
|
||||
## Kontrak Lama (jaga, AI-CONTRACT §10)
|
||||
|
||||
- [x] 1. Bikin `_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan.ts` (Valtio + zod)
|
||||
- [x] 2. `findUnique.load()` — GET + sync ke form
|
||||
- [x] 3. `update.submit()` — zod validate (count ≥ 0, Pct 0-100) + PUT + refresh
|
||||
- [x] 4. `update.reset()`
|
||||
- [x] 5. `bun run build` — exit 0
|
||||
- [x] 6. Bump `0.1.49 → 0.1.50`
|
||||
- [x] 7. Commit + push ke `stg` (2 remote)
|
||||
- `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 terminal lokal — apply 4 kolom baru ke DB
|
||||
- [ ] Trigger GitHub Workflow (publish + re-pull) bila mau deploy ke STG
|
||||
- [ ] User jalankan `bunx prisma migrate deploy` di lokal setelah Step A & B
|
||||
- [ ] User trigger publish + re-pull workflow bila mau deploy STG
|
||||
|
||||
## Prinsip
|
||||
- Additive only — field lama (`ibuHamilAkh`, `balitaTerdaftar`, `alertStunting`) tidak disentuh
|
||||
- Validasi range 0-100 di Elysia `updt.ts`
|
||||
- Tidak ada breaking change kontrak
|
||||
|
||||
- **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
|
||||
|
||||
Reference in New Issue
Block a user