Compare commits

...

22 Commits

Author SHA1 Message Date
16b9b74a73 merge: feat(beasiswa) tambah UI konfigurasi beasiswa di admin 2026-05-06 11:52:02 +08:00
c0b08f4f69 feat(beasiswa): tambah UI konfigurasi beasiswa di admin pendidikan
- Tambah tab "Konfigurasi Beasiswa" di layoutTabs beasiswa-desa
- Buat halaman beasiswa-config/page.tsx dengan stats card (penerima,
  dana, tahun ajaran) + form edit tahunAjaran & danaTersalurkan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 11:51:57 +08:00
7b14923620 merge: fix(beasiswa) BigInt serialization error pada ringkasan stats 2026-05-06 11:17:52 +08:00
3cc09c83d8 fix(beasiswa): konversi danaTersalurkan BigInt ke string sebelum JSON serialize
Elysia tidak bisa serialize BigInt ke JSON — ubah return type danaTersalurkan
dari bigint ke string dengan .toString() di beasiswaRingkasanStats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 11:17:48 +08:00
5658063f68 merge: feat(pendidikan) tambah state ringkasan & expand seeder beasiswa 2026-05-06 11:03:20 +08:00
d7e1192ab0 feat(pendidikan): tambah state ringkasan pendidikan & beasiswa + expand seeder beasiswa 45 entry
- Tambah ringkasan-pendidikan.ts: state valtio fetch GET /api/pendidikan/ringkasan/stats
- Tambah ringkasan-beasiswa.ts: state valtio fetch ringkasan stats + beasiswaConfig find/update
- Expand beasiswa-pendaftar.json dari 3 → 45 entry (nama Bali, NIK unik, enum valid)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 11:03:11 +08:00
8857853baf ci: tambah stack_name input opsional di publish.yml 2026-05-05 16:34:49 +08:00
ce26bc7cc8 chore: bump version to 0.1.56 for stg deploy 2026-05-05 16:25:25 +08:00
b479991c27 merge: refactor(ui) posyandu balita & ibu-hamil penghargaan pattern 2026-05-05 16:12:51 +08:00
e71c938b2f refactor(ui): sesuaikan UI balita & ibu-hamil dengan pola penghargaan
- Gunakan HeaderSearch + dua-komponen pattern (outer + inner list)
- Ganti Loader → Skeleton h={600}, ActionIcon → Button size="xs" variant="light"
- Tambah Paper wrapper, layout="fixed" table, desktop/mobile responsive split
- Search debounce 1000ms via useDebouncedValue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:12:46 +08:00
ff25ead2df feat(sosial-dashboard): tambah API ringkasan pendidikan & beasiswa + CRUD event budaya - bump 0.1.55
- API GET /api/pendidikan/ringkasan/stats: siswa per jenjang, jumlah lembaga & pengajar
- API GET /api/pendidikan/beasiswa/ringkasan/stats: jumlah penerima, dana, tahun ajaran
- Schema + migration: model EventBudaya (nama, tanggal, lokasi, deskripsi)
- API CRUD /api/desa/eventbudaya: create, find-many, findUnique, updt, del
- State admin: eventBudaya.ts (valtio proxy, create/findMany/edit/delete)
- Admin CMS: /admin/desa/event-budaya (list, create, edit)
- Navbar: tambah entry Desa_9 Event Budaya di semua role
- Seeder: 8 event budaya Bali untuk Desa Darmasaba

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 15:25:34 +08:00
2497298703 chore: bump version to 0.1.54 for stg deploy 2026-05-05 14:14:19 +08:00
ba632f9d39 merge: fix(kesehatan) konsolidasi posyandu tabs - bump 0.1.53 2026-05-05 12:26:23 +08:00
f1ee53a7b9 fix(kesehatan): konsolidasi balita, ibu-hamil, ringkasan-kesehatan ke dalam posyandu tabs + fix semua routing path - bump 0.1.53
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 12:26:18 +08:00
fd2060405f feat(kesehatan): slim ringkasan kesehatan schema + tambah seeder balita & ibu hamil - bump 0.1.52
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 10:31:40 +08:00
afe0d9d04b fix(kesehatan): solve valtio mutation error and sync db
- Fix 'Cannot assign to read only property' by mutating original Valtio proxy in create/edit pages for IbuHamil and Balita
- Sync database schema with 'prisma db push' to create IbuHamil and Balita tables
- Verify build success
2026-05-04 17:04:44 +08:00
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
fc6846f7a1 feat(kesehatan): admin page ringkasan-kesehatan + sidebar entry - bump ke 0.1.51
- New page src/app/admin/(dashboard)/kesehatan/ringkasan-kesehatan/page.tsx
  konsumsi ringkasanKesehatanState (load + submit) dengan 7 NumberInput
  (3 count + 4 pct).
- Tambah Kesehatan_8 "Ringkasan Kesehatan" di 3 instance sidebar
  (list_PageAdmin.tsx).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:20:39 +08:00
9ef5773cc2 docs(mind): update plan + tambah summary statistik kesehatan ringkasan
Tandai Step A & B selesai, catat decision log dan pending manual
(bunx prisma migrate deploy + trigger GH workflow).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 14:35:42 +08:00
68a2a6390b feat(kesehatan): tambah state file ringkasanKesehatan + bump ke 0.1.50
State Valtio untuk RingkasanKesehatanDesa: findUnique (load + sync form)
dan update (zod validation, PUT, refresh). Cover 3 KPI + 4 field Pct baru.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 14:30:55 +08:00
ba2b90be75 feat(kesehatan): tambah 4 field statistik pct ke RingkasanKesehatanDesa - bump ke 0.1.49
Tambah imunisasiLengkapPct, pemeriksaanRutinPct, giziBaikPct, targetStuntingPct
ke schema + migration + Elysia body validation (range 0-100) + updt handler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 14:19:27 +08:00
3cc30bf0ff docs(mind): update AI collaboration contract plan and summary 2026-05-04 14:13:54 +08:00
76 changed files with 6652 additions and 432 deletions

View File

@@ -16,6 +16,10 @@ on:
description: "Image tag (e.g. 1.0.0)"
required: true
default: "1.0.0"
stack_name:
description: "Stack name (optional, ignored)"
required: false
default: ""
env:

View File

@@ -6,6 +6,6 @@
- [x] Fix `KegiatanCard.tsx` image rendering logic
- [x] Bump version in `package.json` to 0.1.48
- [x] Verify build successful
- [ ] Commit changes
- [ ] Create branch and push
- [ ] Merge to `stg`
- [x] Commit changes
- [x] Create branch and push
- [x] Merge to `stg`

View File

@@ -0,0 +1,246 @@
# 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

View File

@@ -0,0 +1,19 @@
# Summary: Add AI Collaboration Contract and Fix UI Issues
## Work Completed
- **AI Collaboration Contract**: Added `AI-CONTRACT.md` which defines the working agreement between human developers and AI assistants to prevent exponential bugs.
- **Documentation Update**: Linked `AI-CONTRACT.md` in `CLAUDE.md`.
- **UI Fix**: Modified `KegiatanCard.tsx` to handle cases where an activity item might not have an image, preventing layout issues or broken image icons.
- **State Management**: Committed several new state files for Desa Profile (Lambang, Mantan Perbekel, Maskot, Profil Perbekel, Sejarah, Visi Misi).
- **Version Bump**: Incremented project version to `0.1.48`.
- **Verification**: Ran `bun run build` successfully to ensure no regressions.
## Branching & Deployment
- Created branch: `tasks/ai-collaboration-contract/add-contract-and-fix-ui/2026-04-30-15-45`
- Pushed to remotes: `origin` and `deploy`.
- Merged to `stg` branch and pushed to all remotes.
## Impact
- Better AI-Human collaboration guidelines to ensure code quality.
- Improved robustness of the public `Kegiatan Desa` page.
- Completed several state management modules for the admin panel.

View File

@@ -0,0 +1,62 @@
# Summary: Statistik Persentase Kesehatan di RingkasanKesehatanDesa
## Konteks
Dashboard admin punya 4 progress bar (Imunisasi Lengkap, Pemeriksaan Rutin,
Gizi Baik, Target Stunting) yang sebelumnya belum ada backend-nya. Schema
`RingkasanKesehatanDesa` hanya menyimpan 3 field count (`ibuHamilAkh`,
`balitaTerdaftar`, `alertStunting`).
## Yang Dikerjakan
### Step A — Schema + API (v0.1.49)
1. **Schema** — tambah 4 field `Int @default(0)` di `RingkasanKesehatanDesa`:
`imunisasiLengkapPct`, `pemeriksaanRutinPct`, `giziBaikPct`, `targetStuntingPct`.
2. **Migration** — buat manual SQL di
`prisma/migrations/20260504000000_add_statistik_pct_ringkasan_kesehatan/migration.sql`
(sandbox non-interaktif, jadi `migrate dev` di-bypass dengan SQL langsung).
3. **API**
- `kesehatan/ringkasan-kesehatan/updt.ts` — handler create + update terima 4 field baru.
- `kesehatan/ringkasan-kesehatan/index.ts` — Elysia body validation `t.Number({ minimum: 0, maximum: 100 })`.
- `findUnique.ts` tidak perlu diubah — `findFirst` tanpa `select` otomatis bawa field baru.
4. Build clean, push ke 2 remote (bipprojectbali + nicoarya20), merge ke `stg`.
### Step B — State Admin (v0.1.50)
- File baru `_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan.ts` (Valtio proxy):
- `findUnique.load()` — GET `/api/kesehatan/ringkasankesehatan/find`, isi `data` + sync ke `update.form`.
- `update.submit()` — zod validate (count ≥ 0, Pct 0-100) → PUT `/api/kesehatan/ringkasankesehatan/update` → refresh.
- `update.reset()` — reset form ke default.
## Decision Log
- **Gabung ke `RingkasanKesehatanDesa`** (bukan model baru `StatistikKesehatan`)
alasan: domain sama (ringkasan kesehatan desa), pattern existing single-row config
cocok, single fetch untuk dashboard, migration ringan additive.
- **Tipe `Int` (0100)** — UI render integer %, tidak butuh desimal.
- **Suffix `Pct`** — eksplisit semantik persentase (count vs persentase di model yang sama).
- **Field lama tidak disentuh** — additive only, sesuai AI-CONTRACT §10.
## Pending Manual
- Jalankan `bunx prisma migrate deploy` di terminal lokal → apply kolom baru ke DB.
- Trigger GitHub Workflow (publish + re-pull) bila mau deploy STG.
## Affected Files
```
prisma/schema.prisma
prisma/migrations/20260504000000_add_statistik_pct_ringkasan_kesehatan/migration.sql [NEW]
src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/updt.ts
src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/index.ts
src/app/admin/(dashboard)/_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan.ts [NEW]
src/app/admin/(dashboard)/kesehatan/ringkasan-kesehatan/page.tsx [NEW]
src/app/admin/_com/list_PageAdmin.tsx (3 sidebar instances → tambah Kesehatan_8)
package.json (0.1.48 → 0.1.51)
MIND/PLAN/task-statistik-kesehatan-ringkasan.md [NEW]
```
## Commits
- `ba2b90be` feat(kesehatan): tambah 4 field statistik pct ke RingkasanKesehatanDesa - bump ke 0.1.49
- `68a2a639` feat(kesehatan): tambah state file ringkasanKesehatan + bump ke 0.1.50
- (next) feat(kesehatan): admin page ringkasan-kesehatan + sidebar entry - bump ke 0.1.51

View File

@@ -6,25 +6,25 @@
### Tech Stack
| Kategori | Teknologi |
|----------|-----------|
| **Framework** | Next.js 15 (App Router) |
| **Language** | TypeScript (strict mode) |
| **Runtime** | Bun |
| **Backend API** | Elysia.js (high-performance HTTP server) |
| **Database** | PostgreSQL |
| **ORM** | Prisma 6.3.1 |
| **UI Framework** | Mantine UI v7-v8 |
| **State Management** | Jotai + Valtio + SWR |
| **Authentication** | iron-session + JWT (@elysiajs/jwt) |
| **File Storage** | Seafile (self-hosted) |
| **Text Editor** | Tiptap (Rich text editor) |
| **Charts** | Recharts + Chart.js |
| **Maps** | Leaflet + react-leaflet |
| **Testing** | Vitest (unit) + Playwright (E2E) |
| **Styling** | Mantine + PostCSS + Framer Motion |
| **Deployment** | Docker + GHCR + Portainer + GitHub Actions |
| **Version** | 0.1.11 |
| Kategori | Teknologi |
| -------------------- | ------------------------------------------ |
| **Framework** | Next.js 15 (App Router) |
| **Language** | TypeScript (strict mode) |
| **Runtime** | Bun |
| **Backend API** | Elysia.js (high-performance HTTP server) |
| **Database** | PostgreSQL |
| **ORM** | Prisma 6.3.1 |
| **UI Framework** | Mantine UI v7-v8 |
| **State Management** | Jotai + Valtio + SWR |
| **Authentication** | iron-session + JWT (@elysiajs/jwt) |
| **File Storage** | Seafile (self-hosted) |
| **Text Editor** | Tiptap (Rich text editor) |
| **Charts** | Recharts + Chart.js |
| **Maps** | Leaflet + react-leaflet |
| **Testing** | Vitest (unit) + Playwright (E2E) |
| **Styling** | Mantine + PostCSS + Framer Motion |
| **Deployment** | Docker + GHCR + Portainer + GitHub Actions |
| **Version** | 0.1.11 |
---
@@ -195,137 +195,148 @@ Browser
## 4. Modul Domain
### A. PPID (Pejabat Pengelola Informasi dan Dokumentasi)
**Lokasi**: `src/app/admin/(dashboard)/ppid/` dan `src/app/darmasaba/(pages)/ppid/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Profil PPID | Profil pejabat pengelola informasi |
| Struktur PPID | Struktur organisasi PPID dengan hierarki |
| Visi & Misi PPID | Visi dan misi PPID desa |
| Daftar Informasi Publik | Katalog informasi publik yang tersedia |
| Dasar Hukum | Regulasi dan dasar hukum PPID |
| Sub-modul | Deskripsi |
| --------------------------- | ---------------------------------------------- |
| Profil PPID | Profil pejabat pengelola informasi |
| Struktur PPID | Struktur organisasi PPID dengan hierarki |
| Visi & Misi PPID | Visi dan misi PPID desa |
| Daftar Informasi Publik | Katalog informasi publik yang tersedia |
| Dasar Hukum | Regulasi dan dasar hukum PPID |
| Permohonan Informasi Publik | Form permohonan informasi (NIK, kontak, jenis) |
| Permohonan Keberatan | Formulir keberatan informasi |
| Indeks Kepuasan Masyarakat | Survey kepuasan dengan grafik demografis |
| Permohonan Keberatan | Formulir keberatan informasi |
| Indeks Kepuasan Masyarakat | Survey kepuasan dengan grafik demografis |
### B. Desa (Landing Page & Umum)
**Lokasi**: `src/app/admin/(dashboard)/desa/` dan `src/app/darmasaba/(pages)/desa/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Profil Desa | Sejarah, visi-misi, lambang, maskot |
| Profil Perbekel | Biodata, pengalaman, program unggulan perbekel |
| Perbekel dari Masa ke Masa | Historis perbekel per periode |
| Berita | Artikel berita dengan kategori & multi-image |
| Gallery | Foto dan video galeri |
| Pengumuman | Pengumuman desa dengan kategori |
| Potensi Desa | Potensi desa dengan kategori |
| Layanan Desa | Surat keterangan, ajukan permohonan |
| Penghargaan | Prestasi dan penghargaan desa |
| Desa Anti Korupsi | Transparansi anti-korupsi |
| SDGs Desa | Sustainable Development Goals desa |
| APBDes | Anggaran desa dengan hierarki item & realisasi |
| Prestasi Desa | Katalog prestasi |
| Sub-modul | Deskripsi |
| -------------------------- | ---------------------------------------------- |
| Profil Desa | Sejarah, visi-misi, lambang, maskot |
| Profil Perbekel | Biodata, pengalaman, program unggulan perbekel |
| Perbekel dari Masa ke Masa | Historis perbekel per periode |
| Berita | Artikel berita dengan kategori & multi-image |
| Gallery | Foto dan video galeri |
| Pengumuman | Pengumuman desa dengan kategori |
| Potensi Desa | Potensi desa dengan kategori |
| Layanan Desa | Surat keterangan, ajukan permohonan |
| Penghargaan | Prestasi dan penghargaan desa |
| Desa Anti Korupsi | Transparansi anti-korupsi |
| SDGs Desa | Sustainable Development Goals desa |
| APBDes | Anggaran desa dengan hierarki item & realisasi |
| Prestasi Desa | Katalog prestasi |
### C. Kesehatan
**Lokasi**: `src/app/admin/(dashboard)/kesehatan/` dan `src/app/darmasaba/(pages)/kesehatan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Fasilitas Kesehatan | Info rumah sakit/klinik (jam, dokter, tarif) |
| Puskesmas | Data puskesmas dengan jam operasional & kontak |
| Posyandu | Jadwal dan informasi posyandu |
| Program Kesehatan | Program-program kesehatan desa |
| Penanganan Darurat | Prosedur penanganan darurat |
| Kontak Darurat | Kontak emergency dengan WhatsApp |
| Info Wabah Penyakit | Informasi wabah penyakit |
| Artikel Kesehatan | Artikel kesehatan lengkap |
| Data Kesehatan Warga | Statistik kesehatan warga |
| Kelahiran & Kematian | Data vital statistik |
| Grafik Kepuasan | Grafik kepuasan layanan kesehatan |
| Sub-modul | Deskripsi |
| -------------------- | ---------------------------------------------- |
| Fasilitas Kesehatan | Info rumah sakit/klinik (jam, dokter, tarif) |
| Puskesmas | Data puskesmas dengan jam operasional & kontak |
| Posyandu | Jadwal dan informasi posyandu |
| Program Kesehatan | Program-program kesehatan desa |
| Penanganan Darurat | Prosedur penanganan darurat |
| Kontak Darurat | Kontak emergency dengan WhatsApp |
| Info Wabah Penyakit | Informasi wabah penyakit |
| Artikel Kesehatan | Artikel kesehatan lengkap |
| Data Kesehatan Warga | Statistik kesehatan warga |
| Kelahiran & Kematian | Data vital statistik |
| Grafik Kepuasan | Grafik kepuasan layanan kesehatan |
### D. Ekonomi
**Lokasi**: `src/app/admin/(dashboard)/ekonomi/` dan `src/app/darmasaba/(pages)/ekonomi/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Pasar Desa | Katalog pasar desa dengan produk & rating |
| Struktur BUMDes | Organisasi BUMDes dengan pengurus |
| APBDes (PADesa) | Pendapatan Asli Desa |
| Program Kemiskinan | Program dan statistik kemiskinan |
| Sektor Unggulan | Sektor ekonomi unggulan desa |
| Lowongan Kerja Lokal | Info lowongan pekerjaan |
| Demografi Pekerjaan | Distribusi pekerjaan penduduk |
| Jumlah Pengangguran | Statistik pengangguran |
| Sub-modul | Deskripsi |
| ------------------------------ | ------------------------------------------ |
| Pasar Desa | Katalog pasar desa dengan produk & rating |
| Struktur BUMDes | Organisasi BUMDes dengan pengurus |
| APBDes (PADesa) | Pendapatan Asli Desa |
| Program Kemiskinan | Program dan statistik kemiskinan |
| Sektor Unggulan | Sektor ekonomi unggulan desa |
| Lowongan Kerja Lokal | Info lowongan pekerjaan |
| Demografi Pekerjaan | Distribusi pekerjaan penduduk |
| Jumlah Pengangguran | Statistik pengangguran |
| Penduduk Usia Kerja Menganggur | Analisis pengangguran by usia & pendidikan |
| Jumlah Penduduk Miskin | Tren kemiskinan tahunan |
| Jumlah Penduduk Miskin | Tren kemiskinan tahunan |
### E. Kependudukan
**Lokasi**: `src/app/admin/(dashboard)/kependudukan/` dan `src/app/darmasaba/(pages)/kependudukan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Data Banjar | Data penduduk per banjar |
| Distribusi Agama | Statistik agama penduduk |
| Distribusi Umur | Piramida umur penduduk |
| Migrasi Penduduk | Data migrasi masuk/keluar |
| Sub-modul | Deskripsi |
| ----------------- | -------------------------------------- |
| Data Banjar | Data penduduk per banjar |
| Distribusi Agama | Statistik agama penduduk |
| Distribusi Umur | Piramida umur penduduk |
| Migrasi Penduduk | Data migrasi masuk/keluar |
| Dinamika Penduduk | Kelahiran, kematian, migrasi per tahun |
### F. Pendidikan
**Lokasi**: `src/app/admin/(dashboard)/pendidikan/` dan `src/app/darmasaba/(pages)/pendidikan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Info Sekolah & PAUD | Data sekolah per jenjang (TK, SD, SMP, SMA) |
| Beasiswa Desa | Program beasiswa & pendaftar |
| Program Pendidikan Anak | Program pendidikan anak |
| Bimbingan Belajar | Informasi bimbingan belajar |
| Pendidikan Non Formal | Tempat & program non-formal |
| Perpustakaan Digital | Katalog buku & peminjaman |
| Data Pendidikan | Statistik pendidikan |
| Sub-modul | Deskripsi |
| ----------------------- | ------------------------------------------- |
| Info Sekolah & PAUD | Data sekolah per jenjang (TK, SD, SMP, SMA) |
| Beasiswa Desa | Program beasiswa & pendaftar |
| Program Pendidikan Anak | Program pendidikan anak |
| Bimbingan Belajar | Informasi bimbingan belajar |
| Pendidikan Non Formal | Tempat & program non-formal |
| Perpustakaan Digital | Katalog buku & peminjaman |
| Data Pendidikan | Statistik pendidikan |
### G. Keamanan
**Lokasi**: `src/app/admin/(dashboard)/keamanan/` dan `src/app/darmasaba/(pages)/keamanan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Keamanan Lingkungan (Pecalang/Patwal) | Sistem keamanan tradisional Bali |
| Polsek Terdekat | Data polsek dengan layanan & map |
| Kontak Darurat | Kontak darurat keamanan |
| Pencegahan Kriminalitas | Info pencegahan kriminal |
| Laporan Publik | Laporan masyarakat dengan tracking status |
| Tips Keamanan | Tips dan panduan keamanan |
| Sub-modul | Deskripsi |
| ------------------------------------- | ----------------------------------------- |
| Keamanan Lingkungan (Pecalang/Patwal) | Sistem keamanan tradisional Bali |
| Polsek Terdekat | Data polsek dengan layanan & map |
| Kontak Darurat | Kontak darurat keamanan |
| Pencegahan Kriminalitas | Info pencegahan kriminal |
| Laporan Publik | Laporan masyarakat dengan tracking status |
| Tips Keamanan | Tips dan panduan keamanan |
### H. Lingkungan
**Lokasi**: `src/app/admin/(dashboard)/lingkungan/` dan `src/app/darmasaba/(pages)/lingkungan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Pengelolaan Sampah | Bank sampah & pengelolaan |
| Program Penghijauan | Program penghijauan desa |
| Data Lingkungan | Data lingkungan desa |
| Gotong Royong | Kegiatan gotong royong |
| Edukasi Lingkungan | Edukasi lingkungan hidup |
| Sub-modul | Deskripsi |
| -------------------- | --------------------------------- |
| Pengelolaan Sampah | Bank sampah & pengelolaan |
| Program Penghijauan | Program penghijauan desa |
| Data Lingkungan | Data lingkungan desa |
| Gotong Royong | Kegiatan gotong royong |
| Edukasi Lingkungan | Edukasi lingkungan hidup |
| Konservasi Adat Bali | Tri Hita Karana & konservasi adat |
### I. Inovasi
**Lokasi**: `src/app/admin/(dashboard)/inovasi/` dan `src/app/darmasaba/(pages)/inovasi/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Desa Digital (Smart Village) | Transformasi digital desa |
| Program Kreatif Desa | Program kreatif & inovatif |
| Kolaborasi Inovasi | Kolaborasi dengan mitra |
| Info Teknologi Tepat Guna | Info teknologi untuk desa |
| Ajukan Ide Inovatif | Form pengajuan ide dari warga |
| Layanan Online Desa | Layanan administrasi online |
| Sub-modul | Deskripsi |
| ---------------------------- | ----------------------------- |
| Desa Digital (Smart Village) | Transformasi digital desa |
| Program Kreatif Desa | Program kreatif & inovatif |
| Kolaborasi Inovasi | Kolaborasi dengan mitra |
| Info Teknologi Tepat Guna | Info teknologi untuk desa |
| Ajukan Ide Inovatif | Form pengajuan ide dari warga |
| Layanan Online Desa | Layanan administrasi online |
### J. Musik Desa
**Lokasi**: `src/app/admin/(dashboard)/musik/` dan `src/app/darmasaba/(pages)/musik/`
Model `MusikDesa` dengan audio file, cover image, genre, dan durasi. Dilengkapi dengan `FixedPlayerBar` di layout publik.
### K. User & Role (Admin)
**Lokasi**: `src/app/admin/(dashboard)/user&role/`
- **Role-based Access Control**: Role dengan permission JSON
@@ -341,124 +352,124 @@ Schema terdiri dari **2413 baris** dengan **100+ model** dan **berbagai enum**.
### Core Models
| Model | Keterangan |
|-------|-----------|
| `FileStorage` | Central file storage untuk semua uploaded files |
| `AppMenu` / `AppMenuChild` | Menu navigasi aplikasi |
| `User` / `Role` / `UserSession` / `UserMenuAccess` | Sistem autentikasi & otorisasi |
| `KodeOtp` | OTP codes untuk login |
| Model | Keterangan |
| -------------------------------------------------- | ----------------------------------------------- |
| `FileStorage` | Central file storage untuk semua uploaded files |
| `AppMenu` / `AppMenuChild` | Menu navigasi aplikasi |
| `User` / `Role` / `UserSession` / `UserMenuAccess` | Sistem autentikasi & otorisasi |
| `KodeOtp` | OTP codes untuk login |
### Landing Page & Desa
| Model | Keterangan |
|-------|-----------|
| `PejabatDesa` | Pejabat desa dengan foto |
| `ProfilPerbekel` | Profil perbekel (biodata, pengalaman, program) |
| `PerbekelDariMasaKeMasa` | Historis perbekel |
| `Berita` / `KategoriBerita` | Berita desa |
| `PotensiDesa` / `KategoriPotensi` | Potensi desa |
| `Pengumuman` / `CategoryPengumuman` | Pengumuman |
| `GalleryFoto` / `GalleryVideo` | Gallery media |
| `Penghargaan` | Penghargaan desa |
| `APBDes` / `APBDesItem` / `RealisasiItem` | Anggaran dengan realisasi |
| `DesaAntiKorupsi` / `KategoriDesaAntiKorupsi` | Transparansi |
| `SdgsDesa` | SDGs desa |
| `PrestasiDesa` / `KategoriPrestasiDesa` | Prestasi |
| `MusikDesa` | Musik desa |
| Model | Keterangan |
| --------------------------------------------- | ---------------------------------------------- |
| `PejabatDesa` | Pejabat desa dengan foto |
| `ProfilPerbekel` | Profil perbekel (biodata, pengalaman, program) |
| `PerbekelDariMasaKeMasa` | Historis perbekel |
| `Berita` / `KategoriBerita` | Berita desa |
| `PotensiDesa` / `KategoriPotensi` | Potensi desa |
| `Pengumuman` / `CategoryPengumuman` | Pengumuman |
| `GalleryFoto` / `GalleryVideo` | Gallery media |
| `Penghargaan` | Penghargaan desa |
| `APBDes` / `APBDesItem` / `RealisasiItem` | Anggaran dengan realisasi |
| `DesaAntiKorupsi` / `KategoriDesaAntiKorupsi` | Transparansi |
| `SdgsDesa` | SDGs desa |
| `PrestasiDesa` / `KategoriPrestasiDesa` | Prestasi |
| `MusikDesa` | Musik desa |
### PPID
| Model | Keterangan |
|-------|-----------|
| `StrukturPPID` / `PosisiOrganisasiPPID` / `PegawaiPPID` | Struktur organisasi |
| `VisiMisiPPID` | Visi misi |
| `ProfilePPID` | Profil pejabat |
| `DasarHukumPPID` | Regulasi |
| `DaftarInformasiPublik` | Katalog informasi |
| `PermohonanInformasiPublik` | Permohonan + lookup tables |
| `FormulirPermohonanKeberatan` | Keberatan |
| `IndeksKepuasanMasyarakat` + grafik breakdown | Survey kepuasan |
| Model | Keterangan |
| ------------------------------------------------------- | -------------------------- |
| `StrukturPPID` / `PosisiOrganisasiPPID` / `PegawaiPPID` | Struktur organisasi |
| `VisiMisiPPID` | Visi misi |
| `ProfilePPID` | Profil pejabat |
| `DasarHukumPPID` | Regulasi |
| `DaftarInformasiPublik` | Katalog informasi |
| `PermohonanInformasiPublik` | Permohonan + lookup tables |
| `FormulirPermohonanKeberatan` | Keberatan |
| `IndeksKepuasanMasyarakat` + grafik breakdown | Survey kepuasan |
### Kesehatan
| Model | Keterangan |
|-------|-----------|
| `FasilitasKesehatan` | Fasilitas lengkap (dokter, tarif, prosedur) |
| `Puskesmas` / `JamOperasional` / `KontakPuskesmas` | Puskesmas |
| `Posyandu` | Pos pelayanan terpadu |
| `ProgramKesehatan` | Program kesehatan |
| `ArtikelKesehatan` | Artikel lengkap (gejala, pencegahan, P3K, dll) |
| `PenangananDarurat` / `KontakDarurat` | Darurat |
| `InfoWabahPenyakit` | Wabah |
| `DataKematian_Kelahiran` / `Kelahiran` / `Kematian` | Vital statistik |
| `GrafikKepuasan` | Kepuasan |
| Model | Keterangan |
| --------------------------------------------------- | ---------------------------------------------- |
| `FasilitasKesehatan` | Fasilitas lengkap (dokter, tarif, prosedur) |
| `Puskesmas` / `JamOperasional` / `KontakPuskesmas` | Puskesmas |
| `Posyandu` | Pos pelayanan terpadu |
| `ProgramKesehatan` | Program kesehatan |
| `ArtikelKesehatan` | Artikel lengkap (gejala, pencegahan, P3K, dll) |
| `PenangananDarurat` / `KontakDarurat` | Darurat |
| `InfoWabahPenyakit` | Wabah |
| `DataKematian_Kelahiran` / `Kelahiran` / `Kematian` | Vital statistik |
| `GrafikKepuasan` | Kepuasan |
### Ekonomi
| Model | Keterangan |
|-------|-----------|
| `PasarDesa` / `KategoriProduk` / `KategoriToPasar` | Pasar desa |
| `StrukturBumDes` / `PosisiOrganisasiBumDes` / `PegawaiBumDes` | BUMDes |
| `ProgramKemiskinan` / `StatistikKemiskinan` | Kemiskinan |
| `SektorUnggulanDesa` | Sektor unggulan |
| `LowonganPekerjaan` | Lowongan |
| `DataDemografiPekerjaan` | Demografi pekerjaan |
| `DetailDataPengangguran` | Pengangguran |
| `GrafikJumlahPendudukMiskin` | Tren kemiskinan |
| Model | Keterangan |
| ------------------------------------------------------------- | ------------------- |
| `PasarDesa` / `KategoriProduk` / `KategoriToPasar` | Pasar desa |
| `StrukturBumDes` / `PosisiOrganisasiBumDes` / `PegawaiBumDes` | BUMDes |
| `ProgramKemiskinan` / `StatistikKemiskinan` | Kemiskinan |
| `SektorUnggulanDesa` | Sektor unggulan |
| `LowonganPekerjaan` | Lowongan |
| `DataDemografiPekerjaan` | Demografi pekerjaan |
| `DetailDataPengangguran` | Pengangguran |
| `GrafikJumlahPendudukMiskin` | Tren kemiskinan |
### Kependudukan
| Model | Keterangan |
|-------|-----------|
| `DataBanjar` | Data per banjar |
| `DistribusiAgama` | Distribusi agama |
| `DistribusiUmur` | Distribusi umur |
| `MigrasiPenduduk` | Migrasi (MASUK/KELUAR) |
| `DinamikaPenduduk` | Dinamika tahunan |
| Model | Keterangan |
| ------------------ | ---------------------- |
| `DataBanjar` | Data per banjar |
| `DistribusiAgama` | Distribusi agama |
| `DistribusiUmur` | Distribusi umur |
| `MigrasiPenduduk` | Migrasi (MASUK/KELUAR) |
| `DinamikaPenduduk` | Dinamika tahunan |
### Pendidikan
| Model | Keterangan |
|-------|-----------|
| `JenjangPendidikan` / `Lembaga` / `Siswa` / `Pengajar` | Data sekolah |
| `BeasiswaPendaftar` | Beasiswa (dengan enum lengkap) |
| `DataPerpustakaan` / `KategoriBuku` / `PeminjamanBuku` | Perpustakaan |
| `DataPendidikan` | Statistik |
| Model | Keterangan |
| ------------------------------------------------------ | ------------------------------ |
| `JenjangPendidikan` / `Lembaga` / `Siswa` / `Pengajar` | Data sekolah |
| `BeasiswaPendaftar` | Beasiswa (dengan enum lengkap) |
| `DataPerpustakaan` / `KategoriBuku` / `PeminjamanBuku` | Perpustakaan |
| `DataPendidikan` | Statistik |
### Keamanan
| Model | Keterangan |
|-------|-----------|
| `KeamananLingkungan` | Keamanan lingkungan |
| `PolsekTerdekat` / `LayananPolsek` / `LayananToPolsek` | Polsek |
| `KontakDaruratKeamanan` / `KontakItem` | Kontak darurat |
| `PencegahanKriminalitas` | Pencegahan |
| `LaporanPublik` / `PenangananLaporanPublik` (enum StatusLaporan) | Laporan |
| `Pelapor` | Pelapor |
| `MenuTipsKeamanan` | Tips |
| Model | Keterangan |
| ---------------------------------------------------------------- | ------------------- |
| `KeamananLingkungan` | Keamanan lingkungan |
| `PolsekTerdekat` / `LayananPolsek` / `LayananToPolsek` | Polsek |
| `KontakDaruratKeamanan` / `KontakItem` | Kontak darurat |
| `PencegahanKriminalitas` | Pencegahan |
| `LaporanPublik` / `PenangananLaporanPublik` (enum StatusLaporan) | Laporan |
| `Pelapor` | Pelapor |
| `MenuTipsKeamanan` | Tips |
### Lingkungan
| Model | Keterangan |
|-------|-----------|
| `PengelolaanSampah` | Pengelolaan sampah |
| `KeteranganBankSampahTerdekat` | Bank sampah |
| `ProgramPenghijauan` | Penghijauan |
| `DataLingkunganDesa` | Data lingkungan |
| `KegiatanDesa` / `KategoriKegiatan` | Gotong royong |
| `FilosofiTriHita` / `BentukKonservasiBerdasarkanAdat` | Konservasi Bali |
| Model | Keterangan |
| ----------------------------------------------------- | ------------------ |
| `PengelolaanSampah` | Pengelolaan sampah |
| `KeteranganBankSampahTerdekat` | Bank sampah |
| `ProgramPenghijauan` | Penghijauan |
| `DataLingkunganDesa` | Data lingkungan |
| `KegiatanDesa` / `KategoriKegiatan` | Gotong royong |
| `FilosofiTriHita` / `BentukKonservasiBerdasarkanAdat` | Konservasi Bali |
### Inovasi
| Model | Keterangan |
|-------|-----------|
| `DesaDigital` | Smart village |
| `ProgramKreatif` | Program kreatif |
| `KolaborasiInovasi` / `MitraKolaborasi` | Kolaborasi |
| `InfoTekno` | Teknologi tepat guna |
| `AjukanIdeInovatif` | Ide dari warga |
| `AdministrasiOnline` / `JenisLayanan` | Layanan online |
| `PengaduanMasyarakat` / `JenisPengaduan` | Pengaduan |
| Model | Keterangan |
| ---------------------------------------- | -------------------- |
| `DesaDigital` | Smart village |
| `ProgramKreatif` | Program kreatif |
| `KolaborasiInovasi` / `MitraKolaborasi` | Kolaborasi |
| `InfoTekno` | Teknologi tepat guna |
| `AjukanIdeInovatif` | Ide dari warga |
| `AdministrasiOnline` / `JenisLayanan` | Layanan online |
| `PengaduanMasyarakat` / `JenisPengaduan` | Pengaduan |
---
@@ -466,43 +477,43 @@ Schema terdiri dari **2413 baris** dengan **100+ model** dan **berbagai enum**.
Semua API ditangani oleh **Elysia.js** di `src/app/api/[[...slugs]]/route.ts`:
| Endpoint Group | Prefix | Deskripsi |
|---------------|--------|-----------|
| **File Storage** | `/api/file-storage` | CRUD file storage |
| **Landing Page** | `/api/landing-page` | Profil, prestasi, anti-korupsi, SDGs, APBDes |
| **Desa** | `/api/desa` | Berita, gallery, potensi, pengumuman, layanan |
| **PPID** | `/api/ppid` | Semua endpoint PPID |
| **Kesehatan** | `/api/kesehatan` | Fasilitas, puskesmas, posyandu, artikel, wabah |
| **Ekonomi** | `/api/ekonomi` | Pasar desa, BUMDes, APBDes, pengangguran |
| **Keamanan** | `/api/keamanan` | Keamanan, polsek, laporan, kriminalitas |
| **Lingkungan** | `/api/lingkungan` | Sampah, penghijauan, gotong royong |
| **Pendidikan** | `/api/pendidikan` | Sekolah, beasiswa, perpustakaan |
| **Kependudukan** | `/api/kependudukan` | Banjar, agama, umur, migrasi |
| **Inovasi** | `/api/inovasi` | Desa digital, kolaborasi, pengaduan |
| **User** | `/api/admin/user` | CRUD user |
| **Role** | `/api/admin/role` | CRUD role |
| **Search** | `/api/search` | Global search |
| **Utils** | `/api/utils/version` | Version info |
| Endpoint Group | Prefix | Deskripsi |
| ---------------- | -------------------- | ---------------------------------------------- |
| **File Storage** | `/api/file-storage` | CRUD file storage |
| **Landing Page** | `/api/landing-page` | Profil, prestasi, anti-korupsi, SDGs, APBDes |
| **Desa** | `/api/desa` | Berita, gallery, potensi, pengumuman, layanan |
| **PPID** | `/api/ppid` | Semua endpoint PPID |
| **Kesehatan** | `/api/kesehatan` | Fasilitas, puskesmas, posyandu, artikel, wabah |
| **Ekonomi** | `/api/ekonomi` | Pasar desa, BUMDes, APBDes, pengangguran |
| **Keamanan** | `/api/keamanan` | Keamanan, polsek, laporan, kriminalitas |
| **Lingkungan** | `/api/lingkungan` | Sampah, penghijauan, gotong royong |
| **Pendidikan** | `/api/pendidikan` | Sekolah, beasiswa, perpustakaan |
| **Kependudukan** | `/api/kependudukan` | Banjar, agama, umur, migrasi |
| **Inovasi** | `/api/inovasi` | Desa digital, kolaborasi, pengaduan |
| **User** | `/api/admin/user` | CRUD user |
| **Role** | `/api/admin/role` | CRUD role |
| **Search** | `/api/search` | Global search |
| **Utils** | `/api/utils/version` | Version info |
### Utility Endpoints
| Endpoint | Method | Deskripsi |
|----------|--------|-----------|
| `/api/img/:name` | GET | Serve image dengan resize |
| `/api/img/:name` | DELETE | Delete image |
| `/api/imgs` | GET | List images dengan pagination |
| `/api/upl-img` | POST | Upload multiple images |
| `/api/upl-img-single` | POST | Upload single image |
| `/api/upl-csv` | POST | Upload CSV multiple |
| `/api/upl-csv-single` | POST | Upload single CSV |
| Endpoint | Method | Deskripsi |
| --------------------- | ------ | ----------------------------- |
| `/api/img/:name` | GET | Serve image dengan resize |
| `/api/img/:name` | DELETE | Delete image |
| `/api/imgs` | GET | List images dengan pagination |
| `/api/upl-img` | POST | Upload multiple images |
| `/api/upl-img-single` | POST | Upload single image |
| `/api/upl-csv` | POST | Upload CSV multiple |
| `/api/upl-csv-single` | POST | Upload single CSV |
### Auth Endpoints
| Endpoint | Method | Deskripsi |
|----------|--------|-----------|
| `/api/auth/login` | POST | Login dengan OTP |
| `/api/auth/logout` | POST | Logout |
| `/api/auth/me` | GET | Get current user |
| Endpoint | Method | Deskripsi |
| ------------------ | ------ | ---------------- |
| `/api/auth/login` | POST | Login dengan OTP |
| `/api/auth/logout` | POST | Logout |
| `/api/auth/me` | GET | Get current user |
**Swagger Documentation**: Tersedia di `/api/docs`
@@ -514,22 +525,23 @@ Admin dashboard menggunakan **Mantine AppShell** dengan sidebar navigasi dinamis
### Route Group: `/admin`
| Section | Path | Deskripsi |
|---------|------|-----------|
| **Landing Page** | `/admin/landing-page/` | Profil desa, prestasi, anti-korupsi, SDGs, media sosial |
| **Desa** | `/admin/desa/` | Berita, gallery, layanan, penghargaan, pengumuman, potensi, profil |
| **PPID** | `/admin/ppid/` | 8 sub-modul PPID lengkap |
| **Kesehatan** | `/admin/kesehatan/` | 8 sub-modul kesehatan |
| **Ekonomi** | `/admin/ekonomi/` | 10 sub-modul ekonomi |
| **Kependudukan** | `/admin/kependudukan/` | 4 sub-modul kependudukan |
| **Pendidikan** | `/admin/pendidikan/` | 7 sub-modul pendidikan |
| **Keamanan** | `/admin/keamanan/` | 6 sub-modul keamanan |
| **Lingkungan** | `/admin/lingkungan/` | 6 sub-modul lingkungan |
| **Inovasi** | `/admin/inovasi/` | 6 sub-modul inovasi |
| **Musik** | `/admin/musik/` | Manajemen musik desa |
| **User & Role** | `/admin/user&role/` | Manajemen user, role, menu access |
| Section | Path | Deskripsi |
| ---------------- | ---------------------- | ------------------------------------------------------------------ |
| **Landing Page** | `/admin/landing-page/` | Profil desa, prestasi, anti-korupsi, SDGs, media sosial |
| **Desa** | `/admin/desa/` | Berita, gallery, layanan, penghargaan, pengumuman, potensi, profil |
| **PPID** | `/admin/ppid/` | 8 sub-modul PPID lengkap |
| **Kesehatan** | `/admin/kesehatan/` | 8 sub-modul kesehatan |
| **Ekonomi** | `/admin/ekonomi/` | 10 sub-modul ekonomi |
| **Kependudukan** | `/admin/kependudukan/` | 4 sub-modul kependudukan |
| **Pendidikan** | `/admin/pendidikan/` | 7 sub-modul pendidikan |
| **Keamanan** | `/admin/keamanan/` | 6 sub-modul keamanan |
| **Lingkungan** | `/admin/lingkungan/` | 6 sub-modul lingkungan |
| **Inovasi** | `/admin/inovasi/` | 6 sub-modul inovasi |
| **Musik** | `/admin/musik/` | Manajemen musik desa |
| **User & Role** | `/admin/user&role/` | Manajemen user, role, menu access |
### Fitur Admin:
- **Role-based Dynamic Navbar**: Navbar berubah berdasarkan roleId user
- **Dark Mode Toggle**: Tema gelap/terang
- **OTP Login**: Login dengan nomor telepon + kode OTP
@@ -539,11 +551,12 @@ Admin dashboard menggunakan **Mantine AppShell** dengan sidebar navigasi dinamis
- **Rich Text Editor**: Tiptap untuk konten HTML
### Role-Based Redirect:
| roleId | Role | Default Redirect |
|--------|------|-----------------|
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
| roleId | Role | Default Redirect |
| ------- | ------------------------ | --------------------------------------------------- |
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu/list-posyandu` |
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
---
@@ -553,22 +566,23 @@ Public website di `/darmasaba/` dengan layout yang mencakup **Navbar**, **Footer
### Route Group: `/darmasaba`
| Section | Path | Deskripsi |
|---------|------|-----------|
| **Home** | `/darmasaba` | Landing page utama |
| **Desa** | `/darmasaba/desa` | Profil, berita, gallery, layanan, pengumuman, potensi |
| **PPID** | `/darmasaba/ppid` | 7 sub-halaman PPID publik |
| **Kesehatan** | `/darmasaba/kesehatan` | Info kesehatan publik |
| **Ekonomi** | `/darmasaba/ekonomi` | Info ekonomi desa |
| **Kependudukan** | `/darmasaba/kependudukan` | Data kependudukan |
| **Pendidikan** | `/darmasaba/pendidikan` | Info pendidikan |
| **Keamanan** | `/darmasaba/keamanan` | Info keamanan |
| **Lingkungan** | `/darmasaba/lingkungan` | Info lingkungan |
| **Inovasi** | `/darmasaba/inovasi` | Info inovasi |
| **Musik** | `/darmasaba/musik` | Musik desa |
| **Module** | `/darmasaba/module/*` | Link ke modul eksternal (DAVES, MANGAN, Bicara-Darma, BARES, dll) |
| Section | Path | Deskripsi |
| ---------------- | ------------------------- | ----------------------------------------------------------------- |
| **Home** | `/darmasaba` | Landing page utama |
| **Desa** | `/darmasaba/desa` | Profil, berita, gallery, layanan, pengumuman, potensi |
| **PPID** | `/darmasaba/ppid` | 7 sub-halaman PPID publik |
| **Kesehatan** | `/darmasaba/kesehatan` | Info kesehatan publik |
| **Ekonomi** | `/darmasaba/ekonomi` | Info ekonomi desa |
| **Kependudukan** | `/darmasaba/kependudukan` | Data kependudukan |
| **Pendidikan** | `/darmasaba/pendidikan` | Info pendidikan |
| **Keamanan** | `/darmasaba/keamanan` | Info keamanan |
| **Lingkungan** | `/darmasaba/lingkungan` | Info lingkungan |
| **Inovasi** | `/darmasaba/inovasi` | Info inovasi |
| **Musik** | `/darmasaba/musik` | Musik desa |
| **Module** | `/darmasaba/module/*` | Link ke modul eksternal (DAVES, MANGAN, Bicara-Darma, BARES, dll) |
### Fitur Publik:
- **Fixed Music Player Bar**: Player musik yang selalu tampil di bottom
- **Global Search**: Pencarian global
- **News Reader**: Notifikasi berita modern
@@ -581,33 +595,33 @@ Public website di `/darmasaba/` dengan layout yang mencakup **Navbar**, **Footer
### Admin Components (`src/components/admin/`)
| Komponen | Deskripsi |
|----------|-----------|
| `AdminThemeProvider.tsx` | Theme provider untuk admin |
| `DarkModeToggle.tsx` | Toggle dark/light mode |
| `UnifiedSurface.tsx` | Consistent surface/card component |
| `UnifiedTypography.tsx` | Consistent typography system |
| Komponen | Deskripsi |
| ------------------------ | --------------------------------- |
| `AdminThemeProvider.tsx` | Theme provider untuk admin |
| `DarkModeToggle.tsx` | Toggle dark/light mode |
| `UnifiedSurface.tsx` | Consistent surface/card component |
| `UnifiedTypography.tsx` | Consistent typography system |
### Public Shared Components (`src/app/darmasaba/_com/`)
| Komponen | Deskripsi |
|----------|-----------|
| `Navbar.tsx` | Main navigation bar |
| `NavbarMainMenu.tsx` | Main menu dengan kategori |
| `NavbarSubMenu.tsx` | Submenu dropdown |
| `Footer.tsx` | Footer dengan info desa |
| `FixedPlayerBar.tsx` | Music player bar fixed di bottom |
| `LoadDataFirstClient.tsx` | Client-side data preloader |
| `globalSearch.tsx` | Global search component |
| `NewsReader.tsx` | News notification reader |
| `ModernNewsNotification.tsx` | News toast notifications |
| Komponen | Deskripsi |
| ---------------------------- | -------------------------------- |
| `Navbar.tsx` | Main navigation bar |
| `NavbarMainMenu.tsx` | Main menu dengan kategori |
| `NavbarSubMenu.tsx` | Submenu dropdown |
| `Footer.tsx` | Footer dengan info desa |
| `FixedPlayerBar.tsx` | Music player bar fixed di bottom |
| `LoadDataFirstClient.tsx` | Client-side data preloader |
| `globalSearch.tsx` | Global search component |
| `NewsReader.tsx` | News notification reader |
| `ModernNewsNotification.tsx` | News toast notifications |
### Global Components (`src/app/_com/`)
| Komponen | Deskripsi |
|----------|-----------|
| Komponen | Deskripsi |
| ----------------- | --------------------- |
| `SpashScreen.tsx` | Splash screen on load |
| `WebVitals.tsx` | Web Vitals monitoring |
| `WebVitals.tsx` | Web Vitals monitoring |
---
@@ -615,13 +629,13 @@ Public website di `/darmasaba/` dengan layout yang mencakup **Navbar**, **Footer
Proyek menggunakan **multi-layer state management**:
| Library | Penggunaan | Lokasi |
|---------|-----------|--------|
| **Jotai** | Auth state (`authStore`) | `src/store/authStore.ts` |
| **Valtio** | Dark mode, layanan, image list, nav state | `src/state/*.ts` |
| **SWR** | Server state fetching & caching | Digunakan di components |
| **React Context** | Music player context | `src/app/context/MusicContext.tsx` |
| **React useState** | Local component state | Di components |
| Library | Penggunaan | Lokasi |
| ------------------ | ----------------------------------------- | ---------------------------------- |
| **Jotai** | Auth state (`authStore`) | `src/store/authStore.ts` |
| **Valtio** | Dark mode, layanan, image list, nav state | `src/state/*.ts` |
| **SWR** | Server state fetching & caching | Digunakan di components |
| **React Context** | Music player context | `src/app/context/MusicContext.tsx` |
| **React useState** | Local component state | Di components |
### State Files:
@@ -643,6 +657,7 @@ src/store/
Sistem autentikasi menggunakan **OTP (One-Time Password)** via WhatsApp/Telepon dengan **iron-session** untuk session management.
### Flow Autentikasi:
1. User memasukkan **nomor telepon** di `/login`
2. Sistem mengirim **kode OTP** via WhatsApp Server
3. OTP disimpan di model `KodeOtp`
@@ -651,6 +666,7 @@ Sistem autentikasi menggunakan **OTP (One-Time Password)** via WhatsApp/Telepon
6. Session disimpan di `UserSession` model dengan expiry
### Session Structure:
```typescript
// src/lib/session.ts
type SessionData = {
@@ -665,13 +681,15 @@ type SessionData = {
```
### Role-Based Access:
| roleId | Role | Default Redirect |
|--------|------|-----------------|
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
| roleId | Role | Default Redirect |
| ------- | ------------------------ | --------------------------------------------------- |
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu/list-posyandu` |
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
### Authorization:
- **UserMenuAccess**: Mapping user ke menu yang boleh diakses
- **Dynamic Navbar**: Navbar dirender berdasarkan `menuIds` user
- **Inactive Users**: Dialihkan ke `/waiting-room`
@@ -698,6 +716,7 @@ Stage 2: Runner
```
### Entry Point (`docker-entrypoint.sh`):
```bash
bunx prisma migrate deploy # Run migrations
exec bun start # Start Next.js production server
@@ -707,11 +726,11 @@ exec bun start # Start Next.js production server
Terdapat **3 workflow**:
| Workflow | Trigger | Fungsi |
|----------|---------|--------|
| `docker-publish.yml` | Push tag `v*` | Auto build & push ke GHCR |
| `publish.yml` | Manual (workflow_dispatch) | Build & push ke GHCR dengan input `stack_env` + `tag` |
| `re-pull.yml` | Manual (workflow_dispatch) | Re-pull image di Portainer dengan input `stack_name` + `stack_env` |
| Workflow | Trigger | Fungsi |
| -------------------- | -------------------------- | ------------------------------------------------------------------ |
| `docker-publish.yml` | Push tag `v*` | Auto build & push ke GHCR |
| `publish.yml` | Manual (workflow_dispatch) | Build & push ke GHCR dengan input `stack_env` + `tag` |
| `re-pull.yml` | Manual (workflow_dispatch) | Re-pull image di Portainer dengan input `stack_name` + `stack_env` |
### Deployment Workflow (Sequential):
@@ -730,32 +749,35 @@ Terdapat **3 workflow**:
**PENTING**: `publish.yml` dan `re-pull.yml` TIDAK boleh dijalankan bersamaan (race condition).
### Environments:
- **dev**: Development
- **stg**: Staging (`desa-darmasaba-stg.wibudev.com`)
- **prod**: Production
### Notification:
- Telegram notification via `notify.sh` script setelah setiap workflow
---
## 13. Scripts
| Script | Command | Deskripsi |
|--------|---------|-----------|
| `dev` | `next dev` | Development server |
| `build` | `next build` | Production build |
| `start` | `next start` | Production server |
| `test:api` | `vitest run` | Run API unit tests |
| `test:e2e` | `playwright test` | Run E2E tests |
| `test` | `bun run test:api && bun run test:e2e` | Run all tests |
| `seed` | `bun run prisma/seed.ts` | Seed database |
| `prisma:generate` | `bunx prisma generate` | Generate Prisma client |
| `prisma:push` | `bunx prisma db push` | Push schema to database |
| `prisma:studio` | `bunx prisma studio` | Open Prisma Studio GUI |
| `gen:api` | *(empty)* | Generate API types (placeholder) |
| Script | Command | Deskripsi |
| ----------------- | -------------------------------------- | -------------------------------- |
| `dev` | `next dev` | Development server |
| `build` | `next build` | Production build |
| `start` | `next start` | Production server |
| `test:api` | `vitest run` | Run API unit tests |
| `test:e2e` | `playwright test` | Run E2E tests |
| `test` | `bun run test:api && bun run test:e2e` | Run all tests |
| `seed` | `bun run prisma/seed.ts` | Seed database |
| `prisma:generate` | `bunx prisma generate` | Generate Prisma client |
| `prisma:push` | `bunx prisma db push` | Push schema to database |
| `prisma:studio` | `bunx prisma studio` | Open Prisma Studio GUI |
| `gen:api` | _(empty)_ | Generate API types (placeholder) |
### Prisma Seed Configuration:
```json
// package.json
{
@@ -771,35 +793,37 @@ Terdapat **3 workflow**:
File: `.env.example`
| Variable | Deskripsi | Contoh |
|----------|-----------|--------|
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/desa-darmasaba` |
| `SEAFILE_TOKEN` | Seafile API token | `your_seafile_token` |
| `SEAFILE_REPO_ID` | Seafile repository ID | `your_repo_id` |
| `SEAFILE_URL` | Seafile instance URL | `https://seafile.example.com` |
| `SEAFILE_PUBLIC_SHARE_TOKEN` | Token untuk public share | `your_share_token` |
| `WIBU_UPLOAD_DIR` | Upload directory path | `uploads` |
| `WA_SERVER_TOKEN` | WhatsApp server token | `your_wa_token` |
| `NEXT_PUBLIC_BASE_URL` | Base URL aplikasi | `/` (relative) |
| `EMAIL_USER` | Email untuk notifikasi | `your_email@gmail.com` |
| `EMAIL_PASS` | Email app password | `your_app_password` |
| `BASE_TOKEN_KEY` | JWT secret key | `your_jwt_secret` |
| `BOT_TOKEN` | Telegram bot token | `your_bot_token` |
| `CHAT_ID` | Telegram chat ID | `your_chat_id` |
| `SESSION_PASSWORD` | iron-session password (min 32 chars) | `secure_32_char_password` |
| `ELEVENLABS_API_KEY` | ElevenLabs API (TTS - optional) | `your_elevenlabs_key` |
| Variable | Deskripsi | Contoh |
| ---------------------------- | ------------------------------------ | ------------------------------------------------------ |
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/desa-darmasaba` |
| `SEAFILE_TOKEN` | Seafile API token | `your_seafile_token` |
| `SEAFILE_REPO_ID` | Seafile repository ID | `your_repo_id` |
| `SEAFILE_URL` | Seafile instance URL | `https://seafile.example.com` |
| `SEAFILE_PUBLIC_SHARE_TOKEN` | Token untuk public share | `your_share_token` |
| `WIBU_UPLOAD_DIR` | Upload directory path | `uploads` |
| `WA_SERVER_TOKEN` | WhatsApp server token | `your_wa_token` |
| `NEXT_PUBLIC_BASE_URL` | Base URL aplikasi | `/` (relative) |
| `EMAIL_USER` | Email untuk notifikasi | `your_email@gmail.com` |
| `EMAIL_PASS` | Email app password | `your_app_password` |
| `BASE_TOKEN_KEY` | JWT secret key | `your_jwt_secret` |
| `BOT_TOKEN` | Telegram bot token | `your_bot_token` |
| `CHAT_ID` | Telegram chat ID | `your_chat_id` |
| `SESSION_PASSWORD` | iron-session password (min 32 chars) | `secure_32_char_password` |
| `ELEVENLABS_API_KEY` | ElevenLabs API (TTS - optional) | `your_elevenlabs_key` |
---
## 15. Layanan Eksternal
### PostgreSQL
- **Provider**: PostgreSQL via Prisma ORM
- **Schema**: `public`
- **Connection**: Via `DATABASE_URL` environment variable
- **Migrations**: `prisma migrate deploy` di docker entrypoint
### Seafile (File Storage)
- **Tipe**: Self-hosted file sync & share
- **Penggunaan**: Storage untuk images, documents, audio files
- **Integrasi**: `src/lib/seafile-auth-service.ts`
@@ -807,19 +831,23 @@ File: `.env.example`
- **Config**: Token, repo ID, base URL
### WhatsApp Server
- **Penggunaan**: Kirim OTP codes saat login
- **Config**: `WA_SERVER_TOKEN`
### Telegram Bot
- **Penggunaan**: Notifikasi deployment & sistem
- **Config**: `BOT_TOKEN` + `CHAT_ID`
- **Integration**: `notify.sh` script di GitHub Actions
### ElevenLabs (Optional)
- **Penggunaan**: Text-to-Speech (TTS) features
- **Config**: `ELEVENLABS_API_KEY`
### Email (Nodemailer)
- **Penggunaan**: Notifikasi email untuk subscription/pengumuman
- **Config**: `EMAIL_USER` + `EMAIL_PASS`
- **Provider**: Gmail (app password)
@@ -828,15 +856,15 @@ File: `.env.example`
## Ringkasan Cepat
| Aspek | Detail |
|-------|--------|
| **Framework** | Next.js 15 (App Router) + Elysia.js |
| **Database** | PostgreSQL + Prisma (100+ models) |
| **Auth** | OTP + iron-session + JWT |
| **Storage** | Seafile + local uploads |
| **UI** | Mantine UI + Tiptap + Framer Motion |
| **State** | Jotai + Valtio + SWR |
| **Deploy** | Docker + GHCR + Portainer + GitHub Actions |
| **Runtime** | Bun |
| **Testing** | Vitest + Playwright |
| **Version** | 0.1.11 |
| Aspek | Detail |
| ------------- | ------------------------------------------ |
| **Framework** | Next.js 15 (App Router) + Elysia.js |
| **Database** | PostgreSQL + Prisma (100+ models) |
| **Auth** | OTP + iron-session + JWT |
| **Storage** | Seafile + local uploads |
| **UI** | Mantine UI + Tiptap + Framer Motion |
| **State** | Jotai + Valtio + SWR |
| **Deploy** | Docker + GHCR + Portainer + GitHub Actions |
| **Runtime** | Bun |
| **Testing** | Vitest + Playwright |
| **Version** | 0.1.11 |

View File

@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
"version": "0.1.48",
"version": "0.1.56",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -0,0 +1,30 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const eventBudayaJson = loadJsonData("desa/event-budaya/event-budaya.json");
export async function seedEventBudaya() {
console.log("🔄 Seeding Event Budaya...");
for (const item of eventBudayaJson) {
await prisma.eventBudaya.upsert({
where: { id: item.id },
update: {
nama: item.nama,
tanggal: new Date(item.tanggal),
lokasi: item.lokasi,
deskripsi: item.deskripsi,
},
create: {
id: item.id,
nama: item.nama,
tanggal: new Date(item.tanggal),
lokasi: item.lokasi,
deskripsi: item.deskripsi,
},
});
console.log(` ✅ Event: ${item.nama}`);
}
console.log("🎉 Event Budaya seed selesai");
}

View File

@@ -0,0 +1,502 @@
import prisma from "@/lib/prisma";
import { JenisKelaminBalita, StatusStunting } from "@prisma/client";
// Fokus data: proporsi stunting realistis untuk simulasi dashboard
// 10 STUNTING, 7 ALERT, 8 NORMAL dari 25 total
const BALITA_DATA = [
// ===== STUNTING (TB/U < -2 SD dari median WHO) =====
{
id: "balita_001",
nama: "Wayan Aditya Pratama",
nik: "5101014505230001",
tanggalLahir: new Date("2023-05-04"), // 36 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 11.5,
tinggiBadanCm: 83.0, // median 96cm, -2SD ~89cm
namaOrtu: "I Wayan Suardika",
alamat: "Banjar Pudak, Desa Darmasaba",
noHpOrtu: "08123456801",
posyanduId: "cmkanjnmx0006vntz1cn7owpb",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.8 SD. Dalam program PMT (Pemberian Makanan Tambahan). Orang tua sudah mendapat konseling gizi.",
},
{
id: "balita_002",
nama: "Ni Kadek Mira Sari",
nik: "5101014501240002",
tanggalLahir: new Date("2024-01-04"), // 16 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 7.8,
tinggiBadanCm: 70.5, // median ~78cm
namaOrtu: "I Kadek Suartha",
alamat: "Banjar Dahlia, Desa Darmasaba",
noHpOrtu: "08123456802",
posyanduId: "posyandu_dahlia_001",
imunisasiLengkap: false,
giziBaik: false,
pemeriksaanRutin: false,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -3.1 SD. Imunisasi belum lengkap. Ibu bekerja, kunjungan posyandu tidak rutin. Perlu pendampingan kader.",
},
{
id: "balita_003",
nama: "Putu Rian Saputra",
nik: "5101014501220003",
tanggalLahir: new Date("2022-01-04"), // 40 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 12.5,
tinggiBadanCm: 89.0, // median 103cm
namaOrtu: "Ni Putu Sumiati",
alamat: "Banjar Dahlia, Desa Darmasaba",
noHpOrtu: "08123456803",
posyanduId: "posyandu_dahlia_001",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -3.2 SD. Sudah dirujuk ke Puskesmas Abiansemal 3 untuk pemeriksaan lebih lanjut.",
},
{
id: "balita_004",
nama: "Ni Komang Ayu Lestari",
nik: "5101014507230004",
tanggalLahir: new Date("2023-07-04"), // 22 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 8.2,
tinggiBadanCm: 74.0, // median ~84cm
namaOrtu: "I Komang Sudiarta",
alamat: "Banjar Anggrek, Desa Darmasaba",
noHpOrtu: "08123456804",
posyanduId: "posyandu_anggrek_001",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.5 SD. Riwayat BBLR (berat lahir rendah) 2.3 kg.",
},
{
id: "balita_005",
nama: "Ketut Agus Pratama",
nik: "5101014507240005",
tanggalLahir: new Date("2024-07-04"), // 10 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 6.9,
tinggiBadanCm: 66.0, // median ~72cm
namaOrtu: "Ni Ketut Sariani",
alamat: "Banjar Kamboja, Desa Darmasaba",
noHpOrtu: "08123456805",
posyanduId: "posyandu_kamboja_001",
imunisasiLengkap: false,
giziBaik: false,
pemeriksaanRutin: false,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.3 SD. Lahir prematur 35 minggu. Keluarga prasejahtera, masuk program PKH.",
},
{
id: "balita_006",
nama: "Ni Made Sinta Dewi",
nik: "5101014507220006",
tanggalLahir: new Date("2022-07-04"), // 34 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 11.0,
tinggiBadanCm: 84.5, // median ~94cm
namaOrtu: "I Made Sudarsana",
alamat: "Banjar Melur, Desa Darmasaba",
noHpOrtu: "08123456806",
posyanduId: "posyandu_melur_001",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.6 SD. Nafsu makan rendah, sedang dalam pantauan ahli gizi Puskesmas.",
},
{
id: "balita_007",
nama: "Made Dani Putra",
nik: "5101014501250007",
tanggalLahir: new Date("2025-01-04"), // 4 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 5.0,
tinggiBadanCm: 57.0, // median ~63cm
namaOrtu: "Ni Made Suparni",
alamat: "Banjar Kenanga, Desa Darmasaba",
noHpOrtu: "08123456807",
posyanduId: "posyandu_kenanga_001",
imunisasiLengkap: false,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.4 SD. BBLR 2.1 kg, ibu ASI eksklusif. Kunjungan rutin ke posyandu.",
},
{
id: "balita_008",
nama: "Ni Putu Ratna Sari",
nik: "5101014507210008",
tanggalLahir: new Date("2021-07-04"), // 46 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 13.0,
tinggiBadanCm: 92.0, // median ~106cm
namaOrtu: "I Putu Suarjana",
alamat: "Banjar Mawar, Desa Darmasaba",
noHpOrtu: "08123456808",
posyanduId: "posyandu_mawar_001",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.9 SD. Sudah 6 bulan dalam program intervensi stunting desa.",
},
{
id: "balita_009",
nama: "Gede Yoga Pratama",
nik: "5101014505210009",
tanggalLahir: new Date("2021-05-04"), // 48 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 13.5,
tinggiBadanCm: 91.0, // median ~108cm
namaOrtu: "Ni Wayan Suarningsih",
alamat: "Banjar Pudak, Desa Darmasaba",
noHpOrtu: "08123456809",
posyanduId: "cmkanjnmx0006vntz1cn7owpb",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: false,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -3.0 SD. Keluarga baru pindah dari luar desa. Sedang proses pendataan ulang.",
},
{
id: "balita_010",
nama: "Ni Nyoman Sari Utami",
nik: "5101014505230010",
tanggalLahir: new Date("2023-05-04"), // 24 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 9.5,
tinggiBadanCm: 80.0, // median ~87cm
namaOrtu: "I Nyoman Sueca",
alamat: "Banjar Melati, Desa Darmasaba",
noHpOrtu: "08123456810",
posyanduId: "posyandu_melati_001",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.STUNTING,
catatan: "TB/U -2.2 SD. Batas bawah stunting. Perlu dipantau ketat tiap bulan.",
},
// ===== ALERT (TB/U antara -1 SD dan -2 SD) =====
{
id: "balita_011",
nama: "Wayan Krisna Dewa",
nik: "5101014501240011",
tanggalLahir: new Date("2024-01-04"), // 16 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 9.8,
tinggiBadanCm: 74.0, // median ~78cm, -1SD ~75cm
namaOrtu: "I Wayan Artana",
alamat: "Banjar Mawar, Desa Darmasaba",
noHpOrtu: "08123456811",
posyanduId: "posyandu_mawar_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.5 SD. Perlu pemantauan lebih lanjut, gizi cukup baik.",
},
{
id: "balita_012",
nama: "Ni Wayan Novi Andriani",
nik: "5101014505230012",
tanggalLahir: new Date("2023-05-04"), // 24 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 10.8,
tinggiBadanCm: 83.0, // median ~87cm
namaOrtu: "Ni Wayan Artini",
alamat: "Banjar Melati, Desa Darmasaba",
noHpOrtu: "08123456812",
posyanduId: "posyandu_melati_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.8 SD. Nafsu makan baik, BB naik konsisten.",
},
{
id: "balita_013",
nama: "Putu Deva Mahendra",
nik: "5101014511240013",
tanggalLahir: new Date("2024-11-04"), // 6 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 6.8,
tinggiBadanCm: 63.5, // median ~67cm
namaOrtu: "I Putu Ariana",
alamat: "Banjar Anggrek, Desa Darmasaba",
noHpOrtu: "08123456813",
posyanduId: "posyandu_anggrek_001",
imunisasiLengkap: false,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.6 SD. ASI eksklusif. Jadwal imunisasi DPT ketiga belum terlaksana.",
},
{
id: "balita_014",
nama: "Ni Komang Dewi Lestari",
nik: "5101014501220014",
tanggalLahir: new Date("2022-01-04"), // 40 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 14.0,
tinggiBadanCm: 96.0, // median ~103cm
namaOrtu: "I Komang Wirawan",
alamat: "Banjar Kamboja, Desa Darmasaba",
noHpOrtu: "08123456814",
posyanduId: "posyandu_kamboja_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.4 SD. Konsumsi protein hewani belum cukup, edukasi diberikan.",
},
{
id: "balita_015",
nama: "Made Surya Darma",
nik: "5101014511230015",
tanggalLahir: new Date("2023-11-04"), // 18 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 10.2,
tinggiBadanCm: 76.0, // median ~82cm
namaOrtu: "Ni Made Sudarmi",
alamat: "Banjar Melur, Desa Darmasaba",
noHpOrtu: "08123456815",
posyanduId: "posyandu_melur_001",
imunisasiLengkap: true,
giziBaik: false,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.9 SD. Sedang mendapat PMT (makanan tambahan) dari desa.",
},
{
id: "balita_016",
nama: "Ni Kadek Ayu Purnami",
nik: "5101014505250016",
tanggalLahir: new Date("2025-05-04"), // 0 bulan (baru lahir - 1 bulan)
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 3.5,
tinggiBadanCm: 49.0, // median ~52cm
namaOrtu: "I Kadek Suartha",
alamat: "Banjar Kenanga, Desa Darmasaba",
noHpOrtu: "08123456816",
posyanduId: "posyandu_kenanga_001",
imunisasiLengkap: false,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.7 SD. Bayi baru, lahir 2.8 kg. Dipantau dari awal.",
},
{
id: "balita_017",
nama: "Ketut Bayu Setiawan",
nik: "5101014511220017",
tanggalLahir: new Date("2022-11-04"), // 30 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 13.2,
tinggiBadanCm: 88.0, // median ~93cm
namaOrtu: "Ni Ketut Suarni",
alamat: "Banjar Dahlia, Desa Darmasaba",
noHpOrtu: "08123456817",
posyanduId: "posyandu_dahlia_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.ALERT,
catatan: "TB/U -1.3 SD. Tumbuh kembang membaik dalam 3 bulan terakhir.",
},
// ===== NORMAL =====
{
id: "balita_018",
nama: "Ni Made Intan Permata",
nik: "5101014501240018",
tanggalLahir: new Date("2024-01-04"), // 16 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 10.5,
tinggiBadanCm: 78.0,
namaOrtu: "I Made Sudiarsa",
alamat: "Banjar Mawar, Desa Darmasaba",
noHpOrtu: "08123456818",
posyanduId: "posyandu_mawar_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_019",
nama: "Wayan Arya Nugraha",
nik: "5101014505230019",
tanggalLahir: new Date("2023-05-04"), // 24 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 12.0,
tinggiBadanCm: 87.0,
namaOrtu: "Ni Wayan Suarni",
alamat: "Banjar Pudak, Desa Darmasaba",
noHpOrtu: "08123456819",
posyanduId: "cmkanjnmx0006vntz1cn7owpb",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_020",
nama: "Ni Putu Cantika Dewi",
nik: "5101014501220020",
tanggalLahir: new Date("2022-01-04"), // 40 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 15.5,
tinggiBadanCm: 103.0,
namaOrtu: "I Putu Sudiarta",
alamat: "Banjar Melati, Desa Darmasaba",
noHpOrtu: "08123456820",
posyanduId: "posyandu_melati_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_021",
nama: "Komang Danu Mahesa",
nik: "5101014505250021",
tanggalLahir: new Date("2025-05-04"), // 0 bulan (newborn)
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 3.8,
tinggiBadanCm: 52.0,
namaOrtu: "Ni Komang Artini",
alamat: "Banjar Anggrek, Desa Darmasaba",
noHpOrtu: "08123456821",
posyanduId: "posyandu_anggrek_001",
imunisasiLengkap: false,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_022",
nama: "Ni Nyoman Suka Rani",
nik: "5101014505210022",
tanggalLahir: new Date("2021-05-04"), // 48 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 16.5,
tinggiBadanCm: 105.0,
namaOrtu: "I Nyoman Suarman",
alamat: "Banjar Kamboja, Desa Darmasaba",
noHpOrtu: "08123456822",
posyanduId: "posyandu_kamboja_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_023",
nama: "Made Giri Putra Santosa",
nik: "5101014511240023",
tanggalLahir: new Date("2024-11-04"), // 6 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 8.1,
tinggiBadanCm: 67.5,
namaOrtu: "Ni Made Suciati",
alamat: "Banjar Melur, Desa Darmasaba",
noHpOrtu: "08123456823",
posyanduId: "posyandu_melur_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_024",
nama: "Ni Wayan Arta Yanti",
nik: "5101014511230024",
tanggalLahir: new Date("2023-11-04"), // 18 bulan
jenisKelamin: JenisKelaminBalita.P,
beratBadanKg: 11.0,
tinggiBadanCm: 82.0,
namaOrtu: "I Wayan Suarsa",
alamat: "Banjar Kenanga, Desa Darmasaba",
noHpOrtu: "08123456824",
posyanduId: "posyandu_kenanga_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
{
id: "balita_025",
nama: "Kadek Dika Permana",
nik: "5101014511220025",
tanggalLahir: new Date("2022-11-04"), // 30 bulan
jenisKelamin: JenisKelaminBalita.L,
beratBadanKg: 14.0,
tinggiBadanCm: 93.0,
namaOrtu: "Ni Kadek Suriati",
alamat: "Banjar Dahlia, Desa Darmasaba",
noHpOrtu: "08123456825",
posyanduId: "posyandu_dahlia_001",
imunisasiLengkap: true,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: StatusStunting.NORMAL,
},
];
export async function seedBalita() {
console.log("🔄 Seeding Balita...");
for (const d of BALITA_DATA) {
await prisma.balita.upsert({
where: { id: d.id },
update: {
nama: d.nama,
nik: d.nik,
tanggalLahir: d.tanggalLahir,
jenisKelamin: d.jenisKelamin,
beratBadanKg: d.beratBadanKg,
tinggiBadanCm: d.tinggiBadanCm,
namaOrtu: d.namaOrtu,
alamat: d.alamat,
noHpOrtu: d.noHpOrtu,
posyanduId: d.posyanduId,
imunisasiLengkap: d.imunisasiLengkap,
giziBaik: d.giziBaik,
pemeriksaanRutin: d.pemeriksaanRutin,
statusStunting: d.statusStunting,
catatan: d.catatan ?? null,
},
create: {
id: d.id,
nama: d.nama,
nik: d.nik,
tanggalLahir: d.tanggalLahir,
jenisKelamin: d.jenisKelamin,
beratBadanKg: d.beratBadanKg,
tinggiBadanCm: d.tinggiBadanCm,
namaOrtu: d.namaOrtu,
alamat: d.alamat,
noHpOrtu: d.noHpOrtu,
posyanduId: d.posyanduId,
imunisasiLengkap: d.imunisasiLengkap,
giziBaik: d.giziBaik,
pemeriksaanRutin: d.pemeriksaanRutin,
statusStunting: d.statusStunting,
catatan: d.catatan ?? null,
},
});
console.log(`✅ Balita seeded: ${d.nama} (${d.statusStunting})`);
}
console.log("🎉 Balita seed selesai");
}

View File

@@ -0,0 +1,222 @@
import prisma from "@/lib/prisma";
import { IbuHamilStatus } from "@prisma/client";
const IBU_HAMIL_DATA = [
{
id: "ibu_hamil_001",
nama: "Ni Wayan Sari Dewi",
nik: "5101014504960001",
usiaKehamilan: 28,
hpht: new Date("2025-10-20"),
taksiranLahir: new Date("2026-07-26"),
alamat: "Banjar Pudak, Desa Darmasaba",
noHp: "08123456701",
posyanduId: "cmkanjnmx0006vntz1cn7owpb",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_002",
nama: "Ni Made Artini",
nik: "5101012808980002",
usiaKehamilan: 16,
hpht: new Date("2026-01-13"),
taksiranLahir: new Date("2026-10-20"),
alamat: "Banjar Mawar, Desa Darmasaba",
noHp: "08123456702",
posyanduId: "posyandu_mawar_001",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_003",
nama: "Ni Putu Rahayu",
nik: "5101010109000003",
usiaKehamilan: 32,
hpht: new Date("2025-09-22"),
taksiranLahir: new Date("2026-06-29"),
alamat: "Banjar Melati, Desa Darmasaba",
noHp: "08123456703",
posyanduId: "posyandu_melati_001",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_004",
nama: "Ni Komang Lestari",
nik: "5101011505010004",
usiaKehamilan: 8,
hpht: new Date("2026-03-10"),
taksiranLahir: new Date("2026-12-14"),
alamat: "Banjar Dahlia, Desa Darmasaba",
noHp: "08123456704",
posyanduId: "posyandu_dahlia_001",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_005",
nama: "Ni Nyoman Suartini",
nik: "5101012012990005",
usiaKehamilan: 24,
hpht: new Date("2025-11-17"),
taksiranLahir: new Date("2026-08-24"),
alamat: "Banjar Anggrek, Desa Darmasaba",
noHp: "08123456705",
posyanduId: "posyandu_anggrek_001",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_006",
nama: "Ni Ketut Suriani",
nik: "5101010307970006",
usiaKehamilan: 20,
hpht: new Date("2025-12-15"),
taksiranLahir: new Date("2026-09-21"),
alamat: "Banjar Kamboja, Desa Darmasaba",
noHp: "08123456706",
posyanduId: "posyandu_kamboja_001",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_007",
nama: "Ni Wayan Rustini",
nik: "5101011806960007",
usiaKehamilan: 36,
hpht: new Date("2025-08-25"),
taksiranLahir: new Date("2026-06-01"),
alamat: "Banjar Melur, Desa Darmasaba",
noHp: "08123456707",
posyanduId: "posyandu_melur_001",
status: IbuHamilStatus.AKTIF,
catatan: "Tekanan darah perlu dipantau rutin",
},
{
id: "ibu_hamil_008",
nama: "Ni Made Sudiani",
nik: "5101010202020008",
usiaKehamilan: 12,
hpht: new Date("2026-02-10"),
taksiranLahir: new Date("2026-11-17"),
alamat: "Banjar Kenanga, Desa Darmasaba",
noHp: "08123456708",
posyanduId: "posyandu_kenanga_001",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_009",
nama: "Ni Putu Yuliani",
nik: "5101011507980009",
usiaKehamilan: 28,
hpht: new Date("2025-10-20"),
taksiranLahir: new Date("2026-07-26"),
alamat: "Banjar Pudak, Desa Darmasaba",
noHp: "08123456709",
posyanduId: "cmkanjnmx0006vntz1cn7owpb",
status: IbuHamilStatus.AKTIF,
},
{
id: "ibu_hamil_010",
nama: "Ni Nyoman Darmayanti",
nik: "5101012309010010",
usiaKehamilan: 16,
hpht: new Date("2026-01-13"),
taksiranLahir: new Date("2026-10-20"),
alamat: "Banjar Mawar, Desa Darmasaba",
noHp: "08123456710",
posyanduId: "posyandu_mawar_001",
status: IbuHamilStatus.AKTIF,
catatan: "Anemia ringan, konsumsi suplemen zat besi",
},
{
id: "ibu_hamil_011",
nama: "Ni Wayan Purwati",
nik: "5101010905950011",
usiaKehamilan: 40,
hpht: new Date("2025-07-28"),
taksiranLahir: new Date("2026-05-04"),
alamat: "Banjar Melati, Desa Darmasaba",
noHp: "08123456711",
posyanduId: "posyandu_melati_001",
status: IbuHamilStatus.MELAHIRKAN,
catatan: "Melahirkan normal di Puskesmas Abiansemal 3",
},
{
id: "ibu_hamil_012",
nama: "Ni Made Suarningsih",
nik: "5101011403930012",
usiaKehamilan: 39,
hpht: new Date("2025-08-04"),
taksiranLahir: new Date("2026-05-11"),
alamat: "Banjar Dahlia, Desa Darmasaba",
noHp: "08123456712",
posyanduId: "posyandu_dahlia_001",
status: IbuHamilStatus.MELAHIRKAN,
},
{
id: "ibu_hamil_013",
nama: "Ni Komang Sugiantari",
nik: "5101012706010013",
usiaKehamilan: 10,
alamat: "Banjar Anggrek, Desa Darmasaba",
noHp: "08123456713",
posyanduId: "posyandu_anggrek_001",
status: IbuHamilStatus.KEGUGURAN,
catatan: "Keguguran pada usia kehamilan 10 minggu, sudah ditangani",
},
{
id: "ibu_hamil_014",
nama: "Ni Putu Aryanti",
nik: "5101010508940014",
usiaKehamilan: 0,
alamat: "Banjar Kamboja, Desa Darmasaba",
noHp: "08123456714",
posyanduId: "posyandu_kamboja_001",
status: IbuHamilStatus.NONAKTIF,
catatan: "Data lama, tidak aktif terdaftar",
},
{
id: "ibu_hamil_015",
nama: "Ni Ketut Suparmi",
nik: "5101011912920015",
usiaKehamilan: 0,
alamat: "Banjar Melur, Desa Darmasaba",
noHp: "08123456715",
posyanduId: "posyandu_melur_001",
status: IbuHamilStatus.NONAKTIF,
},
];
export async function seedIbuHamil() {
console.log("🔄 Seeding Ibu Hamil...");
for (const d of IBU_HAMIL_DATA) {
await prisma.ibuHamil.upsert({
where: { id: d.id },
update: {
nama: d.nama,
nik: d.nik,
usiaKehamilan: d.usiaKehamilan,
hpht: d.hpht ?? null,
taksiranLahir: d.taksiranLahir ?? null,
alamat: d.alamat,
noHp: d.noHp,
posyanduId: d.posyanduId,
status: d.status,
catatan: d.catatan ?? null,
},
create: {
id: d.id,
nama: d.nama,
nik: d.nik,
usiaKehamilan: d.usiaKehamilan,
hpht: d.hpht ?? null,
taksiranLahir: d.taksiranLahir ?? null,
alamat: d.alamat,
noHp: d.noHp,
posyanduId: d.posyanduId,
status: d.status,
catatan: d.catatan ?? null,
},
});
console.log(`✅ Ibu hamil seeded: ${d.nama} (${d.status})`);
}
console.log("🎉 Ibu Hamil seed selesai");
}

View File

@@ -7,17 +7,8 @@ export async function seedRingkasanKesehatan() {
await prisma.ringkasanKesehatanDesa.upsert({
where: { id: SINGLETON_ID },
update: {
ibuHamilAkh: 87,
balitaTerdaftar: 342,
alertStunting: 12,
},
create: {
id: SINGLETON_ID,
ibuHamilAkh: 87,
balitaTerdaftar: 342,
alertStunting: 12,
},
update: { targetStuntingPct: 10 },
create: { id: SINGLETON_ID, targetStuntingPct: 10 },
});
console.log("✅ Ringkasan Kesehatan Desa seeded");

View File

@@ -0,0 +1,58 @@
[
{
"id": "event-budaya-1",
"nama": "Hari Kesaktian Pancasila",
"tanggal": "2025-10-01T07:00:00.000Z",
"lokasi": "Balai Desa Darmasaba",
"deskripsi": "Peringatan Hari Kesaktian Pancasila diikuti seluruh perangkat desa dan warga Desa Darmasaba dengan upacara bendera dan kegiatan budaya."
},
{
"id": "event-budaya-2",
"nama": "Upacara Ngusaba Desa",
"tanggal": "2025-11-15T08:00:00.000Z",
"lokasi": "Pura Puseh Desa Darmasaba",
"deskripsi": "Upacara adat tahunan Ngusaba Desa sebagai bentuk rasa syukur kepada Ida Sang Hyang Widhi Wasa atas keselamatan dan kemakmuran desa."
},
{
"id": "event-budaya-3",
"nama": "Festival Budaya Desa Darmasaba",
"tanggal": "2026-05-20T09:00:00.000Z",
"lokasi": "Lapangan Desa Darmasaba",
"deskripsi": "Festival tahunan menampilkan kesenian tradisional Bali seperti tari kecak, legong, dan barong oleh sanggar seni dari Desa Darmasaba."
},
{
"id": "event-budaya-4",
"nama": "Perayaan HUT Desa Darmasaba",
"tanggal": "2026-08-17T07:30:00.000Z",
"lokasi": "Balai Desa Darmasaba",
"deskripsi": "Peringatan Hari Ulang Tahun Kemerdekaan Republik Indonesia sekaligus hari jadi Desa Darmasaba dengan berbagai lomba dan pertunjukan budaya."
},
{
"id": "event-budaya-5",
"nama": "Perayaan Galungan dan Kuningan",
"tanggal": "2026-03-04T06:00:00.000Z",
"lokasi": "Seluruh wilayah Desa Darmasaba",
"deskripsi": "Rangkaian perayaan Hari Raya Galungan dan Kuningan sebagai hari kemenangan dharma melawan adharma, dirayakan seluruh umat Hindu di Desa Darmasaba."
},
{
"id": "event-budaya-6",
"nama": "Lomba Ogoh-Ogoh Desa",
"tanggal": "2026-03-18T15:00:00.000Z",
"lokasi": "Lapangan Desa Darmasaba",
"deskripsi": "Lomba pembuatan dan parade ogoh-ogoh antar banjar se-Desa Darmasaba dalam rangka menyambut Hari Raya Nyepi."
},
{
"id": "event-budaya-7",
"nama": "Pementasan Wayang Kulit",
"tanggal": "2026-06-10T19:00:00.000Z",
"lokasi": "Wantilan Desa Darmasaba",
"deskripsi": "Pementasan wayang kulit semalam suntuk oleh dalang dari Desa Darmasaba sebagai bagian dari pelestarian seni budaya Bali."
},
{
"id": "event-budaya-8",
"nama": "Upacara Melaspas Gedung Balai Banjar",
"tanggal": "2026-09-05T08:00:00.000Z",
"lokasi": "Banjar Desa Darmasaba",
"deskripsi": "Upacara Melaspas sebagai ritual penyucian bangunan baru balai banjar agar membawa keselamatan dan kesejahteraan bagi krama banjar."
}
]

View File

@@ -9,14 +9,14 @@
"tempatLahir": "Badung",
"tanggalLahir": "2009-05-15",
"namaOrtu": "I Ketut Pratama",
"nik": "5106123456780001",
"nik": "5106121505090001",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.500.000/bulan",
"noHp": "081234567891",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Badung, Bali",
"email": "komang.wahyu@email.com",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "komang.wahyu001@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
@@ -30,14 +30,14 @@
"tempatLahir": "Badung",
"tanggalLahir": "2008-08-22",
"namaOrtu": "Ni Made Dewi",
"nik": "5106123456780002",
"nik": "5106126208080002",
"pekerjaanOrtu": "Pedagang",
"penghasilan": "Rp 2.000.000/bulan",
"noHp": "081234567892",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Badung, Bali",
"email": "niluh.dw@email.com",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "niluh.ayu002@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
@@ -51,14 +51,896 @@
"tempatLahir": "Badung",
"tanggalLahir": "2011-03-10",
"namaOrtu": "I Wayan Setiawan",
"nik": "5106123456780003",
"nik": "5106121003110003",
"pekerjaanOrtu": "Buruh",
"penghasilan": "Rp 1.200.000/bulan",
"noHp": "081234567893",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Badung, Bali",
"email": "made.agung@email.com",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "made.agung003@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-004",
"namaLengkap": "Ni Ketut Sari Utami",
"nis": "2024004",
"kelas": "XII IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-11-05",
"namaOrtu": "I Nyoman Utama",
"nik": "5106124511070004",
"pekerjaanOrtu": "Nelayan",
"penghasilan": "Rp 1.800.000/bulan",
"noHp": "081234567894",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "niketut.sari004@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-005",
"namaLengkap": "I Wayan Dharma Putra",
"nis": "2024005",
"kelas": "VIII",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Gianyar",
"tanggalLahir": "2011-07-20",
"namaOrtu": "I Made Dharma",
"nik": "5106122007110005",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.300.000/bulan",
"noHp": "081234567895",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.dharma005@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-006",
"namaLengkap": "Ni Putu Lestari Wulandari",
"nis": "2024006",
"kelas": "X IPS",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-02-14",
"namaOrtu": "Ni Made Lestari",
"nik": "5106125402090006",
"pekerjaanOrtu": "Ibu Rumah Tangga",
"penghasilan": "Rp 900.000/bulan",
"noHp": "081234567896",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "niputu.lestari006@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-007",
"namaLengkap": "I Nyoman Surya Budiana",
"nis": "2024007",
"kelas": "XI IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-09-30",
"namaOrtu": "I Ketut Budiana",
"nik": "5106123009080007",
"pekerjaanOrtu": "Tukang Bangunan",
"penghasilan": "Rp 2.500.000/bulan",
"noHp": "081234567897",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "nyoman.surya007@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-008",
"namaLengkap": "Ni Made Indah Suryani",
"nis": "2024008",
"kelas": "VII",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2012-01-18",
"namaOrtu": "I Wayan Suryani",
"nik": "5106125801120008",
"pekerjaanOrtu": "Buruh Tani",
"penghasilan": "Rp 1.100.000/bulan",
"noHp": "081234567898",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "nimade.indah008@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-009",
"namaLengkap": "I Gede Mahendra Yudha",
"nis": "2024009",
"kelas": "XII IPS",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Denpasar",
"tanggalLahir": "2007-06-12",
"namaOrtu": "I Made Mahendra",
"nik": "5106121206070009",
"pekerjaanOrtu": "Sopir",
"penghasilan": "Rp 2.200.000/bulan",
"noHp": "081234567899",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "gede.mahendra009@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-010",
"namaLengkap": "Ni Wayan Artini Padmini",
"nis": "2024010",
"kelas": "X IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-04-25",
"namaOrtu": "Ni Ketut Artini",
"nik": "5106126504090010",
"pekerjaanOrtu": "Pedagang Kecil",
"penghasilan": "Rp 1.600.000/bulan",
"noHp": "081234567900",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.artini010@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-011",
"namaLengkap": "I Putu Arnawa Santosa",
"nis": "2024011",
"kelas": "IX",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2010-10-08",
"namaOrtu": "I Komang Arnawa",
"nik": "5106120810100011",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.400.000/bulan",
"noHp": "081234567901",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "putu.arnawa011@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-012",
"namaLengkap": "Ni Komang Rini Listiani",
"nis": "2024012",
"kelas": "XI IPS",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-12-03",
"namaOrtu": "I Wayan Listia",
"nik": "5106124312080012",
"pekerjaanOrtu": "Buruh",
"penghasilan": "Rp 1.000.000/bulan",
"noHp": "081234567902",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "komang.rini012@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-013",
"namaLengkap": "I Ketut Wirawan Sumerta",
"nis": "2024013",
"kelas": "VIII",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2011-08-16",
"namaOrtu": "I Made Sumerta",
"nik": "5106121608110013",
"pekerjaanOrtu": "Kuli Bangunan",
"penghasilan": "Rp 1.700.000/bulan",
"noHp": "081234567903",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "ketut.wirawan013@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-014",
"namaLengkap": "Ni Nyoman Wahyuni Damayanti",
"nis": "2024014",
"kelas": "X IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Gianyar",
"tanggalLahir": "2009-03-28",
"namaOrtu": "Ni Ketut Wahyuni",
"nik": "5106126803090014",
"pekerjaanOrtu": "Penjahit",
"penghasilan": "Rp 1.500.000/bulan",
"noHp": "081234567904",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "nyoman.wahyuni014@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-015",
"namaLengkap": "I Made Prabawa Artana",
"nis": "2024015",
"kelas": "XII IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-05-09",
"namaOrtu": "I Nyoman Artana",
"nik": "5106120905070015",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.200.000/bulan",
"noHp": "081234567905",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "made.prabawa015@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-016",
"namaLengkap": "Ni Gede Putri Sukma",
"nis": "2024016",
"kelas": "VII",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2012-07-17",
"namaOrtu": "I Wayan Sukma",
"nik": "5106125707120016",
"pekerjaanOrtu": "Buruh Tani",
"penghasilan": "Rp 950.000/bulan",
"noHp": "081234567906",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "gede.putri016@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-017",
"namaLengkap": "I Wayan Adnyana Gunawan",
"nis": "2024017",
"kelas": "XI IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-01-22",
"namaOrtu": "I Ketut Gunawan",
"nik": "5106122201080017",
"pekerjaanOrtu": "Pedagang",
"penghasilan": "Rp 2.000.000/bulan",
"noHp": "081234567907",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.adnyana017@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-018",
"namaLengkap": "Ni Putu Sartini Wati",
"nis": "2024018",
"kelas": "IX",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Tabanan",
"tanggalLahir": "2010-09-11",
"namaOrtu": "I Made Wati",
"nik": "5106125109100018",
"pekerjaanOrtu": "Peternak",
"penghasilan": "Rp 1.600.000/bulan",
"noHp": "081234567908",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "putu.sartini018@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-019",
"namaLengkap": "I Komang Arta Wira",
"nis": "2024019",
"kelas": "X IPS",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-11-04",
"namaOrtu": "I Nyoman Arta",
"nik": "5106120411090019",
"pekerjaanOrtu": "Tukang Ojek",
"penghasilan": "Rp 1.800.000/bulan",
"noHp": "081234567909",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "komang.arta019@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-020",
"namaLengkap": "Ni Made Yani Astawa",
"nis": "2024020",
"kelas": "XII IPS",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-02-19",
"namaOrtu": "I Wayan Astawa",
"nik": "5106125902070020",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.300.000/bulan",
"noHp": "081234567910",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "made.yani020@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-021",
"namaLengkap": "I Nyoman Suharta Antara",
"nis": "2024021",
"kelas": "VIII",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2011-06-07",
"namaOrtu": "I Ketut Antara",
"nik": "5106120706110021",
"pekerjaanOrtu": "Buruh",
"penghasilan": "Rp 1.100.000/bulan",
"noHp": "081234567911",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "nyoman.suharta021@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-022",
"namaLengkap": "Ni Ketut Suryani Arnawa",
"nis": "2024022",
"kelas": "XI IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-04-13",
"namaOrtu": "Ni Made Arnawa",
"nik": "5106125304080022",
"pekerjaanOrtu": "Ibu Rumah Tangga",
"penghasilan": "Rp 800.000/bulan",
"noHp": "081234567912",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "ketut.suryani022@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-023",
"namaLengkap": "I Gede Sudirman Wirawan",
"nis": "2024023",
"kelas": "IX",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Klungkung",
"tanggalLahir": "2010-12-25",
"namaOrtu": "I Wayan Wirawan",
"nik": "5106122512100023",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.400.000/bulan",
"noHp": "081234567913",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "gede.sudirman023@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-024",
"namaLengkap": "Ni Wayan Padmini Sutari",
"nis": "2024024",
"kelas": "X IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-08-31",
"namaOrtu": "I Nyoman Sutari",
"nik": "5106127108090024",
"pekerjaanOrtu": "Pedagang Sayur",
"penghasilan": "Rp 1.700.000/bulan",
"noHp": "081234567914",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.padmini024@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-025",
"namaLengkap": "I Putu Yudha Saputra",
"nis": "2024025",
"kelas": "XII IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-10-16",
"namaOrtu": "I Made Saputra",
"nik": "5106121610070025",
"pekerjaanOrtu": "Buruh Pabrik",
"penghasilan": "Rp 2.100.000/bulan",
"noHp": "081234567915",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "putu.yudha025@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-026",
"namaLengkap": "Ni Komang Ayu Widiastuti",
"nis": "2024026",
"kelas": "VII",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2012-05-02",
"namaOrtu": "I Wayan Widiastuti",
"nik": "5106124205120026",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.000.000/bulan",
"noHp": "081234567916",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "komang.ayu026@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-027",
"namaLengkap": "I Made Bayu Permana",
"nis": "2024027",
"kelas": "XI IPS",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-07-27",
"namaOrtu": "I Nyoman Permana",
"nik": "5106122707080027",
"pekerjaanOrtu": "Tukang Kayu",
"penghasilan": "Rp 2.300.000/bulan",
"noHp": "081234567917",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "made.bayu027@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-028",
"namaLengkap": "Ni Nyoman Diah Permatasari",
"nis": "2024028",
"kelas": "X IPS",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-01-06",
"namaOrtu": "I Ketut Permata",
"nik": "5106124601090028",
"pekerjaanOrtu": "Buruh Tani",
"penghasilan": "Rp 1.100.000/bulan",
"noHp": "081234567918",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "nyoman.diah028@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-029",
"namaLengkap": "I Ketut Dipa Darma",
"nis": "2024029",
"kelas": "VIII",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2011-04-14",
"namaOrtu": "I Made Darma",
"nik": "5106121404110029",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.300.000/bulan",
"noHp": "081234567919",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "ketut.dipa029@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-030",
"namaLengkap": "Ni Putu Ratna Sari",
"nis": "2024030",
"kelas": "XII IPS",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-09-23",
"namaOrtu": "I Wayan Ratna",
"nik": "5106126309070030",
"pekerjaanOrtu": "Ibu Rumah Tangga",
"penghasilan": "Rp 850.000/bulan",
"noHp": "081234567920",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "putu.ratna030@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-031",
"namaLengkap": "I Wayan Eka Prasetya",
"nis": "2024031",
"kelas": "IX",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2010-02-28",
"namaOrtu": "I Nyoman Prasetya",
"nik": "5106122802100031",
"pekerjaanOrtu": "Nelayan",
"penghasilan": "Rp 1.900.000/bulan",
"noHp": "081234567921",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.eka031@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-032",
"namaLengkap": "Ni Made Sintya Dewi",
"nis": "2024032",
"kelas": "X IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-06-19",
"namaOrtu": "I Ketut Sintya",
"nik": "5106125906090032",
"pekerjaanOrtu": "Pedagang",
"penghasilan": "Rp 1.600.000/bulan",
"noHp": "081234567922",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "made.sintya032@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-033",
"namaLengkap": "I Komang Dika Pranata",
"nis": "2024033",
"kelas": "XI IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-10-11",
"namaOrtu": "I Wayan Pranata",
"nik": "5106121110080033",
"pekerjaanOrtu": "Kuli Bangunan",
"penghasilan": "Rp 2.000.000/bulan",
"noHp": "081234567923",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "komang.dika033@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-034",
"namaLengkap": "Ni Gede Wulandari Nirmala",
"nis": "2024034",
"kelas": "VII",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2012-03-07",
"namaOrtu": "Ni Made Nirmala",
"nik": "5106124703120034",
"pekerjaanOrtu": "Buruh",
"penghasilan": "Rp 1.050.000/bulan",
"noHp": "081234567924",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "gede.wulandari034@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-035",
"namaLengkap": "I Nyoman Rian Kusuma",
"nis": "2024035",
"kelas": "XII IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-08-04",
"namaOrtu": "I Ketut Kusuma",
"nik": "5106120408070035",
"pekerjaanOrtu": "Sopir",
"penghasilan": "Rp 2.400.000/bulan",
"noHp": "081234567925",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "nyoman.rian035@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-036",
"namaLengkap": "Ni Ketut Mira Astuti",
"nis": "2024036",
"kelas": "VIII",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2011-11-29",
"namaOrtu": "I Made Astuti",
"nik": "5106126911110036",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.250.000/bulan",
"noHp": "081234567926",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "ketut.mira036@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-037",
"namaLengkap": "I Putu Galih Satriana",
"nis": "2024037",
"kelas": "X IPS",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Bangli",
"tanggalLahir": "2009-04-18",
"namaOrtu": "I Wayan Satriana",
"nik": "5106121804090037",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.350.000/bulan",
"noHp": "081234567927",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "putu.galih037@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-038",
"namaLengkap": "Ni Wayan Eka Pratiwi",
"nis": "2024038",
"kelas": "XI IPS",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-06-03",
"namaOrtu": "Ni Ketut Pratiwi",
"nik": "5106124306080038",
"pekerjaanOrtu": "Penjual Canang",
"penghasilan": "Rp 1.200.000/bulan",
"noHp": "081234567928",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.eka038@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-039",
"namaLengkap": "I Made Wahyu Artha",
"nis": "2024039",
"kelas": "IX",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2010-08-21",
"namaOrtu": "I Nyoman Artha",
"nik": "5106122108100039",
"pekerjaanOrtu": "Tukang Batu",
"penghasilan": "Rp 1.800.000/bulan",
"noHp": "081234567929",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "made.wahyu039@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-040",
"namaLengkap": "Ni Putu Dwi Cahyani",
"nis": "2024040",
"kelas": "X IPA",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-12-10",
"namaOrtu": "I Wayan Cahyani",
"nik": "5106125012090040",
"pekerjaanOrtu": "Buruh Tani",
"penghasilan": "Rp 1.100.000/bulan",
"noHp": "081234567930",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "putu.dwi040@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-041",
"namaLengkap": "I Gede Arsa Wijaya",
"nis": "2024041",
"kelas": "XII IPS",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kauh, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2007-03-14",
"namaOrtu": "I Ketut Wijaya",
"nik": "5106121403070041",
"pekerjaanOrtu": "Pedagang",
"penghasilan": "Rp 2.200.000/bulan",
"noHp": "081234567931",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kauh, Desa Darmasaba, Abiansemal, Badung",
"email": "gede.arsa041@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "L"
},
{
"id": "cmk-beasiswa-042",
"namaLengkap": "Ni Komang Trisna Yanti",
"nis": "2024042",
"kelas": "VII",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kangin, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2012-09-06",
"namaOrtu": "Ni Made Yanti",
"nik": "5106124609120042",
"pekerjaanOrtu": "Petani",
"penghasilan": "Rp 1.050.000/bulan",
"noHp": "081234567932",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kangin, Desa Darmasaba, Abiansemal, Badung",
"email": "komang.trisna042@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-043",
"namaLengkap": "I Wayan Surya Negara",
"nis": "2024043",
"kelas": "XI IPA",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Tengah, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2008-05-26",
"namaOrtu": "I Nyoman Negara",
"nik": "5106122605080043",
"pekerjaanOrtu": "Buruh",
"penghasilan": "Rp 1.500.000/bulan",
"noHp": "081234567933",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Tengah, Desa Darmasaba, Abiansemal, Badung",
"email": "wayan.surya043@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
},
{
"id": "cmk-beasiswa-044",
"namaLengkap": "Ni Made Juniari Santi",
"nis": "2024044",
"kelas": "IX",
"jenisKelamin": "PEREMPUAN",
"alamatDomisili": "Banjar Adat Kaja, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2010-07-13",
"namaOrtu": "I Wayan Santi",
"nik": "5106125307100044",
"pekerjaanOrtu": "Ibu Rumah Tangga",
"penghasilan": "Rp 900.000/bulan",
"noHp": "081234567934",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kaja, Desa Darmasaba, Abiansemal, Badung",
"email": "made.juniari044@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "S"
},
{
"id": "cmk-beasiswa-045",
"namaLengkap": "I Nyoman Krisna Mahardika",
"nis": "2024045",
"kelas": "X IPS",
"jenisKelamin": "LAKI_LAKI",
"alamatDomisili": "Banjar Adat Kelod, Desa Darmasaba",
"tempatLahir": "Badung",
"tanggalLahir": "2009-10-01",
"namaOrtu": "I Made Mahardika",
"nik": "5106120110090045",
"pekerjaanOrtu": "Kuli Bangunan",
"penghasilan": "Rp 2.000.000/bulan",
"noHp": "081234567935",
"kewarganegaraan": "Indonesia",
"agama": "HINDU",
"alamatKTP": "Banjar Adat Kelod, Desa Darmasaba, Abiansemal, Badung",
"email": "nyoman.krisna045@email.com",
"statusPernikahan": "BELUM_MENIKAH",
"ukuranBaju": "M"
}

View File

@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "RingkasanKesehatanDesa"
ADD COLUMN "imunisasiLengkapPct" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "pemeriksaanRutinPct" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "giziBaikPct" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "targetStuntingPct" INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,59 @@
-- CreateEnum
CREATE TYPE "IbuHamilStatus" AS ENUM ('AKTIF', 'MELAHIRKAN', 'KEGUGURAN', 'NONAKTIF');
-- CreateEnum
CREATE TYPE "JenisKelaminBalita" AS ENUM ('L', 'P');
-- CreateEnum
CREATE TYPE "StatusStunting" AS ENUM ('NORMAL', 'ALERT', 'STUNTING');
-- CreateTable
CREATE TABLE "IbuHamil" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"nik" TEXT,
"usiaKehamilan" INTEGER NOT NULL DEFAULT 0,
"hpht" TIMESTAMP(3),
"taksiranLahir" TIMESTAMP(3),
"alamat" TEXT,
"noHp" TEXT,
"catatan" TEXT,
"posyanduId" TEXT,
"status" "IbuHamilStatus" NOT NULL DEFAULT 'AKTIF',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "IbuHamil_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Balita" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"nik" TEXT,
"tanggalLahir" TIMESTAMP(3) NOT NULL,
"jenisKelamin" "JenisKelaminBalita" NOT NULL,
"beratBadanKg" DOUBLE PRECISION,
"tinggiBadanCm" DOUBLE PRECISION,
"namaOrtu" TEXT,
"alamat" TEXT,
"noHpOrtu" TEXT,
"posyanduId" TEXT,
"imunisasiLengkap" BOOLEAN NOT NULL DEFAULT false,
"giziBaik" BOOLEAN NOT NULL DEFAULT true,
"pemeriksaanRutin" BOOLEAN NOT NULL DEFAULT true,
"statusStunting" "StatusStunting" NOT NULL DEFAULT 'NORMAL',
"catatan" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Balita_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "IbuHamil" ADD CONSTRAINT "IbuHamil_posyanduId_fkey" FOREIGN KEY ("posyanduId") REFERENCES "Posyandu"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Balita" ADD CONSTRAINT "Balita_posyanduId_fkey" FOREIGN KEY ("posyanduId") REFERENCES "Posyandu"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,9 @@
-- Drop redundant columns from RingkasanKesehatanDesa.
-- These values are auto-derived live from IbuHamil + Balita tables (see stats endpoint).
-- Only targetStuntingPct is a policy config that needs to be stored.
ALTER TABLE "RingkasanKesehatanDesa" DROP COLUMN "ibuHamilAkh";
ALTER TABLE "RingkasanKesehatanDesa" DROP COLUMN "balitaTerdaftar";
ALTER TABLE "RingkasanKesehatanDesa" DROP COLUMN "alertStunting";
ALTER TABLE "RingkasanKesehatanDesa" DROP COLUMN "imunisasiLengkapPct";
ALTER TABLE "RingkasanKesehatanDesa" DROP COLUMN "pemeriksaanRutinPct";
ALTER TABLE "RingkasanKesehatanDesa" DROP COLUMN "giziBaikPct";

View File

@@ -0,0 +1,22 @@
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriProdukId_fkey";
-- AlterTable
ALTER TABLE "KategoriProdukUmkm" ALTER COLUMN "updatedAt" DROP DEFAULT;
-- CreateTable
CREATE TABLE "EventBudaya" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"tanggal" TIMESTAMP(3) NOT NULL,
"lokasi" TEXT NOT NULL,
"deskripsi" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "EventBudaya_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProdukUmkm"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1159,6 +1159,8 @@ model Posyandu {
jadwalPelayanan String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
ibuHamil IbuHamil[]
balita Balita[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -2481,14 +2483,85 @@ model BeasiswaConfig {
isActive Boolean @default(true)
}
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
model RingkasanKesehatanDesa {
id String @id @default(cuid())
ibuHamilAkh Int @default(0)
balitaTerdaftar Int @default(0)
alertStunting Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
// ========================================= IBU HAMIL ========================================= //
enum IbuHamilStatus {
AKTIF
MELAHIRKAN
KEGUGURAN
NONAKTIF
}
model IbuHamil {
id String @id @default(cuid())
nama String
nik String?
usiaKehamilan Int @default(0)
hpht DateTime?
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)
}
// ========================================= BALITA ========================================= //
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)
}
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
model RingkasanKesehatanDesa {
id String @id @default(cuid())
targetStuntingPct Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
}
// ========================================= EVENT BUDAYA ========================================= //
model EventBudaya {
id String @id @default(cuid())
nama String
tanggal DateTime
lokasi String
deskripsi String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
}

View File

@@ -3,6 +3,7 @@
import prisma from "@/lib/prisma";
import { seedBerita } from "./_seeder_list/desa/berita/seed_berita";
import { seedKegiatanDesa } from "./_seeder_list/desa/seed_kegiatan_desa";
import { seedEventBudaya } from "./_seeder_list/desa/event-budaya/seed_event_budaya";
import { seedFoto } from "./_seeder_list/desa/gallery/foto/seed_foto";
import { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video";
import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan";
@@ -48,6 +49,8 @@ import { seedPuskesmas } from "./_seeder_list/kesehatan/puskesmas/seed_puskesmas
import { seedGrafikKepuasan } from "./_seeder_list/kesehatan/seed_grafik_kepuasan";
import { seedKelahiranKematian } from "./_seeder_list/kesehatan/seed_kelahiran_kematian";
import { seedRingkasanKesehatan } from "./_seeder_list/kesehatan/seed_ringkasan_kesehatan";
import { seedIbuHamil } from "./_seeder_list/kesehatan/seed_ibu_hamil";
import { seedBalita } from "./_seeder_list/kesehatan/seed_balita";
import { seedDesaAntiKorupsi } from "./_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi";
import { seedPrestasiDesa } from "./_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa";
import { seedMediaSosial } from "./_seeder_list/landing-page/profil_landing_page/seed_media_sosial";
@@ -244,6 +247,10 @@ import seedAssets from "./seed_assets";
// // ==================== SUBMENU POSYANDU =========================
await seedPosyandu();
// // ==================== SUBMENU IBU HAMIL + BALITA =========================
await seedIbuHamil();
await seedBalita();
// // ==================== SUBMENU PUSKESMAS =========================
await seedPuskesmas();
@@ -386,6 +393,7 @@ import seedAssets from "./seed_assets";
// ===== SOSIAL DASHBOARD =====
await seedRingkasanKesehatan();
await seedKegiatanDesa();
await seedEventBudaya();
// ===== DESA =====
await seedMusikDesa();

View File

@@ -0,0 +1,211 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama event harus diisi"),
tanggal: z.string().min(1, "Tanggal harus diisi"),
lokasi: z.string().min(1, "Lokasi harus diisi"),
deskripsi: z.string().optional(),
});
const defaultForm = {
nama: "",
tanggal: "",
lokasi: "",
deskripsi: "",
};
const eventBudayaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(eventBudayaState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
eventBudayaState.create.loading = true;
const res = await ApiFetch.api.desa["eventbudaya"]["create"].post(
eventBudayaState.create.form
);
if (res.status === 200 && res.data?.success) {
eventBudayaState.findMany.load();
toast.success("Event budaya berhasil disimpan!");
eventBudayaState.create.form = { ...defaultForm };
return true;
}
toast.error(res.data?.message || "Gagal menyimpan event budaya");
return false;
} catch (error) {
console.error(error);
toast.error("Gagal menyimpan event budaya");
return false;
} finally {
eventBudayaState.create.loading = false;
}
},
},
findMany: {
data: null as Prisma.EventBudayaGetPayload<object>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
eventBudayaState.findMany.loading = true;
eventBudayaState.findMany.page = page;
eventBudayaState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa["eventbudaya"]["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
eventBudayaState.findMany.data = res.data.data ?? [];
eventBudayaState.findMany.total = res.data.total ?? 0;
eventBudayaState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
eventBudayaState.findMany.data = [];
eventBudayaState.findMany.total = 0;
eventBudayaState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading event budaya:", error);
eventBudayaState.findMany.data = [];
} finally {
eventBudayaState.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.EventBudayaGetPayload<object> | null,
loading: false,
async load(id: string) {
if (!id) return;
this.loading = true;
try {
const res = await fetch(`/api/desa/eventbudaya/${id}`);
if (res.ok) {
const result = await res.json();
eventBudayaState.findUnique.data = result.data ?? null;
}
} catch (error) {
console.error("Error fetching event budaya:", error);
} finally {
this.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) return;
try {
eventBudayaState.edit.loading = true;
const res = await fetch(`/api/desa/eventbudaya/${id}`);
const result = await res.json();
if (result?.success) {
const data = result.data;
eventBudayaState.edit.id = data.id;
eventBudayaState.edit.form = {
nama: data.nama,
tanggal: data.tanggal
? new Date(data.tanggal).toISOString().split("T")[0]
: "",
lokasi: data.lokasi,
deskripsi: data.deskripsi ?? "",
};
}
} catch (error) {
console.error("Error loading event budaya for edit:", error);
} finally {
eventBudayaState.edit.loading = false;
}
},
async save() {
const cek = templateForm.safeParse(eventBudayaState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
eventBudayaState.edit.loading = true;
const res = await fetch(
`/api/desa/eventbudaya/${eventBudayaState.edit.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(eventBudayaState.edit.form),
}
);
const result = await res.json();
if (result.success) {
toast.success("Event budaya berhasil diupdate");
eventBudayaState.findMany.load();
return true;
}
toast.error(result.message);
return false;
} catch (error) {
console.error(error);
return false;
} finally {
eventBudayaState.edit.loading = false;
}
},
reset() {
eventBudayaState.edit.id = "";
eventBudayaState.edit.form = { ...defaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
eventBudayaState.delete.loading = true;
const res = await fetch(`/api/desa/eventbudaya/del/${id}`, {
method: "DELETE",
});
const result = await res.json();
if (res.ok && result?.success) {
toast.success(result.message || "Event budaya berhasil dihapus");
await eventBudayaState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus event budaya");
}
} catch (error) {
console.error(error);
toast.error("Gagal menghapus event budaya");
} finally {
eventBudayaState.delete.loading = false;
}
},
},
});
export default eventBudayaState;

View File

@@ -0,0 +1,218 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { JenisKelaminBalita, Prisma, StatusStunting } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const formSchema = z.object({
nama: z.string().min(1, { message: "Nama wajib diisi" }),
nik: z.string().optional(),
tanggalLahir: z.string().min(1, { message: "Tanggal lahir wajib diisi" }),
jenisKelamin: z.enum(["L", "P"]),
beratBadanKg: z.number().optional(),
tinggiBadanCm: z.number().optional(),
namaOrtu: z.string().optional(),
alamat: z.string().optional(),
noHpOrtu: z.string().optional(),
posyanduId: z.string().optional(),
imunisasiLengkap: z.boolean(),
giziBaik: z.boolean(),
pemeriksaanRutin: z.boolean(),
statusStunting: z.enum(["NORMAL", "ALERT", "STUNTING"]),
catatan: z.string().optional(),
});
const defaultForm = {
nama: "",
nik: "",
tanggalLahir: "",
jenisKelamin: "L" as JenisKelaminBalita,
beratBadanKg: undefined as number | undefined,
tinggiBadanCm: undefined as number | undefined,
namaOrtu: "",
alamat: "",
noHpOrtu: "",
posyanduId: "",
imunisasiLengkap: false,
giziBaik: true,
pemeriksaanRutin: true,
statusStunting: "NORMAL" as StatusStunting,
catatan: "",
};
const balitaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = formSchema.safeParse(balitaState.create.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join(", ");
toast.error(err);
return false;
}
try {
balitaState.create.loading = true;
const res = await fetch("/api/kesehatan/balita/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cek.data),
});
const result = await res.json();
if (result.success) {
toast.success("Balita berhasil ditambahkan");
balitaState.findMany.load();
return true;
}
toast.error(result.message || "Gagal menambahkan data");
return false;
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan");
return false;
} finally {
balitaState.create.loading = false;
}
},
reset() {
balitaState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.BalitaGetPayload<{
include: { posyandu: { select: { id: true; name: true } } };
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
statusStuntingFilter: "",
async load(page = 1, limit = 10, search = "", statusStuntingFilter = "") {
balitaState.findMany.loading = true;
balitaState.findMany.page = page;
balitaState.findMany.search = search;
balitaState.findMany.statusStuntingFilter = statusStuntingFilter;
try {
const query = new URLSearchParams({ page: String(page), limit: String(limit) });
if (search) query.set("search", search);
if (statusStuntingFilter) query.set("statusStunting", statusStuntingFilter);
const res = await fetch(`/api/kesehatan/balita/find-many?${query}`);
const result = await res.json();
if (result.success) {
balitaState.findMany.data = result.data ?? [];
balitaState.findMany.totalPages = result.totalPages ?? 1;
balitaState.findMany.total = result.total ?? 0;
} else {
balitaState.findMany.data = [];
}
} catch (e) {
console.error("balitaFindMany error:", e);
balitaState.findMany.data = [];
} finally {
balitaState.findMany.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/balita/${id}`);
const result = await res.json();
if (result.success) {
const d = result.data;
balitaState.edit.id = d.id;
balitaState.edit.form = {
nama: d.nama,
nik: d.nik ?? "",
tanggalLahir: d.tanggalLahir ? d.tanggalLahir.slice(0, 10) : "",
jenisKelamin: d.jenisKelamin,
beratBadanKg: d.beratBadanKg ?? undefined,
tinggiBadanCm: d.tinggiBadanCm ?? undefined,
namaOrtu: d.namaOrtu ?? "",
alamat: d.alamat ?? "",
noHpOrtu: d.noHpOrtu ?? "",
posyanduId: d.posyanduId ?? "",
imunisasiLengkap: d.imunisasiLengkap,
giziBaik: d.giziBaik,
pemeriksaanRutin: d.pemeriksaanRutin,
statusStunting: d.statusStunting,
catatan: d.catatan ?? "",
};
return d;
}
toast.error("Gagal memuat data");
return null;
} catch (e) {
console.error(e);
toast.error("Gagal memuat data");
return null;
}
},
async update() {
const cek = formSchema.safeParse(balitaState.edit.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join(", ");
toast.error(err);
return false;
}
try {
balitaState.edit.loading = true;
const res = await fetch(`/api/kesehatan/balita/${balitaState.edit.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cek.data),
});
const result = await res.json();
if (result.success) {
toast.success("Data balita berhasil diperbarui");
balitaState.findMany.load();
return true;
}
toast.error(result.message || "Gagal memperbarui data");
return false;
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan");
return false;
} finally {
balitaState.edit.loading = false;
}
},
reset() {
balitaState.edit.id = "";
balitaState.edit.form = { ...defaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
try {
balitaState.delete.loading = true;
const res = await fetch(`/api/kesehatan/balita/del/${id}`, { method: "DELETE" });
const result = await res.json();
if (result.success) {
toast.success(result.message || "Data berhasil dihapus");
await balitaState.findMany.load();
} else {
toast.error(result.message || "Gagal menghapus data");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
balitaState.delete.loading = false;
}
},
},
});
export default balitaState;

View File

@@ -0,0 +1,203 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { IbuHamilStatus, Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const formSchema = z.object({
nama: z.string().min(1, { message: "Nama wajib diisi" }),
nik: z.string().optional(),
usiaKehamilan: z.number().min(0),
hpht: z.string().optional(),
taksiranLahir: z.string().optional(),
alamat: z.string().optional(),
noHp: z.string().optional(),
catatan: z.string().optional(),
posyanduId: z.string().optional(),
status: z.enum(["AKTIF", "MELAHIRKAN", "KEGUGURAN", "NONAKTIF"]),
});
const defaultForm = {
nama: "",
nik: "",
usiaKehamilan: 0,
hpht: "",
taksiranLahir: "",
alamat: "",
noHp: "",
catatan: "",
posyanduId: "",
status: "AKTIF" as IbuHamilStatus,
};
const ibuHamilState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = formSchema.safeParse(ibuHamilState.create.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join(", ");
toast.error(err);
return false;
}
try {
ibuHamilState.create.loading = true;
const res = await fetch("/api/kesehatan/ibuhamil/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cek.data),
});
const result = await res.json();
if (result.success) {
toast.success("Ibu hamil berhasil ditambahkan");
ibuHamilState.findMany.load();
return true;
}
toast.error(result.message || "Gagal menambahkan data");
return false;
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan");
return false;
} finally {
ibuHamilState.create.loading = false;
}
},
reset() {
ibuHamilState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.IbuHamilGetPayload<{
include: { posyandu: { select: { id: true; name: true } } };
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
statusFilter: "",
async load(page = 1, limit = 10, search = "", statusFilter = "") {
ibuHamilState.findMany.loading = true;
ibuHamilState.findMany.page = page;
ibuHamilState.findMany.search = search;
ibuHamilState.findMany.statusFilter = statusFilter;
try {
const query = new URLSearchParams({ page: String(page), limit: String(limit) });
if (search) query.set("search", search);
if (statusFilter) query.set("status", statusFilter);
const res = await fetch(`/api/kesehatan/ibuhamil/find-many?${query}`);
const result = await res.json();
if (result.success) {
ibuHamilState.findMany.data = result.data ?? [];
ibuHamilState.findMany.totalPages = result.totalPages ?? 1;
ibuHamilState.findMany.total = result.total ?? 0;
} else {
ibuHamilState.findMany.data = [];
}
} catch (e) {
console.error("ibuHamilFindMany error:", e);
ibuHamilState.findMany.data = [];
} finally {
ibuHamilState.findMany.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/ibuhamil/${id}`);
const result = await res.json();
if (result.success) {
const d = result.data;
ibuHamilState.edit.id = d.id;
ibuHamilState.edit.form = {
nama: d.nama,
nik: d.nik ?? "",
usiaKehamilan: d.usiaKehamilan,
hpht: d.hpht ? d.hpht.slice(0, 10) : "",
taksiranLahir: d.taksiranLahir ? d.taksiranLahir.slice(0, 10) : "",
alamat: d.alamat ?? "",
noHp: d.noHp ?? "",
catatan: d.catatan ?? "",
posyanduId: d.posyanduId ?? "",
status: d.status,
};
return d;
}
toast.error("Gagal memuat data");
return null;
} catch (e) {
console.error(e);
toast.error("Gagal memuat data");
return null;
}
},
async update() {
const cek = formSchema.safeParse(ibuHamilState.edit.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join(", ");
toast.error(err);
return false;
}
try {
ibuHamilState.edit.loading = true;
const res = await fetch(`/api/kesehatan/ibuhamil/${ibuHamilState.edit.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cek.data),
});
const result = await res.json();
if (result.success) {
toast.success("Ibu hamil berhasil diperbarui");
ibuHamilState.findMany.load();
return true;
}
toast.error(result.message || "Gagal memperbarui data");
return false;
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan");
return false;
} finally {
ibuHamilState.edit.loading = false;
}
},
reset() {
ibuHamilState.edit.id = "";
ibuHamilState.edit.form = { ...defaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
try {
ibuHamilState.delete.loading = true;
const res = await fetch(`/api/kesehatan/ibuhamil/del/${id}`, { method: "DELETE" });
const result = await res.json();
if (result.success) {
toast.success(result.message || "Data berhasil dihapus");
await ibuHamilState.findMany.load();
} else {
toast.error(result.message || "Gagal menghapus data");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
ibuHamilState.delete.loading = false;
}
},
},
});
export default ibuHamilState;

View File

@@ -0,0 +1,84 @@
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
type StatsData = {
ibuHamilAktif: number;
balitaTerdaftar: number;
alertStunting: number;
imunisasiLengkapPct: number;
pemeriksaanRutinPct: number;
giziBaikPct: number;
targetStuntingPct: number;
};
const intPct = z
.number({ invalid_type_error: "Harus berupa angka" })
.int({ message: "Harus bilangan bulat" })
.min(0, { message: "Minimal 0" })
.max(100, { message: "Maksimal 100" });
const ringkasanKesehatanState = proxy({
findStats: {
data: null as StatsData | null,
loading: false,
async load() {
try {
ringkasanKesehatanState.findStats.loading = true;
const res = await fetch(`/api/kesehatan/ringkasankesehatan/stats`);
if (res.ok) {
const result = await res.json();
ringkasanKesehatanState.findStats.data = result?.data ?? null;
if (result?.data) {
ringkasanKesehatanState.update.form.targetStuntingPct =
result.data.targetStuntingPct;
}
} else {
ringkasanKesehatanState.findStats.data = null;
}
} catch (error) {
console.error("Error fetching ringkasan stats:", error);
ringkasanKesehatanState.findStats.data = null;
} finally {
ringkasanKesehatanState.findStats.loading = false;
}
},
},
update: {
form: { targetStuntingPct: 0 },
loading: false,
async submitTarget() {
const pct = ringkasanKesehatanState.update.form.targetStuntingPct;
const cek = intPct.safeParse(pct);
if (!cek.success) {
toast.error("Target stunting harus 0-100");
return false;
}
try {
ringkasanKesehatanState.update.loading = true;
const response = await fetch(`/api/kesehatan/ringkasankesehatan/update`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetStuntingPct: pct }),
});
const result = await response.json();
if (result.success) {
toast.success("Target stunting berhasil disimpan");
await ringkasanKesehatanState.findStats.load();
return true;
}
toast.error(result.message || "Gagal menyimpan");
return false;
} catch (error) {
console.error("Error saving target stunting:", error);
toast.error("Gagal menyimpan target stunting");
return false;
} finally {
ringkasanKesehatanState.update.loading = false;
}
},
},
});
export default ringkasanKesehatanState;

View File

@@ -0,0 +1,91 @@
import { toast } from "react-toastify";
import { proxy } from "valtio";
type StatsBeasiswa = {
jumlahPenerima: number;
danaTersalurkan: string;
tahunAjaran: string;
};
type BeasiswaConfig = {
id: string;
tahunAjaran: string;
danaTersalurkan: string;
};
const ringkasanBeasiswaState = proxy({
findStats: {
data: null as StatsBeasiswa | null,
loading: false,
async load() {
try {
ringkasanBeasiswaState.findStats.loading = true;
const res = await fetch(`/api/pendidikan/beasiswa/ringkasan/stats`);
if (res.ok) {
const result = await res.json();
ringkasanBeasiswaState.findStats.data = result?.data ?? null;
} else {
ringkasanBeasiswaState.findStats.data = null;
}
} catch (error) {
console.error("Error fetching ringkasan beasiswa:", error);
ringkasanBeasiswaState.findStats.data = null;
} finally {
ringkasanBeasiswaState.findStats.loading = false;
}
},
},
beasiswaConfig: {
data: null as BeasiswaConfig | null,
loading: false,
async find() {
try {
ringkasanBeasiswaState.beasiswaConfig.loading = true;
const res = await fetch(`/api/pendidikan/beasiswa/beasiswaconfig/find`);
if (res.ok) {
const result = await res.json();
ringkasanBeasiswaState.beasiswaConfig.data = result?.data ?? null;
} else {
ringkasanBeasiswaState.beasiswaConfig.data = null;
}
} catch (error) {
console.error("Error fetching beasiswa config:", error);
ringkasanBeasiswaState.beasiswaConfig.data = null;
} finally {
ringkasanBeasiswaState.beasiswaConfig.loading = false;
}
},
update: {
loading: false,
async submit(tahunAjaran: string, danaTersalurkan: string) {
try {
ringkasanBeasiswaState.beasiswaConfig.update.loading = true;
const res = await fetch(`/api/pendidikan/beasiswa/beasiswaconfig/update`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tahunAjaran, danaTersalurkan }),
});
const result = await res.json();
if (result.success) {
toast.success("Konfigurasi beasiswa berhasil disimpan");
await ringkasanBeasiswaState.beasiswaConfig.find();
await ringkasanBeasiswaState.findStats.load();
return true;
}
toast.error(result.message || "Gagal menyimpan konfigurasi");
return false;
} catch (error) {
console.error("Error updating beasiswa config:", error);
toast.error("Gagal menyimpan konfigurasi beasiswa");
return false;
} finally {
ringkasanBeasiswaState.beasiswaConfig.update.loading = false;
}
},
},
},
});
export default ringkasanBeasiswaState;

View File

@@ -0,0 +1,35 @@
import { proxy } from "valtio";
type PerJenjang = { nama: string; jumlahSiswa: number };
type StatsPendidikan = {
perJenjang: PerJenjang[];
jumlahLembaga: number;
jumlahPengajar: number;
};
const ringkasanPendidikanState = proxy({
findStats: {
data: null as StatsPendidikan | null,
loading: false,
async load() {
try {
ringkasanPendidikanState.findStats.loading = true;
const res = await fetch(`/api/pendidikan/ringkasan/stats`);
if (res.ok) {
const result = await res.json();
ringkasanPendidikanState.findStats.data = result?.data ?? null;
} else {
ringkasanPendidikanState.findStats.data = null;
}
} catch (error) {
console.error("Error fetching ringkasan pendidikan:", error);
ringkasanPendidikanState.findStats.data = null;
} finally {
ringkasanPendidikanState.findStats.loading = false;
}
},
},
});
export default ringkasanPendidikanState;

View File

@@ -190,7 +190,7 @@ export default function Validasi() {
case 2:
return '/admin/landing-page/profil/program-inovasi';
case 3:
return '/admin/kesehatan/posyandu';
return '/admin/kesehatan/posyandu/list-posyandu';
case 4:
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
default:

View File

@@ -0,0 +1,110 @@
'use client';
import eventBudayaState from '@/app/admin/(dashboard)/_state/desa/eventBudaya';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditEventBudayaPage() {
const state = useProxy(eventBudayaState);
const router = useRouter();
const params = useParams();
const id = params.id as string;
useEffect(() => {
if (id) state.edit.load(id);
return () => state.edit.reset();
}, [id]);
const handleSave = async () => {
const ok = await state.edit.save();
if (ok) router.push('/admin/desa/event-budaya');
};
if (state.edit.loading && !state.edit.form.nama) {
return <Skeleton h={400} radius="md" />;
}
return (
<Box>
<Group mb="lg">
<ActionIcon
variant="light"
size="lg"
onClick={() => router.push('/admin/desa/event-budaya')}
>
<IconArrowBack size={18} />
</ActionIcon>
<Title order={4}>Edit Event Budaya</Title>
</Group>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack gap="md">
<TextInput
label="Nama Event"
placeholder="Contoh: Festival Budaya Desa"
required
value={state.edit.form.nama}
onChange={(e) => (state.edit.form.nama = e.currentTarget.value)}
/>
<TextInput
label="Tanggal"
type="date"
required
value={state.edit.form.tanggal}
onChange={(e) =>
(state.edit.form.tanggal = e.currentTarget.value)
}
/>
<TextInput
label="Lokasi"
placeholder="Contoh: Balai Desa Darmasaba"
required
value={state.edit.form.lokasi}
onChange={(e) => (state.edit.form.lokasi = e.currentTarget.value)}
/>
<Textarea
label="Deskripsi"
placeholder="Deskripsi singkat event (opsional)"
rows={4}
value={state.edit.form.deskripsi}
onChange={(e) =>
(state.edit.form.deskripsi = e.currentTarget.value)
}
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => router.push('/admin/desa/event-budaya')}
>
Batal
</Button>
<Button
color="blue"
loading={state.edit.loading}
onClick={handleSave}
>
Update
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditEventBudayaPage;

View File

@@ -0,0 +1,95 @@
'use client';
import eventBudayaState from '@/app/admin/(dashboard)/_state/desa/eventBudaya';
import colors from '@/con/colors';
import {
ActionIcon,
Box,
Button,
Group,
Paper,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateEventBudayaPage() {
const state = useProxy(eventBudayaState);
const router = useRouter();
const handleSubmit = async () => {
const ok = await state.create.create();
if (ok) router.push('/admin/desa/event-budaya');
};
return (
<Box>
<Group mb="lg">
<ActionIcon
variant="light"
size="lg"
onClick={() => router.push('/admin/desa/event-budaya')}
>
<IconArrowBack size={18} />
</ActionIcon>
<Title order={4}>Tambah Event Budaya</Title>
</Group>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack gap="md">
<TextInput
label="Nama Event"
placeholder="Contoh: Festival Budaya Desa"
required
value={state.create.form.nama}
onChange={(e) => (state.create.form.nama = e.currentTarget.value)}
/>
<TextInput
label="Tanggal"
type="date"
required
value={state.create.form.tanggal}
onChange={(e) => (state.create.form.tanggal = e.currentTarget.value)}
/>
<TextInput
label="Lokasi"
placeholder="Contoh: Balai Desa Darmasaba"
required
value={state.create.form.lokasi}
onChange={(e) => (state.create.form.lokasi = e.currentTarget.value)}
/>
<Textarea
label="Deskripsi"
placeholder="Deskripsi singkat event (opsional)"
rows={4}
value={state.create.form.deskripsi}
onChange={(e) =>
(state.create.form.deskripsi = e.currentTarget.value)
}
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => router.push('/admin/desa/event-budaya')}
>
Batal
</Button>
<Button
color="blue"
loading={state.create.loading}
onClick={handleSubmit}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateEventBudayaPage;

View File

@@ -0,0 +1,167 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import eventBudayaState from '@/app/admin/(dashboard)/_state/desa/eventBudaya';
import colors from '@/con/colors';
import {
ActionIcon,
Badge,
Box,
Button,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconCalendarEvent, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
function EventBudayaPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title="Event Budaya"
placeholder="Cari nama atau lokasi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListEventBudaya search={search} />
</Box>
);
}
function ListEventBudaya({ search }: { search: string }) {
const state = useProxy(eventBudayaState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 500);
const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
if (loading || !data) {
return (
<Stack py="md">
<Skeleton h={400} radius="md" />
</Stack>
);
}
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="lg">
<Title order={4}>List Event Budaya</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/event-budaya/create')}
>
Tambah Event
</Button>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh w="30%">Nama Event</TableTh>
<TableTh w="20%">Tanggal</TableTh>
<TableTh w="25%">Lokasi</TableTh>
<TableTh w="25%">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.length > 0 ? (
data.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Group gap="xs">
<IconCalendarEvent size={16} color="blue" />
<Text fz="sm" fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Group>
</TableTd>
<TableTd>
<Badge variant="light" color="indigo">
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Badge>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" truncate="end" lineClamp={1}>
{item.lokasi}
</Text>
</TableTd>
<TableTd>
<Group gap="xs">
<ActionIcon
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/event-budaya/${item.id}/edit`)
}
>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon
variant="light"
color="red"
loading={state.delete.loading}
onClick={() => state.delete.byId(item.id)}
>
<IconTrash size={16} />
</ActionIcon>
</Group>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Text ta="center" c="dimmed" py="xl">
Belum ada data event budaya
</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Group justify="center" mt="lg">
<Pagination
total={totalPages}
value={page}
onChange={(p) => load(p, 10, search)}
/>
</Group>
)}
</Paper>
</Box>
);
}
export default EventBudayaPage;

View File

@@ -0,0 +1,161 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconBabyCarriage, IconCategory, IconClipboard, IconClipboardText, IconGenderDemigirl, IconNews } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsPosyandu({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "List Posyandu",
value: "list_posyandu",
href: "/admin/kesehatan/posyandu/list-posyandu",
icon: <IconNews size={18} stroke={1.8} />
},
{
label: "Balita",
value: "balita",
href: "/admin/kesehatan/posyandu/balita",
icon: <IconBabyCarriage size={18} stroke={1.8} />
},
{
label: "Ibu Hamil",
value: "ibu_hamil",
href: "/admin/kesehatan/posyandu/ibu-hamil",
icon: <IconGenderDemigirl size={18} stroke={1.8} />
},
{
label: "Ringkasan Kesehatan",
value: "ringkasan_kesehatan",
href: "/admin/kesehatan/posyandu/ringkasan-kesehatan",
icon: <IconClipboardText size={18} stroke={1.8} />
}
];
const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars w="100%">
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%",
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsPosyandu;

View File

@@ -0,0 +1,187 @@
'use client';
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
import colors from '@/con/colors';
import {
Box,
Button,
Checkbox,
Group,
Loader,
NumberInput,
Paper,
Select,
SimpleGrid,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
export default function BalitaCreatePage() {
const router = useRouter();
const state = useProxy(balitaState);
const form = state.create.form;
const handleSubmit = async () => {
const ok = await balitaState.create.submit();
if (ok) {
balitaState.create.reset();
router.push('/admin/kesehatan/posyandu/balita');
}
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md" gap="sm">
<Button variant="subtle" onClick={() => router.back()} radius="md">
<IconArrowBack size={20} />
</Button>
<Title order={3} c="black">Tambah Balita</Title>
</Group>
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
<Stack gap="md">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
<TextInput
label="Nama Lengkap"
required
placeholder="Nama balita"
value={form.nama}
onChange={(e) => { balitaState.create.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { balitaState.create.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Tanggal Lahir"
required
type="date"
value={form.tanggalLahir}
onChange={(e) => { balitaState.create.form.tanggalLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
label="Jenis Kelamin"
required
data={[
{ value: 'L', label: 'Laki-laki' },
{ value: 'P', label: 'Perempuan' },
]}
value={form.jenisKelamin}
onChange={(v) => { if (v) balitaState.create.form.jenisKelamin = v as typeof form.jenisKelamin; }}
radius="md"
/>
<NumberInput
label="Berat Badan (kg)"
placeholder="0.0"
min={0}
decimalScale={1}
value={form.beratBadanKg ?? ''}
onChange={(v) => { balitaState.create.form.beratBadanKg = v === '' ? undefined : Number(v); }}
radius="md"
/>
<NumberInput
label="Tinggi Badan (cm)"
placeholder="0.0"
min={0}
decimalScale={1}
value={form.tinggiBadanCm ?? ''}
onChange={(v) => { balitaState.create.form.tinggiBadanCm = v === '' ? undefined : Number(v); }}
radius="md"
/>
<TextInput
label="Nama Orang Tua"
placeholder="Nama ayah/ibu"
value={form.namaOrtu}
onChange={(e) => { balitaState.create.form.namaOrtu = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="No. HP Orang Tua"
placeholder="08xx-xxxx-xxxx"
value={form.noHpOrtu}
onChange={(e) => { balitaState.create.form.noHpOrtu = e.currentTarget.value; }}
radius="md"
/>
<Select
label="Status Stunting"
required
data={[
{ value: 'NORMAL', label: 'Normal' },
{ value: 'ALERT', label: 'Alert (Berisiko)' },
{ value: 'STUNTING', label: 'Stunting' },
]}
value={form.statusStunting}
onChange={(v) => { if (v) balitaState.create.form.statusStunting = v as typeof form.statusStunting; }}
radius="md"
/>
</SimpleGrid>
<TextInput
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { balitaState.create.form.alamat = e.currentTarget.value; }}
radius="md"
/>
<Group gap="xl">
<Checkbox
label="Imunisasi Lengkap"
checked={form.imunisasiLengkap}
onChange={(e) => { balitaState.create.form.imunisasiLengkap = e.currentTarget.checked; }}
/>
<Checkbox
label="Gizi Baik"
checked={form.giziBaik}
onChange={(e) => { balitaState.create.form.giziBaik = e.currentTarget.checked; }}
/>
<Checkbox
label="Pemeriksaan Rutin"
checked={form.pemeriksaanRutin}
onChange={(e) => { balitaState.create.form.pemeriksaanRutin = e.currentTarget.checked; }}
/>
</Group>
<Textarea
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { balitaState.create.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>
<Group justify="right">
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
disabled={state.create.loading}
style={{
background: state.create.loading
? 'linear-gradient(135deg, #ccc, #eee)'
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
}}
>
{state.create.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,191 @@
'use client';
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
import colors from '@/con/colors';
import {
Box,
Button,
Checkbox,
Group,
Loader,
NumberInput,
Paper,
Select,
SimpleGrid,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
export default function BalitaEditPage() {
const router = useRouter();
const params = useParams();
const id = params.id as string;
const state = useProxy(balitaState);
const form = state.edit.form;
useEffect(() => {
if (id) balitaState.edit.load(id);
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const handleSubmit = async () => {
const ok = await balitaState.edit.update();
if (ok) router.push('/admin/kesehatan/posyandu/balita');
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md" gap="sm">
<Button variant="subtle" onClick={() => router.back()} radius="md">
<IconArrowBack size={20} />
</Button>
<Title order={3} c="black">Edit Balita</Title>
</Group>
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
<Stack gap="md">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
<TextInput
label="Nama Lengkap"
required
placeholder="Nama balita"
value={form.nama}
onChange={(e) => { balitaState.edit.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { balitaState.edit.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Tanggal Lahir"
required
type="date"
value={form.tanggalLahir}
onChange={(e) => { balitaState.edit.form.tanggalLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
label="Jenis Kelamin"
required
data={[
{ value: 'L', label: 'Laki-laki' },
{ value: 'P', label: 'Perempuan' },
]}
value={form.jenisKelamin}
onChange={(v) => { if (v) balitaState.edit.form.jenisKelamin = v as typeof form.jenisKelamin; }}
radius="md"
/>
<NumberInput
label="Berat Badan (kg)"
placeholder="0.0"
min={0}
decimalScale={1}
value={form.beratBadanKg ?? ''}
onChange={(v) => { balitaState.edit.form.beratBadanKg = v === '' ? undefined : Number(v); }}
radius="md"
/>
<NumberInput
label="Tinggi Badan (cm)"
placeholder="0.0"
min={0}
decimalScale={1}
value={form.tinggiBadanCm ?? ''}
onChange={(v) => { balitaState.edit.form.tinggiBadanCm = v === '' ? undefined : Number(v); }}
radius="md"
/>
<TextInput
label="Nama Orang Tua"
placeholder="Nama ayah/ibu"
value={form.namaOrtu}
onChange={(e) => { balitaState.edit.form.namaOrtu = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="No. HP Orang Tua"
placeholder="08xx-xxxx-xxxx"
value={form.noHpOrtu}
onChange={(e) => { balitaState.edit.form.noHpOrtu = e.currentTarget.value; }}
radius="md"
/>
<Select
label="Status Stunting"
required
data={[
{ value: 'NORMAL', label: 'Normal' },
{ value: 'ALERT', label: 'Alert (Berisiko)' },
{ value: 'STUNTING', label: 'Stunting' },
]}
value={form.statusStunting}
onChange={(v) => { if (v) balitaState.edit.form.statusStunting = v as typeof form.statusStunting; }}
radius="md"
/>
</SimpleGrid>
<TextInput
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { balitaState.edit.form.alamat = e.currentTarget.value; }}
radius="md"
/>
<Group gap="xl">
<Checkbox
label="Imunisasi Lengkap"
checked={form.imunisasiLengkap}
onChange={(e) => { balitaState.edit.form.imunisasiLengkap = e.currentTarget.checked; }}
/>
<Checkbox
label="Gizi Baik"
checked={form.giziBaik}
onChange={(e) => { balitaState.edit.form.giziBaik = e.currentTarget.checked; }}
/>
<Checkbox
label="Pemeriksaan Rutin"
checked={form.pemeriksaanRutin}
onChange={(e) => { balitaState.edit.form.pemeriksaanRutin = e.currentTarget.checked; }}
/>
</Group>
<Textarea
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { balitaState.edit.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>
<Group justify="right">
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
disabled={state.edit.loading}
style={{
background: state.edit.loading
? 'linear-gradient(135deg, #ccc, #eee)'
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
}}
>
{state.edit.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,307 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Group,
Pagination,
Paper,
Select,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
const STUNTING_COLORS: Record<string, string> = {
NORMAL: 'green',
ALERT: 'yellow',
STUNTING: 'red',
};
function BalitaPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title="Balita Terdaftar"
placeholder="Cari nama / NIK / ortu..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBalita search={search} />
</Box>
);
}
function ListBalita({ search }: { search: string }) {
const state = useProxy(balitaState);
const router = useRouter();
const [statusFilter, setStatusFilter] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => {
load(page, 10, debouncedSearch, statusFilter);
}, [page, debouncedSearch, statusFilter]);
const handleDelete = async (id: string, nama: string) => {
if (!confirm(`Hapus data balita "${nama}"?`)) return;
await state.delete.byId(id);
};
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py="md">
<Skeleton h={600} radius="md" />
</Stack>
);
}
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="lg">
<Title order={4}>List Balita</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kesehatan/posyandu/balita/create')}
>
Tambah Baru
</Button>
</Group>
<Group mb="md">
<Select
placeholder="Filter stunting"
data={[
{ value: '', label: 'Semua' },
{ value: 'NORMAL', label: 'Normal' },
{ value: 'ALERT', label: 'Alert' },
{ value: 'STUNTING', label: 'Stunting' },
]}
value={statusFilter}
onChange={(v) => setStatusFilter(v ?? '')}
radius="md"
clearable
/>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh w="22%">Nama</TableTh>
<TableTh w="7%">JK</TableTh>
<TableTh w="12%">Tgl Lahir</TableTh>
<TableTh w="12%">Imunisasi</TableTh>
<TableTh w="10%">Gizi</TableTh>
<TableTh w="12%">Pemeriksaan</TableTh>
<TableTh w="11%">Stunting</TableTh>
<TableTh w="14%">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((d) => (
<TableTr key={d.id}>
<TableTd>{d.nama}</TableTd>
<TableTd>{d.jenisKelamin}</TableTd>
<TableTd>
{d.tanggalLahir
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
: '-'}
</TableTd>
<TableTd>
<Badge color={d.imunisasiLengkap ? 'green' : 'red'} variant="light">
{d.imunisasiLengkap ? 'Lengkap' : 'Belum'}
</Badge>
</TableTd>
<TableTd>
<Badge color={d.giziBaik ? 'green' : 'orange'} variant="light">
{d.giziBaik ? 'Baik' : 'Kurang'}
</Badge>
</TableTd>
<TableTd>
<Badge color={d.pemeriksaanRutin ? 'green' : 'orange'} variant="light">
{d.pemeriksaanRutin ? 'Rutin' : 'Tidak'}
</Badge>
</TableTd>
<TableTd>
<Badge
color={STUNTING_COLORS[d.statusStunting] ?? 'gray'}
variant="light"
>
{d.statusStunting}
</Badge>
</TableTd>
<TableTd>
<Group gap="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/kesehatan/posyandu/balita/edit/${d.id}`)
}
>
Edit
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => handleDelete(d.id, d.nama)}
loading={state.delete.loading}
>
Hapus
</Button>
</Group>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={8}>
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data balita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((d) => (
<Paper key={d.id} withBorder p="md" radius="md">
<Box>
<Text fz="sm" fw={600} mb={4}>
{d.nama}
</Text>
<Group gap="xs" mb={6}>
<Text fz="xs" c="dimmed">
{d.jenisKelamin}
</Text>
<Text fz="xs" c="dimmed">
·
</Text>
<Text fz="xs" c="dimmed">
{d.tanggalLahir
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
: '-'}
</Text>
</Group>
<Group gap="xs" mb={8}>
<Badge
size="xs"
color={d.imunisasiLengkap ? 'green' : 'red'}
variant="light"
>
{d.imunisasiLengkap ? 'Imunisasi Lengkap' : 'Imunisasi Belum'}
</Badge>
<Badge
size="xs"
color={d.giziBaik ? 'green' : 'orange'}
variant="light"
>
Gizi {d.giziBaik ? 'Baik' : 'Kurang'}
</Badge>
<Badge
size="xs"
color={STUNTING_COLORS[d.statusStunting] ?? 'gray'}
variant="light"
>
{d.statusStunting}
</Badge>
</Group>
<Group gap="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/kesehatan/posyandu/balita/edit/${d.id}`)
}
>
Edit
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => handleDelete(d.id, d.nama)}
loading={state.delete.loading}
>
Hapus
</Button>
</Group>
</Box>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data balita yang cocok
</Text>
</Center>
)}
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search, statusFilter);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="lg"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default BalitaPage;

View File

@@ -0,0 +1,146 @@
'use client';
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Select,
SimpleGrid,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
export default function IbuHamilCreatePage() {
const router = useRouter();
const state = useProxy(ibuHamilState);
const form = state.create.form;
const handleSubmit = async () => {
const ok = await ibuHamilState.create.submit();
if (ok) {
ibuHamilState.create.reset();
router.push('/admin/kesehatan/posyandu/ibu-hamil');
}
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md" gap="sm">
<Button variant="subtle" onClick={() => router.back()} radius="md">
<IconArrowBack size={20} />
</Button>
<Title order={3} c="black">Tambah Ibu Hamil</Title>
</Group>
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
<Stack gap="md">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
<TextInput
label="Nama Lengkap"
required
placeholder="Nama ibu hamil"
value={form.nama}
onChange={(e) => { ibuHamilState.create.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { ibuHamilState.create.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Usia Kehamilan (minggu)"
type="number"
placeholder="0"
value={String(form.usiaKehamilan)}
onChange={(e) => { ibuHamilState.create.form.usiaKehamilan = Number(e.currentTarget.value) || 0; }}
radius="md"
/>
<TextInput
label="No. HP"
placeholder="08xx-xxxx-xxxx"
value={form.noHp}
onChange={(e) => { ibuHamilState.create.form.noHp = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="HPHT (Hari Pertama Haid Terakhir)"
type="date"
value={form.hpht}
onChange={(e) => { ibuHamilState.create.form.hpht = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Taksiran Persalinan"
type="date"
value={form.taksiranLahir}
onChange={(e) => { ibuHamilState.create.form.taksiranLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
label="Status"
required
data={[
{ value: 'AKTIF', label: 'Aktif' },
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
{ value: 'KEGUGURAN', label: 'Keguguran' },
{ value: 'NONAKTIF', label: 'Nonaktif' },
]}
value={form.status}
onChange={(v) => { if (v) ibuHamilState.create.form.status = v as typeof form.status; }}
radius="md"
/>
</SimpleGrid>
<TextInput
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { ibuHamilState.create.form.alamat = e.currentTarget.value; }}
radius="md"
/>
<Textarea
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { ibuHamilState.create.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>
<Group justify="right">
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
disabled={state.create.loading}
style={{
background: state.create.loading
? 'linear-gradient(135deg, #ccc, #eee)'
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
}}
>
{state.create.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,149 @@
'use client';
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Select,
SimpleGrid,
Stack,
Textarea,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
export default function IbuHamilEditPage() {
const router = useRouter();
const params = useParams();
const id = params.id as string;
const state = useProxy(ibuHamilState);
const form = state.edit.form;
useEffect(() => {
if (id) ibuHamilState.edit.load(id);
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const handleSubmit = async () => {
const ok = await ibuHamilState.edit.update();
if (ok) router.push('/admin/kesehatan/posyandu/ibu-hamil');
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group mb="md" gap="sm">
<Button variant="subtle" onClick={() => router.back()} radius="md">
<IconArrowBack size={20} />
</Button>
<Title order={3} c="black">Edit Ibu Hamil</Title>
</Group>
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
<Stack gap="md">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
<TextInput
label="Nama Lengkap"
required
placeholder="Nama ibu hamil"
value={form.nama}
onChange={(e) => { ibuHamilState.edit.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { ibuHamilState.edit.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Usia Kehamilan (minggu)"
type="number"
placeholder="0"
value={String(form.usiaKehamilan)}
onChange={(e) => { ibuHamilState.edit.form.usiaKehamilan = Number(e.currentTarget.value) || 0; }}
radius="md"
/>
<TextInput
label="No. HP"
placeholder="08xx-xxxx-xxxx"
value={form.noHp}
onChange={(e) => { ibuHamilState.edit.form.noHp = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="HPHT"
type="date"
value={form.hpht}
onChange={(e) => { ibuHamilState.edit.form.hpht = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Taksiran Persalinan"
type="date"
value={form.taksiranLahir}
onChange={(e) => { ibuHamilState.edit.form.taksiranLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
label="Status"
required
data={[
{ value: 'AKTIF', label: 'Aktif' },
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
{ value: 'KEGUGURAN', label: 'Keguguran' },
{ value: 'NONAKTIF', label: 'Nonaktif' },
]}
value={form.status}
onChange={(v) => { if (v) ibuHamilState.edit.form.status = v as typeof form.status; }}
radius="md"
/>
</SimpleGrid>
<TextInput
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { ibuHamilState.edit.form.alamat = e.currentTarget.value; }}
radius="md"
/>
<Textarea
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { ibuHamilState.edit.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>
<Group justify="right">
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
disabled={state.edit.loading}
style={{
background: state.edit.loading
? 'linear-gradient(135deg, #ccc, #eee)'
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
}}
>
{state.edit.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,278 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Group,
Pagination,
Paper,
Select,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
const STATUS_COLORS: Record<string, string> = {
AKTIF: 'green',
MELAHIRKAN: 'blue',
KEGUGURAN: 'gray',
NONAKTIF: 'red',
};
function IbuHamilPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title="Ibu Hamil"
placeholder="Cari nama / NIK..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListIbuHamil search={search} />
</Box>
);
}
function ListIbuHamil({ search }: { search: string }) {
const state = useProxy(ibuHamilState);
const router = useRouter();
const [statusFilter, setStatusFilter] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 1000);
const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => {
load(page, 10, debouncedSearch, statusFilter);
}, [page, debouncedSearch, statusFilter]);
const handleDelete = async (id: string, nama: string) => {
if (!confirm(`Hapus data ibu hamil "${nama}"?`)) return;
await state.delete.byId(id);
};
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py="md">
<Skeleton h={600} radius="md" />
</Stack>
);
}
return (
<Box py="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="lg">
<Title order={4}>List Ibu Hamil</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil/create')}
>
Tambah Baru
</Button>
</Group>
<Group mb="md">
<Select
placeholder="Filter status"
data={[
{ value: '', label: 'Semua Status' },
{ value: 'AKTIF', label: 'Aktif' },
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
{ value: 'KEGUGURAN', label: 'Keguguran' },
{ value: 'NONAKTIF', label: 'Nonaktif' },
]}
value={statusFilter}
onChange={(v) => setStatusFilter(v ?? '')}
radius="md"
clearable
/>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh w="25%">Nama</TableTh>
<TableTh w="18%">NIK</TableTh>
<TableTh w="17%">Usia Kehamilan</TableTh>
<TableTh w="15%">No. HP</TableTh>
<TableTh w="12%">Status</TableTh>
<TableTh w="13%">Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((d) => (
<TableTr key={d.id}>
<TableTd>{d.nama}</TableTd>
<TableTd>{d.nik || '-'}</TableTd>
<TableTd>{d.usiaKehamilan} minggu</TableTd>
<TableTd>{d.noHp || '-'}</TableTd>
<TableTd>
<Badge
color={STATUS_COLORS[d.status] ?? 'gray'}
variant="light"
>
{d.status}
</Badge>
</TableTd>
<TableTd>
<Group gap="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/kesehatan/posyandu/ibu-hamil/edit/${d.id}`)
}
>
Edit
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => handleDelete(d.id, d.nama)}
loading={state.delete.loading}
>
Hapus
</Button>
</Group>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={6}>
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data ibu hamil yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Stack gap="sm" hiddenFrom="md">
{filteredData.length > 0 ? (
filteredData.map((d) => (
<Paper key={d.id} withBorder p="md" radius="md">
<Box>
<Text fz="sm" fw={600} mb={4}>
{d.nama}
</Text>
<Group gap="xs" mb={6}>
<Text fz="xs" c="dimmed">
NIK: {d.nik || '-'}
</Text>
<Text fz="xs" c="dimmed">
·
</Text>
<Text fz="xs" c="dimmed">
{d.usiaKehamilan} minggu
</Text>
</Group>
<Group gap="xs" mb={8}>
<Badge
size="xs"
color={STATUS_COLORS[d.status] ?? 'gray'}
variant="light"
>
{d.status}
</Badge>
{d.noHp && (
<Text fz="xs" c="dimmed">
{d.noHp}
</Text>
)}
</Group>
<Group gap="xs">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/kesehatan/posyandu/ibu-hamil/edit/${d.id}`)
}
>
Edit
</Button>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => handleDelete(d.id, d.nama)}
loading={state.delete.loading}
>
Hapus
</Button>
</Group>
</Box>
</Paper>
))
) : (
<Center py="xl">
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data ibu hamil yang cocok
</Text>
</Center>
)}
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search, statusFilter);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="lg"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default IbuHamilPage;

View File

@@ -0,0 +1,35 @@
'use client'
import { Box } from '@mantine/core';
import { usePathname } from 'next/navigation';
import React from 'react';
import LayoutTabsPosyandu from './_com/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return (
<LayoutTabsPosyandu>
{children}
</LayoutTabsPosyandu>
);
}
export default Layout;

View File

@@ -1,6 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors';
@@ -145,7 +144,7 @@ function EditPosyandu() {
await statePosyandu.edit.update();
toast.success('Posyandu berhasil diperbarui!');
router.push('/admin/kesehatan/posyandu');
router.push('/admin/kesehatan/posyandu/list-posyandu');
} catch (error) {
console.error('Error updating posyandu:', error);
toast.error('Terjadi kesalahan saat memperbarui posyandu');
@@ -168,7 +167,7 @@ function EditPosyandu() {
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Tombol Back */}
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">

View File

@@ -1,4 +1,6 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
@@ -6,12 +8,11 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import posyanduState from '../../../_state/kesehatan/posyandu/posyandu';
function DetailPosyandu() {
const statePosyandu = useProxy(posyanduState);
const statePosyandu = useProxy(posyandustate);
const params = useParams();
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
@@ -28,7 +29,7 @@ function DetailPosyandu() {
statePosyandu.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/kesehatan/posyandu");
router.push("/admin/kesehatan/posyandu/list-posyandu");
}
};
@@ -147,7 +148,7 @@ function DetailPosyandu() {
<Button
color="green"
onClick={() => router.push(`/admin/kesehatan/posyandu/${data.id}/edit`)}
onClick={() => router.push(`/admin/kesehatan/posyandu/list-posyandu/${data.id}/edit`)}
variant="light"
radius="md"
size="md"

View File

@@ -1,4 +1,6 @@
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
@@ -20,8 +22,7 @@ import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
function CreatePosyandu() {
@@ -105,7 +106,7 @@ function CreatePosyandu() {
statePosyandu.create.form.imageId = uploaded.id;
await statePosyandu.create.create();
resetForm();
router.push('/admin/kesehatan/posyandu');
router.push('/admin/kesehatan/posyandu/list-posyandu');
} catch (error) {
console.error('Error creating posyandu:', error);
toast.error('Gagal menambahkan posyandu');

View File

@@ -23,8 +23,10 @@ import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import posyandustate from '../../_state/kesehatan/posyandu/posyandu';
import HeaderSearch from '../../../_com/header';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
function Posyandu() {
const [search, setSearch] = useState("");
@@ -80,18 +82,18 @@ function ListPosyandu({ search }: { search: string }) {
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kesehatan/posyandu/create')}
onClick={() => router.push('/admin/kesehatan/posyandu/list-posyandu/create')}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}
>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}
>
<TableThead>
<TableTr>
<TableTh w={220} fz="sm" fw={600} ta="left" lh={1.4}>Nama Posyandu</TableTh>
@@ -130,7 +132,7 @@ function ListPosyandu({ search }: { search: string }) {
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}
onClick={() => router.push(`/admin/kesehatan/posyandu/list-posyandu/${item.id}`)}
>
Detail
</Button>
@@ -192,7 +194,7 @@ function ListPosyandu({ search }: { search: string }) {
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}
onClick={() => router.push(`/admin/kesehatan/posyandu/list-posyandu/${item.id}`)}
fullWidth
>
Detail

View File

@@ -0,0 +1,220 @@
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Card,
Divider,
Group,
Loader,
NumberInput,
Paper,
SimpleGrid,
Stack,
Text,
Title,
} from '@mantine/core';
import {
IconArrowRight,
IconMoodBoy,
IconHeartbeat,
IconPercentage,
IconUser,
IconAlertTriangle,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useCallback } from 'react';
import { useProxy } from 'valtio/utils';
import ringkasanKesehatanState from '../../../_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan';
type StatCardProps = {
label: string;
value: string | number;
icon: React.ReactNode;
color?: string;
suffix?: string;
};
function StatCard({ label, value, icon, color = 'blue', suffix }: StatCardProps) {
return (
<Card withBorder radius="md" p="md">
<Group gap="sm" align="flex-start">
<Box
style={{
width: 40,
height: 40,
borderRadius: 8,
background: `var(--mantine-color-${color}-1)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: `var(--mantine-color-${color}-6)`,
flexShrink: 0,
}}
>
{icon}
</Box>
<Box>
<Text fz="xs" c="dimmed" fw={500}>{label}</Text>
<Text fz="xl" fw={700}>
{value}{suffix && <Text component="span" fz="sm" c="dimmed" fw={400}> {suffix}</Text>}
</Text>
</Box>
</Group>
</Card>
);
}
export default function RingkasanKesehatanPage() {
const router = useRouter();
const state = useProxy(ringkasanKesehatanState);
const stats = state.findStats.data;
const loadStats = useCallback(() => { ringkasanKesehatanState.findStats.load(); }, []);
useEffect(() => { loadStats(); }, [loadStats]);
const isLoading = state.findStats.loading;
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Title order={3} c="black" mb="md">Ringkasan Kesehatan Desa</Title>
{isLoading ? (
<Group justify="center" py="xl"><Loader /></Group>
) : (
<Stack gap="lg">
{/* KPI Utama */}
<Box>
<Text fw={600} mb="sm" c="dark">KPI Utama</Text>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
<StatCard
label="Ibu Hamil Aktif"
value={stats?.ibuHamilAktif ?? 0}
icon={<IconUser size={20} />}
color="pink"
suffix="orang"
/>
<StatCard
label="Balita Terdaftar"
value={stats?.balitaTerdaftar ?? 0}
icon={<IconMoodBoy size={20} />}
color="blue"
suffix="anak"
/>
<StatCard
label="Alert Stunting"
value={stats?.alertStunting ?? 0}
icon={<IconAlertTriangle size={20} />}
color="red"
suffix="anak"
/>
</SimpleGrid>
</Box>
<Divider />
{/* Statistik % */}
<Box>
<Text fw={600} mb="sm" c="dark">Statistik Kesehatan Balita</Text>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md">
<StatCard
label="Imunisasi Lengkap"
value={stats?.imunisasiLengkapPct ?? 0}
icon={<IconHeartbeat size={20} />}
color="teal"
suffix="%"
/>
<StatCard
label="Pemeriksaan Rutin"
value={stats?.pemeriksaanRutinPct ?? 0}
icon={<IconHeartbeat size={20} />}
color="green"
suffix="%"
/>
<StatCard
label="Gizi Baik"
value={stats?.giziBaikPct ?? 0}
icon={<IconHeartbeat size={20} />}
color="lime"
suffix="%"
/>
<StatCard
label="Target Penurunan Stunting"
value={stats?.targetStuntingPct ?? 0}
icon={<IconPercentage size={20} />}
color="orange"
suffix="%"
/>
</SimpleGrid>
</Box>
<Divider />
{/* Target Stunting Config */}
<Paper withBorder p="md" radius="md" maw={400}>
<Text fw={600} mb="sm" c="dark">Atur Target Stunting</Text>
<Text fz="xs" c="dimmed" mb="sm">
Target penurunan angka stunting adalah nilai kebijakan yang ditentukan
oleh admin, bukan turunan dari data.
</Text>
<Group align="flex-end" gap="sm">
<NumberInput
label="Target (%)"
min={0}
max={100}
suffix="%"
value={state.update.form.targetStuntingPct}
onChange={(v) => { state.update.form.targetStuntingPct = Number(v) || 0; }}
radius="md"
style={{ flex: 1 }}
/>
<Button
onClick={() => state.update.submitTarget()}
radius="md"
disabled={state.update.loading}
style={{
background: state.update.loading
? 'linear-gradient(135deg, #ccc, #eee)'
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
marginBottom: 1,
}}
>
{state.update.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Paper>
<Divider />
{/* Kelola Data */}
<Box>
<Text fw={600} mb="sm" c="dark">Kelola Data</Text>
<Group gap="md">
<Button
variant="light"
color="pink"
radius="md"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil')}
>
Kelola Ibu Hamil
</Button>
<Button
variant="light"
color="blue"
radius="md"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push('/admin/kesehatan/posyandu/balita')}
>
Kelola Balita
</Button>
</Group>
</Box>
</Stack>
)}
</Box>
);
}

View File

@@ -2,7 +2,7 @@
'use client'
import colors from '@/con/colors';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconSchool, IconStar } from '@tabler/icons-react';
import { IconSchool, IconSettings2, IconStar } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -23,6 +23,12 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
href: "/admin/pendidikan/beasiswa-desa/keunggulan-program",
icon: <IconStar size={18} stroke={1.8} />
},
{
label: "Konfigurasi Beasiswa",
value: "beasiswa-config",
href: "/admin/pendidikan/beasiswa-desa/beasiswa-config",
icon: <IconSettings2 size={18} stroke={1.8} />
},
];
const currentTab = tabs.find(tab => tab.href === pathname);

View File

@@ -0,0 +1,192 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Card,
Divider,
Group,
NumberInput,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconCash, IconCalendar, IconUsers, IconDeviceFloppy, IconRefresh } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import ringkasanBeasiswaState from '../../../_state/pendidikan/ringkasan-beasiswa';
function formatRupiah(value: string | number) {
const num = typeof value === 'string' ? parseInt(value, 10) : value;
if (isNaN(num)) return 'Rp 0';
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(num);
}
export default function BeasiswaConfigPage() {
const state = useProxy(ringkasanBeasiswaState);
const [tahunAjaran, setTahunAjaran] = useState('');
const [danaTersalurkan, setDanaTersalurkan] = useState<number | string>('');
useEffect(() => {
state.beasiswaConfig.find();
state.findStats.load();
}, []);
useEffect(() => {
const cfg = state.beasiswaConfig.data;
if (cfg) {
setTahunAjaran(cfg.tahunAjaran);
setDanaTersalurkan(parseInt(cfg.danaTersalurkan, 10) || 0);
}
}, [state.beasiswaConfig.data]);
const handleSave = async () => {
await state.beasiswaConfig.update.submit(
tahunAjaran,
String(danaTersalurkan),
);
};
const isLoading = state.beasiswaConfig.loading;
const isSaving = state.beasiswaConfig.update.loading;
const stats = state.findStats.data;
return (
<Stack gap="lg">
{/* ─── Header ─── */}
<Group justify="space-between" align="center">
<Box>
<Title order={4} fw={700} c="#1A1B1E">Konfigurasi Beasiswa</Title>
<Text size="sm" c="dimmed" mt={2}>Atur tahun ajaran aktif dan total dana yang tersalurkan</Text>
</Box>
<Badge color="blue" variant="light" size="lg" radius="md">
Tahun Aktif: {stats?.tahunAjaran ?? '-'}
</Badge>
</Group>
{/* ─── Stats Cards ─── */}
{state.findStats.loading ? (
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
<Skeleton height={90} radius="md" />
<Skeleton height={90} radius="md" />
<Skeleton height={90} radius="md" />
</SimpleGrid>
) : (
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
<Card withBorder radius="md" p="md">
<Group gap="sm">
<Box p={8} style={{ background: '#e7f5ff', borderRadius: 8 }}>
<IconUsers size={20} color={colors['blue-button']} />
</Box>
<Box>
<Text size="xs" c="dimmed" fw={500}>Jumlah Penerima</Text>
<Text size="xl" fw={700} c={colors['blue-button']}>{stats?.jumlahPenerima ?? 0}</Text>
</Box>
</Group>
</Card>
<Card withBorder radius="md" p="md">
<Group gap="sm">
<Box p={8} style={{ background: '#ebfbee', borderRadius: 8 }}>
<IconCash size={20} color="#2f9e44" />
</Box>
<Box>
<Text size="xs" c="dimmed" fw={500}>Dana Tersalurkan</Text>
<Text size="sm" fw={700} c="#2f9e44" lineClamp={1}>
{stats ? formatRupiah(stats.danaTersalurkan) : 'Rp 0'}
</Text>
</Box>
</Group>
</Card>
<Card withBorder radius="md" p="md">
<Group gap="sm">
<Box p={8} style={{ background: '#fff9db', borderRadius: 8 }}>
<IconCalendar size={20} color="#e67700" />
</Box>
<Box>
<Text size="xs" c="dimmed" fw={500}>Tahun Ajaran</Text>
<Text size="xl" fw={700} c="#e67700">{stats?.tahunAjaran ?? '-'}</Text>
</Box>
</Group>
</Card>
</SimpleGrid>
)}
<Divider />
{/* ─── Form Edit ─── */}
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="sm" radius="md">
<Title order={5} fw={600} mb="md" c="#1A1B1E">Edit Konfigurasi</Title>
{isLoading ? (
<Stack gap="sm">
<Skeleton height={56} radius="md" />
<Skeleton height={56} radius="md" />
</Stack>
) : (
<Stack gap="md">
<TextInput
label="Tahun Ajaran"
placeholder="Contoh: 2025/2026"
value={tahunAjaran}
onChange={(e) => setTahunAjaran(e.currentTarget.value)}
leftSection={<IconCalendar size={16} />}
radius="md"
description="Format: YYYY/YYYY"
/>
<NumberInput
label="Dana Tersalurkan (Rp)"
placeholder="Contoh: 1200000000"
value={danaTersalurkan}
onChange={(val) => setDanaTersalurkan(val)}
leftSection={<IconCash size={16} />}
radius="md"
min={0}
step={1000000}
thousandSeparator="."
decimalSeparator=","
allowNegative={false}
description="Total dana yang tersalurkan untuk tahun ajaran ini"
/>
<Group justify="flex-end" mt="xs" gap="sm">
<Button
variant="default"
radius="md"
leftSection={<IconRefresh size={16} />}
onClick={() => {
const cfg = state.beasiswaConfig.data;
if (cfg) {
setTahunAjaran(cfg.tahunAjaran);
setDanaTersalurkan(parseInt(cfg.danaTersalurkan, 10) || 0);
}
}}
>
Reset
</Button>
<Button
color={colors['blue-button']}
radius="md"
leftSection={<IconDeviceFloppy size={16} />}
loading={isSaving}
onClick={handleSave}
disabled={!tahunAjaran}
>
Simpan Konfigurasi
</Button>
</Group>
</Stack>
)}
</Paper>
</Stack>
);
}

View File

@@ -123,6 +123,11 @@ export const devBar = [
id: "Desa_8",
name: "Kegiatan Desa",
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
},
{
id: "Desa_9",
name: "Event Budaya",
path: "/admin/desa/event-budaya"
}
]
@@ -135,7 +140,7 @@ export const devBar = [
{
id: "Kesehatan_1",
name: "Posyandu",
path: "/admin/kesehatan/posyandu"
path: "/admin/kesehatan/posyandu/list-posyandu"
},
{
id: "Kesehatan_2",
@@ -559,6 +564,11 @@ export const navBar = [
id: "Desa_8",
name: "Kegiatan Desa",
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
},
{
id: "Desa_9",
name: "Event Budaya",
path: "/admin/desa/event-budaya"
}
]
@@ -571,7 +581,7 @@ export const navBar = [
{
id: "Kesehatan_1",
name: "Posyandu",
path: "/admin/kesehatan/posyandu"
path: "/admin/kesehatan/posyandu/list-posyandu/list_posyandu"
},
{
id: "Kesehatan_2",
@@ -1010,6 +1020,11 @@ export const role1 = [
id: "Desa_8",
name: "Kegiatan Desa",
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
},
{
id: "Desa_9",
name: "Event Budaya",
path: "/admin/desa/event-budaya"
}
]
@@ -1198,7 +1213,7 @@ export const role1 = [
}
]
},
{
{
id: "Kependudukan",
name: "Kependudukan",
path: "",
@@ -1241,7 +1256,7 @@ export const role2 = [
{
id: "Kesehatan_1",
name: "Posyandu",
path: "/admin/kesehatan/posyandu"
path: "/admin/kesehatan/posyandu/list-posyandu/list_posyandu"
},
{
id: "Kesehatan_2",

View File

@@ -37,7 +37,7 @@ import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
export default function Layout({ children }: { children: React.ReactNode }) {
const { isDark } = useDarkMode();
const tokens = themeTokens(isDark);
const [mounted, setMounted] = useState(false);
const [opened, { toggle, close }] = useDisclosure();
const [loading, setLoading] = useState(true);
@@ -114,7 +114,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
case 2:
return '/admin/landing-page/profil/program-inovasi';
case 3:
return '/admin/kesehatan/posyandu';
return '/admin/kesehatan/posyandu/list-posyandu';
case 4:
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
default:

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaCreate(context: Context) {
const body = context.body as {
nama: string;
tanggal: string;
lokasi: string;
deskripsi?: string;
};
try {
const data = await prisma.eventBudaya.create({
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
lokasi: body.lokasi,
deskripsi: body.deskripsi || null,
},
});
return { success: true, message: "Event budaya berhasil dibuat", data };
} catch (e) {
console.error("Error di eventBudayaCreate:", e);
return { success: false, message: "Gagal membuat event budaya" };
}
}
export default eventBudayaCreate;

View File

@@ -0,0 +1,20 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaDelete(context: Context) {
const { id } = context.params as { id: string };
try {
await prisma.eventBudaya.update({
where: { id },
data: { isActive: false },
});
return { success: true, message: "Event budaya berhasil dihapus" };
} catch (e) {
console.error("Error di eventBudayaDelete:", e);
return { success: false, message: "Gagal menghapus event budaya" };
}
}
export default eventBudayaDelete;

View File

@@ -0,0 +1,46 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit;
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ lokasi: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.eventBudaya.findMany({
where,
skip,
take: limit,
orderBy: { tanggal: "asc" },
}),
prisma.eventBudaya.count({ where }),
]);
return {
success: true,
message: "Berhasil mengambil event budaya",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di eventBudayaFindMany:", e);
return { success: false, message: "Gagal mengambil data event budaya" };
}
}
export default eventBudayaFindMany;

View File

@@ -0,0 +1,23 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaFindUnique(context: Context) {
const { id } = context.params as { id: string };
try {
const data = await prisma.eventBudaya.findFirst({
where: { id, isActive: true },
});
if (!data) {
return { success: false, message: "Event budaya tidak ditemukan" };
}
return { success: true, data };
} catch (e) {
console.error("Error di eventBudayaFindUnique:", e);
return { success: false, message: "Gagal mengambil data event budaya" };
}
}
export default eventBudayaFindUnique;

View File

@@ -0,0 +1,29 @@
import Elysia, { t } from "elysia";
import eventBudayaFindMany from "./find-many";
import eventBudayaFindUnique from "./findUnique";
import eventBudayaCreate from "./create";
import eventBudayaDelete from "./del";
import eventBudayaUpdate from "./updt";
const EventBudaya = new Elysia({ prefix: "/eventbudaya", tags: ["Desa/Event Budaya"] })
.get("/find-many", eventBudayaFindMany)
.get("/:id", eventBudayaFindUnique)
.post("/create", eventBudayaCreate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
lokasi: t.String(),
deskripsi: t.Optional(t.String()),
}),
})
.put("/:id", eventBudayaUpdate, {
body: t.Object({
nama: t.String(),
tanggal: t.String(),
lokasi: t.String(),
deskripsi: t.Optional(t.String()),
}),
})
.delete("/del/:id", eventBudayaDelete);
export default EventBudaya;

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function eventBudayaUpdate(context: Context) {
const { id } = context.params as { id: string };
const body = context.body as {
nama: string;
tanggal: string;
lokasi: string;
deskripsi?: string;
};
try {
const data = await prisma.eventBudaya.update({
where: { id },
data: {
nama: body.nama,
tanggal: new Date(body.tanggal),
lokasi: body.lokasi,
deskripsi: body.deskripsi || null,
},
});
return { success: true, message: "Event budaya berhasil diupdate", data };
} catch (e) {
console.error("Error di eventBudayaUpdate:", e);
return { success: false, message: "Gagal mengupdate event budaya" };
}
}
export default eventBudayaUpdate;

View File

@@ -15,6 +15,7 @@ import AjukanPermohonan from "./layanan/ajukan_permohonan";
import Musik from "./musik";
import KegiatanDesa from "./kegiatan-desa";
import KategoriKegiatan from "./kegiatan-desa/kategori-kegiatan";
import EventBudaya from "./event-budaya";
const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
@@ -34,6 +35,7 @@ const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
.use(Musik)
.use(KegiatanDesa)
.use(KategoriKegiatan)
.use(EventBudaya)
export default Desa;

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
import { JenisKelaminBalita, StatusStunting } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = {
nama: string;
nik?: string;
tanggalLahir: string;
jenisKelamin: JenisKelaminBalita;
beratBadanKg?: number;
tinggiBadanCm?: number;
namaOrtu?: string;
alamat?: string;
noHpOrtu?: string;
posyanduId?: string;
imunisasiLengkap: boolean;
giziBaik: boolean;
pemeriksaanRutin: boolean;
statusStunting: StatusStunting;
catatan?: string;
};
export default async function balitaCreate(context: Context) {
const body = context.body as FormCreate;
const data = await prisma.balita.create({
data: {
nama: body.nama,
nik: body.nik,
tanggalLahir: new Date(body.tanggalLahir),
jenisKelamin: body.jenisKelamin,
beratBadanKg: body.beratBadanKg,
tinggiBadanCm: body.tinggiBadanCm,
namaOrtu: body.namaOrtu,
alamat: body.alamat,
noHpOrtu: body.noHpOrtu,
posyanduId: body.posyanduId || null,
imunisasiLengkap: body.imunisasiLengkap,
giziBaik: body.giziBaik,
pemeriksaanRutin: body.pemeriksaanRutin,
statusStunting: body.statusStunting ?? "NORMAL",
catatan: body.catatan,
},
});
return { success: true, message: "Balita berhasil ditambahkan", data };
}

View File

@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function balitaDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return { success: false, message: "ID tidak diberikan" };
}
const existing = await prisma.balita.findUnique({ where: { id } });
if (!existing) {
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
await prisma.balita.update({ where: { id }, data: { isActive: false } });
return { success: true, message: "Balita berhasil dihapus" };
}

View File

@@ -0,0 +1,38 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function balitaFindById(context: Context) {
const id = context.params?.id as string;
if (!id) {
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
try {
const data = await prisma.balita.findUnique({
where: { id },
include: { posyandu: { select: { id: true, name: true } } },
});
if (!data) {
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("balitaFindById error:", error);
return new Response(JSON.stringify({ success: false, message: "Gagal mengambil data" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function balitaFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const statusStunting = (context.query.statusStunting as string) || "";
const skip = (page - 1) * limit;
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ nik: { contains: search, mode: "insensitive" } },
{ namaOrtu: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } },
];
}
if (statusStunting) {
where.statusStunting = statusStunting;
}
try {
const [data, total] = await Promise.all([
prisma.balita.findMany({
where,
include: { posyandu: { select: { id: true, name: true } } },
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.balita.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data balita",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error balitaFindMany:", e);
return { success: false, message: "Gagal mengambil data balita" };
}
}

View File

@@ -0,0 +1,63 @@
import Elysia, { t } from "elysia";
import balitaCreate from "./create";
import balitaDelete from "./del";
import balitaFindById from "./find-by-id";
import balitaFindMany from "./find-many";
import balitaUpdate from "./updt";
const Balita = new Elysia({ prefix: "/balita", tags: ["Kesehatan/Balita"] })
.post("/create", balitaCreate, {
body: t.Object({
nama: t.String(),
nik: t.Optional(t.String()),
tanggalLahir: t.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()),
}),
})
.get("/find-many", balitaFindMany)
.delete("/del/:id", balitaDelete)
.get("/:id", balitaFindById)
.put(
"/:id",
balitaUpdate,
{
body: t.Object({
nama: t.String(),
nik: t.Optional(t.String()),
tanggalLahir: t.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()),
}),
}
);
export default Balita;

View File

@@ -0,0 +1,75 @@
import prisma from "@/lib/prisma";
import { JenisKelaminBalita, StatusStunting } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = {
nama: string;
nik?: string;
tanggalLahir: string;
jenisKelamin: JenisKelaminBalita;
beratBadanKg?: number;
tinggiBadanCm?: number;
namaOrtu?: string;
alamat?: string;
noHpOrtu?: string;
posyanduId?: string;
imunisasiLengkap: boolean;
giziBaik: boolean;
pemeriksaanRutin: boolean;
statusStunting: StatusStunting;
catatan?: string;
};
export default async function balitaUpdate(context: Context) {
const id = context.params?.id as string;
const body = context.body as FormUpdate;
if (!id) {
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
try {
const existing = await prisma.balita.findUnique({ where: { id } });
if (!existing) {
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
const updated = await prisma.balita.update({
where: { id },
data: {
nama: body.nama,
nik: body.nik,
tanggalLahir: new Date(body.tanggalLahir),
jenisKelamin: body.jenisKelamin,
beratBadanKg: body.beratBadanKg,
tinggiBadanCm: body.tinggiBadanCm,
namaOrtu: body.namaOrtu,
alamat: body.alamat,
noHpOrtu: body.noHpOrtu,
posyanduId: body.posyanduId || null,
imunisasiLengkap: body.imunisasiLengkap,
giziBaik: body.giziBaik,
pemeriksaanRutin: body.pemeriksaanRutin,
statusStunting: body.statusStunting,
catatan: body.catatan,
},
});
return new Response(JSON.stringify({ success: true, message: "Balita berhasil diperbarui", data: updated }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("balitaUpdate error:", error);
return new Response(JSON.stringify({ success: false, message: "Gagal memperbarui data" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { IbuHamilStatus } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = {
nama: string;
nik?: string;
usiaKehamilan: number;
hpht?: string;
taksiranLahir?: string;
alamat?: string;
noHp?: string;
catatan?: string;
posyanduId?: string;
status: IbuHamilStatus;
};
export default async function ibuHamilCreate(context: Context) {
const body = context.body as FormCreate;
const data = await prisma.ibuHamil.create({
data: {
nama: body.nama,
nik: body.nik,
usiaKehamilan: body.usiaKehamilan ?? 0,
hpht: body.hpht ? new Date(body.hpht) : undefined,
taksiranLahir: body.taksiranLahir ? new Date(body.taksiranLahir) : undefined,
alamat: body.alamat,
noHp: body.noHp,
catatan: body.catatan,
posyanduId: body.posyanduId || null,
status: body.status ?? "AKTIF",
},
});
return { success: true, message: "Ibu hamil berhasil ditambahkan", data };
}

View File

@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function ibuHamilDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return { success: false, message: "ID tidak diberikan" };
}
const existing = await prisma.ibuHamil.findUnique({ where: { id } });
if (!existing) {
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
await prisma.ibuHamil.update({ where: { id }, data: { isActive: false } });
return { success: true, message: "Ibu hamil berhasil dihapus" };
}

View File

@@ -0,0 +1,38 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function ibuHamilFindById(context: Context) {
const id = context.params?.id as string;
if (!id) {
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
try {
const data = await prisma.ibuHamil.findUnique({
where: { id },
include: { posyandu: { select: { id: true, name: true } } },
});
if (!data) {
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("ibuHamilFindById error:", error);
return new Response(JSON.stringify({ success: false, message: "Gagal mengambil data" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function ibuHamilFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const status = (context.query.status as string) || "";
const skip = (page - 1) * limit;
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ nik: { contains: search, mode: "insensitive" } },
{ alamat: { contains: search, mode: "insensitive" } },
];
}
if (status) {
where.status = status;
}
try {
const [data, total] = await Promise.all([
prisma.ibuHamil.findMany({
where,
include: { posyandu: { select: { id: true, name: true } } },
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.ibuHamil.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data ibu hamil",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error ibuHamilFindMany:", e);
return { success: false, message: "Gagal mengambil data ibu hamil" };
}
}

View File

@@ -0,0 +1,55 @@
import Elysia, { t } from "elysia";
import ibuHamilCreate from "./create";
import ibuHamilDelete from "./del";
import ibuHamilFindById from "./find-by-id";
import ibuHamilFindMany from "./find-many";
import ibuHamilUpdate from "./updt";
const IbuHamil = new Elysia({ prefix: "/ibuhamil", tags: ["Kesehatan/IbuHamil"] })
.post("/create", ibuHamilCreate, {
body: t.Object({
nama: t.String(),
nik: t.Optional(t.String()),
usiaKehamilan: t.Number({ minimum: 0 }),
hpht: t.Optional(t.String()),
taksiranLahir: t.Optional(t.String()),
alamat: t.Optional(t.String()),
noHp: t.Optional(t.String()),
catatan: t.Optional(t.String()),
posyanduId: t.Optional(t.String()),
status: t.Union([
t.Literal("AKTIF"),
t.Literal("MELAHIRKAN"),
t.Literal("KEGUGURAN"),
t.Literal("NONAKTIF"),
]),
}),
})
.get("/find-many", ibuHamilFindMany)
.delete("/del/:id", ibuHamilDelete)
.get("/:id", ibuHamilFindById)
.put(
"/:id",
ibuHamilUpdate,
{
body: t.Object({
nama: t.String(),
nik: t.Optional(t.String()),
usiaKehamilan: t.Number({ minimum: 0 }),
hpht: t.Optional(t.String()),
taksiranLahir: t.Optional(t.String()),
alamat: t.Optional(t.String()),
noHp: t.Optional(t.String()),
catatan: t.Optional(t.String()),
posyanduId: t.Optional(t.String()),
status: t.Union([
t.Literal("AKTIF"),
t.Literal("MELAHIRKAN"),
t.Literal("KEGUGURAN"),
t.Literal("NONAKTIF"),
]),
}),
}
);
export default IbuHamil;

View File

@@ -0,0 +1,65 @@
import prisma from "@/lib/prisma";
import { IbuHamilStatus } from "@prisma/client";
import { Context } from "elysia";
type FormUpdate = {
nama: string;
nik?: string;
usiaKehamilan: number;
hpht?: string;
taksiranLahir?: string;
alamat?: string;
noHp?: string;
catatan?: string;
posyanduId?: string;
status: IbuHamilStatus;
};
export default async function ibuHamilUpdate(context: Context) {
const id = context.params?.id as string;
const body = context.body as FormUpdate;
if (!id) {
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
try {
const existing = await prisma.ibuHamil.findUnique({ where: { id } });
if (!existing) {
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
const updated = await prisma.ibuHamil.update({
where: { id },
data: {
nama: body.nama,
nik: body.nik,
usiaKehamilan: body.usiaKehamilan ?? 0,
hpht: body.hpht ? new Date(body.hpht) : null,
taksiranLahir: body.taksiranLahir ? new Date(body.taksiranLahir) : null,
alamat: body.alamat,
noHp: body.noHp,
catatan: body.catatan,
posyanduId: body.posyanduId || null,
status: body.status,
},
});
return new Response(JSON.stringify({ success: true, message: "Ibu hamil berhasil diperbarui", data: updated }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("ibuHamilUpdate error:", error);
return new Response(JSON.stringify({ success: false, message: "Gagal memperbarui data" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -22,6 +22,8 @@ import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter
import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
import TarifLayanan from "./data_kesehatan_warga/fasilitas_kesehatan/tarif-layanan";
import RingkasanKesehatan from "./ringkasan-kesehatan";
import IbuHamil from "./ibu-hamil";
import Balita from "./balita";
const Kesehatan = new Elysia({
@@ -51,4 +53,6 @@ const Kesehatan = new Elysia({
.use(TarifLayanan)
.use(PendaftaranJadwalKegiatan)
.use(RingkasanKesehatan)
.use(IbuHamil)
.use(Balita)
export default Kesehatan;

View File

@@ -1,14 +1,14 @@
import Elysia, { t } from "elysia";
import ringkasanKesehatanFindUnique from "./findUnique";
import ringkasanKesehatanUpdate from "./updt";
import ringkasanKesehatanStats from "./stats";
const RingkasanKesehatan = new Elysia({ prefix: "/ringkasankesehatan", tags: ["Kesehatan/Ringkasan"] })
.get("/find", ringkasanKesehatanFindUnique)
.get("/stats", ringkasanKesehatanStats)
.put("/update", ringkasanKesehatanUpdate, {
body: t.Object({
ibuHamilAkh: t.Number(),
balitaTerdaftar: t.Number(),
alertStunting: t.Number(),
targetStuntingPct: t.Number({ minimum: 0, maximum: 100 }),
}),
});

View File

@@ -0,0 +1,52 @@
import prisma from "@/lib/prisma";
type StatsResult = {
ibuHamilAktif: number;
balitaTerdaftar: number;
alertStunting: number;
imunisasiLengkapPct: number;
pemeriksaanRutinPct: number;
giziBaikPct: number;
targetStuntingPct: number;
};
export default async function ringkasanKesehatanStats(): Promise<{ success: boolean; data?: StatsResult; message?: string }> {
try {
const [
ibuHamilAktif,
balitaTotal,
alertStunting,
imunisasiLengkap,
pemeriksaanRutin,
giziBaik,
config,
] = await Promise.all([
prisma.ibuHamil.count({ where: { status: "AKTIF", isActive: true } }),
prisma.balita.count({ where: { isActive: true } }),
prisma.balita.count({ where: { isActive: true, statusStunting: { in: ["ALERT", "STUNTING"] } } }),
prisma.balita.count({ where: { isActive: true, imunisasiLengkap: true } }),
prisma.balita.count({ where: { isActive: true, pemeriksaanRutin: true } }),
prisma.balita.count({ where: { isActive: true, giziBaik: true } }),
prisma.ringkasanKesehatanDesa.findFirst({ where: { isActive: true }, orderBy: { createdAt: "desc" } }),
]);
const pct = (n: number, total: number) =>
total === 0 ? 0 : Math.round((n / total) * 100);
return {
success: true,
data: {
ibuHamilAktif,
balitaTerdaftar: balitaTotal,
alertStunting,
imunisasiLengkapPct: pct(imunisasiLengkap, balitaTotal),
pemeriksaanRutinPct: pct(pemeriksaanRutin, balitaTotal),
giziBaikPct: pct(giziBaik, balitaTotal),
targetStuntingPct: config?.targetStuntingPct ?? 0,
},
};
} catch (e) {
console.error("ringkasanKesehatanStats error:", e);
return { success: false, message: "Gagal menghitung statistik kesehatan" };
}
}

View File

@@ -1,37 +1,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function ringkasanKesehatanUpdate(context: Context) {
const body = context.body as any;
const body = context.body as { targetStuntingPct: number };
try {
const existing = await prisma.ringkasanKesehatanDesa.findFirst({
where: { isActive: true },
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: "desc" },
});
const data = existing
? await prisma.ringkasanKesehatanDesa.update({
where: { id: existing.id },
data: {
ibuHamilAkh: Number(body.ibuHamilAkh),
balitaTerdaftar: Number(body.balitaTerdaftar),
alertStunting: Number(body.alertStunting),
},
data: { targetStuntingPct: Number(body.targetStuntingPct) },
})
: await prisma.ringkasanKesehatanDesa.create({
data: {
ibuHamilAkh: Number(body.ibuHamilAkh),
balitaTerdaftar: Number(body.balitaTerdaftar),
alertStunting: Number(body.alertStunting),
},
data: { targetStuntingPct: Number(body.targetStuntingPct) },
});
return { success: true, message: "Ringkasan kesehatan berhasil disimpan", data };
return { success: true, message: "Target stunting berhasil disimpan", data };
} catch (e) {
console.error("Error di ringkasanKesehatanUpdate:", e);
return { success: false, message: "Gagal menyimpan ringkasan kesehatan" };
return { success: false, message: "Gagal menyimpan target stunting" };
}
}

View File

@@ -2,6 +2,7 @@ import Elysia from "elysia";
import BeasiswaPendaftar from "./beasiswa-pendaftar";
import KeunggulanProgram from "./keunggulan-program";
import BeasiswaConfig from "./beasiswa-config";
import RingkasanBeasiswa from "./ringkasan";
const Beasiswa = new Elysia({
prefix: "/beasiswa",
@@ -10,5 +11,6 @@ const Beasiswa = new Elysia({
.use(BeasiswaPendaftar)
.use(KeunggulanProgram)
.use(BeasiswaConfig)
.use(RingkasanBeasiswa)
export default Beasiswa

View File

@@ -0,0 +1,9 @@
import Elysia from "elysia";
import beasiswaRingkasanStats from "./stats";
const RingkasanBeasiswa = new Elysia({
prefix: "/ringkasan",
tags: ["Pendidikan/Beasiswa Desa"],
}).get("/stats", beasiswaRingkasanStats);
export default RingkasanBeasiswa;

View File

@@ -0,0 +1,35 @@
import prisma from "@/lib/prisma";
type StatsResult = {
jumlahPenerima: number;
danaTersalurkan: string;
tahunAjaran: string;
};
export default async function beasiswaRingkasanStats(): Promise<{
success: boolean;
data?: StatsResult;
message?: string;
}> {
try {
const [jumlahPenerima, config] = await Promise.all([
prisma.beasiswaPendaftar.count({ where: { isActive: true } }),
prisma.beasiswaConfig.findFirst({
where: { isActive: true },
orderBy: { createdAt: "desc" },
}),
]);
return {
success: true,
data: {
jumlahPenerima,
danaTersalurkan: (config?.danaTersalurkan ?? BigInt(0)).toString(),
tahunAjaran: config?.tahunAjaran ?? "-",
},
};
} catch (e) {
console.error("beasiswaRingkasanStats error:", e);
return { success: false, message: "Gagal menghitung statistik beasiswa" };
}
}

View File

@@ -6,6 +6,7 @@ import PendidikanNonFormal from "./pendidikan-non-formal";
import DataPendidikan from "./data-pendidikan";
import Beasiswa from "./beasiswa-desa";
import PerpustakaanDigital from "./perpustakaan-digital";
import RingkasanPendidikan from "./ringkasan";
const Pendidikan = new Elysia({
prefix: "/pendidikan",
@@ -19,5 +20,6 @@ const Pendidikan = new Elysia({
.use(DataPendidikan)
.use(Beasiswa)
.use(PerpustakaanDigital)
.use(RingkasanPendidikan)
export default Pendidikan;

View File

@@ -0,0 +1,9 @@
import Elysia from "elysia";
import pendidikanRingkasanStats from "./stats";
const RingkasanPendidikan = new Elysia({
prefix: "/ringkasan",
tags: ["Pendidikan/Ringkasan"],
}).get("/stats", pendidikanRingkasanStats);
export default RingkasanPendidikan;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
type PerJenjang = { nama: string; jumlahSiswa: number };
type StatsResult = {
perJenjang: PerJenjang[];
jumlahLembaga: number;
jumlahPengajar: number;
};
export default async function pendidikanRingkasanStats(): Promise<{
success: boolean;
data?: StatsResult;
message?: string;
}> {
try {
const [jenjangList, jumlahLembaga, jumlahPengajar] = await Promise.all([
prisma.jenjangPendidikan.findMany({
where: { isActive: true },
include: {
lembagas: {
where: { isActive: true },
include: {
_count: { select: { siswa: { where: { isActive: true } } } },
},
},
},
}),
prisma.lembaga.count({ where: { isActive: true } }),
prisma.pengajar.count({ where: { isActive: true } }),
]);
const perJenjang = jenjangList.map((j) => ({
nama: j.nama,
jumlahSiswa: j.lembagas.reduce((acc, l) => acc + l._count.siswa, 0),
}));
return {
success: true,
data: { perJenjang, jumlahLembaga, jumlahPengajar },
};
} catch (e) {
console.error("pendidikanRingkasanStats error:", e);
return { success: false, message: "Gagal menghitung statistik pendidikan" };
}
}

View File

@@ -47,7 +47,7 @@ export default function WaitingRoom() {
const [error, setError] = useState<string | null>(null);
const [isRedirecting, setIsRedirecting] = useState(false);
const [retryCount, setRetryCount] = useState(0);
// ⏱️ Countdown timer
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
const [hasTimedOut, setHasTimedOut] = useState(false);
@@ -128,7 +128,7 @@ export default function WaitingRoom() {
redirectPath = '/admin/landing-page/profil/program-inovasi';
break;
case "3":
redirectPath = '/admin/kesehatan/posyandu';
redirectPath = '/admin/kesehatan/posyandu/list-posyandu';
break;
case "4":
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
@@ -200,9 +200,9 @@ export default function WaitingRoom() {
Silakan hubungi Superadmin atau coba login ulang nanti.
</Text>
<Group gap="sm" w="100%">
<Button
fullWidth
variant="outline"
<Button
fullWidth
variant="outline"
onClick={handleLogout}
>
Kembali ke Login
@@ -243,11 +243,11 @@ export default function WaitingRoom() {
<Title order={2} c={colors['blue-button']} ta="center">
Menunggu Persetujuan
</Title>
<Text ta="center" c="dimmed">
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
</Text>
<Text ta="center" size="sm" fw={500}>
Nomor: {user?.nomor || '...'}
</Text>
@@ -260,8 +260,8 @@ export default function WaitingRoom() {
{formatTime(timeLeft)}
</Text>
</Group>
<Progress
value={progressValue}
<Progress
value={progressValue}
color={timeLeft < 60 ? 'red' : colors['blue-button']}
size="sm"
animated
@@ -269,15 +269,15 @@ export default function WaitingRoom() {
</Stack>
<Loader size="sm" color={colors['blue-button']} />
<Text ta="center" size="xs" c="dimmed">
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
</Text>
{/* 🚪 Tombol Keluar */}
<Button
variant="subtle"
size="xs"
<Button
variant="subtle"
size="xs"
onClick={handleLogout}
c="dimmed"
>