- 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
8.1 KiB
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 lamaPUT /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
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/createfind-many.ts— GET/find-many?page=&limit=&search=&status=find-by-id.ts— GET/:idupdt.ts— PUT/:iddel.ts— DELETE/del/:id(soft deleteisActive=false)
Daftarkan di kesehatan/index.ts: .use(IbuHamil)
Checklist
- 1. Tambah enum
IbuHamilStatus+ modelIbuHamilkeprisma/schema.prisma - 2. Tambah
@relationreverse di modelPosyandu(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.tspattern Valtio + zod (mirrorposyandu/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 buildexit 0 - 10. Bump version + commit + push
Step B — Model Balita
Schema
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:
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
Balitake schema - 2.
@relationreverse 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+ SelectstatusStunting) - 8. Sidebar entry "Balita" di group Kesehatan
- 9.
bun run buildexit 0 - 10. Bump version + commit + push
Step C — Endpoint Stats Agregat
File baru src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/stats.ts
Return:
{
ibuHamilAktif: number,
balitaTerdaftar: number,
alertStunting: number,
imunisasiLengkapPct: number, // round
pemeriksaanRutinPct: number,
giziBaikPct: number,
targetStuntingPct: number, // dari RingkasanKesehatanDesa
}
Implementasi (Prisma):
- 1
countIbuHamil + 1groupByBalita (status flags) - Hitung pct di JS:
Math.round((n / total) * 100) - Edge case
total=0→ semua pct = 0
Checklist
- 1. Bikin
stats.tshandler - 2. Daftarkan route
GET /statsdiringkasan-kesehatan/index.ts - 3. Update state
ringkasanKesehatan.ts: - tambahfindStats: { data, loading, load() }- method lamafindUnique+update→ tetap (kontrak) - 4. Refactor
page.tsxringkasan: - section atas: 7 stat card read-only darifindStats.data- section bawah: form kecil hanyatargetStuntingPct(policy target) - 2 tombol: "Kelola Ibu Hamil" → push/admin/kesehatan/ibu-hamil"Kelola Balita" → push/admin/kesehatan/balita - 5.
bun run buildexit 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 deploydi lokal setelah Step A & B - User trigger publish + re-pull workflow bila mau deploy STG
Prinsip
- Additive: kontrak
/find+/updateringkasankesehatan 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