Files
desa-darmasaba/MIND/PLAN/task-statistik-kesehatan-ringkasan.md
nico dccba1f82b 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
2026-05-04 16:52:14 +08:00

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 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

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

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 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:

{
  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