Compare commits

..

16 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
56 changed files with 4247 additions and 1018 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,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.52",
"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,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

@@ -2546,16 +2546,22 @@ model Balita {
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
model RingkasanKesehatanDesa {
id String @id @default(cuid())
ibuHamilAkh Int @default(0)
balitaTerdaftar Int @default(0)
alertStunting Int @default(0)
imunisasiLengkapPct Int @default(0)
pemeriksaanRutinPct Int @default(0)
giziBaikPct Int @default(0)
targetStuntingPct Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
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

@@ -1,4 +1,3 @@
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
@@ -19,33 +18,7 @@ const intPct = z
.min(0, { message: "Minimal 0" })
.max(100, { message: "Maksimal 100" });
const intCount = z
.number({ invalid_type_error: "Harus berupa angka" })
.int({ message: "Harus bilangan bulat" })
.min(0, { message: "Minimal 0" });
const templateForm = z.object({
ibuHamilAkh: intCount,
balitaTerdaftar: intCount,
alertStunting: intCount,
imunisasiLengkapPct: intPct,
pemeriksaanRutinPct: intPct,
giziBaikPct: intPct,
targetStuntingPct: intPct,
});
const defaultForm = {
ibuHamilAkh: 0,
balitaTerdaftar: 0,
alertStunting: 0,
imunisasiLengkapPct: 0,
pemeriksaanRutinPct: 0,
giziBaikPct: 0,
targetStuntingPct: 0,
};
const ringkasanKesehatanState = proxy({
// Derived stats aggregated from IbuHamil + Balita tables
findStats: {
data: null as StatsData | null,
loading: false,
@@ -57,7 +30,8 @@ const ringkasanKesehatanState = proxy({
const result = await res.json();
ringkasanKesehatanState.findStats.data = result?.data ?? null;
if (result?.data) {
ringkasanKesehatanState.update.form.targetStuntingPct = result.data.targetStuntingPct;
ringkasanKesehatanState.update.form.targetStuntingPct =
result.data.targetStuntingPct;
}
} else {
ringkasanKesehatanState.findStats.data = null;
@@ -71,42 +45,8 @@ const ringkasanKesehatanState = proxy({
},
},
// Kept for backward-compat — now only used internally for targetStuntingPct config
findUnique: {
data: null as Prisma.RingkasanKesehatanDesaGetPayload<object> | null,
loading: false,
async load() {
try {
ringkasanKesehatanState.findUnique.loading = true;
const res = await fetch(`/api/kesehatan/ringkasankesehatan/find`);
if (res.ok) {
const result = await res.json();
ringkasanKesehatanState.findUnique.data = result?.data ?? null;
if (result?.data) {
ringkasanKesehatanState.update.form = {
ibuHamilAkh: result.data.ibuHamilAkh,
balitaTerdaftar: result.data.balitaTerdaftar,
alertStunting: result.data.alertStunting,
imunisasiLengkapPct: result.data.imunisasiLengkapPct,
pemeriksaanRutinPct: result.data.pemeriksaanRutinPct,
giziBaikPct: result.data.giziBaikPct,
targetStuntingPct: result.data.targetStuntingPct,
};
}
} else {
ringkasanKesehatanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching ringkasan kesehatan:", error);
ringkasanKesehatanState.findUnique.data = null;
} finally {
ringkasanKesehatanState.findUnique.loading = false;
}
},
},
update: {
form: { ...defaultForm },
form: { targetStuntingPct: 0 },
loading: false,
async submitTarget() {
const pct = ringkasanKesehatanState.update.form.targetStuntingPct;
@@ -120,7 +60,7 @@ const ringkasanKesehatanState = proxy({
const response = await fetch(`/api/kesehatan/ringkasankesehatan/update`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(ringkasanKesehatanState.update.form),
body: JSON.stringify({ targetStuntingPct: pct }),
});
const result = await response.json();
if (result.success) {
@@ -138,41 +78,6 @@ const ringkasanKesehatanState = proxy({
ringkasanKesehatanState.update.loading = false;
}
},
// Kept for backward-compat (full update)
async submit() {
const cek = templateForm.safeParse(ringkasanKesehatanState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] invalid`;
toast.error(err);
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(ringkasanKesehatanState.update.form),
});
const result = await response.json();
if (result.success) {
toast.success(result.message || "Berhasil disimpan");
await ringkasanKesehatanState.findUnique.load();
return true;
}
throw new Error(result.message || "Gagal menyimpan");
} catch (error) {
console.error("Error updating ringkasan kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal menyimpan");
return false;
} finally {
ringkasanKesehatanState.update.loading = false;
}
},
reset() {
ringkasanKesehatanState.update.form = { ...defaultForm };
},
},
});

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

@@ -1,188 +0,0 @@
'use client';
import colors from '@/con/colors';
import {
ActionIcon,
Badge,
Box,
Button,
Group,
Loader,
Pagination,
Select,
Stack,
Table,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import balitaState from '../../_state/kesehatan/balita/balita';
const STUNTING_COLORS: Record<string, string> = {
NORMAL: 'green',
ALERT: 'yellow',
STUNTING: 'red',
};
export default function BalitaPage() {
const router = useRouter();
const state = useProxy(balitaState);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
useEffect(() => {
state.findMany.load(1, 10, search, statusFilter);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleSearch = () => {
state.findMany.load(1, 10, search, statusFilter);
};
const handleDelete = async (id: string, nama: string) => {
if (!confirm(`Hapus data balita "${nama}"?`)) return;
await state.delete.byId(id);
};
const rows = state.findMany.data?.map((d) => (
<Table.Tr key={d.id}>
<Table.Td>{d.nama}</Table.Td>
<Table.Td>{d.jenisKelamin}</Table.Td>
<Table.Td>
{d.tanggalLahir
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
: '-'}
</Table.Td>
<Table.Td>
<Badge color={d.imunisasiLengkap ? 'green' : 'red'} variant="light">
{d.imunisasiLengkap ? 'Lengkap' : 'Belum'}
</Badge>
</Table.Td>
<Table.Td>
<Badge color={d.giziBaik ? 'green' : 'orange'} variant="light">
{d.giziBaik ? 'Baik' : 'Kurang'}
</Badge>
</Table.Td>
<Table.Td>
<Badge color={d.pemeriksaanRutin ? 'green' : 'orange'} variant="light">
{d.pemeriksaanRutin ? 'Rutin' : 'Tidak'}
</Badge>
</Table.Td>
<Table.Td>
<Badge color={STUNTING_COLORS[d.statusStunting] ?? 'gray'} variant="light">
{d.statusStunting}
</Badge>
</Table.Td>
<Table.Td>
<Group gap="xs">
<ActionIcon
variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/balita/edit/${d.id}`)}
>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon
variant="light"
color="red"
onClick={() => handleDelete(d.id, d.nama)}
loading={state.delete.loading}
>
<IconTrash size={16} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group justify="space-between" mb="md">
<Title order={3} c="black">Balita Terdaftar</Title>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => router.push('/admin/kesehatan/balita/create')}
radius="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
}}
>
Tambah
</Button>
</Group>
<Group mb="md" gap="sm">
<TextInput
placeholder="Cari nama / NIK / ortu..."
leftSection={<IconSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
radius="md"
style={{ flex: 1, maxWidth: 300 }}
/>
<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 ?? '');
state.findMany.load(1, 10, search, v ?? '');
}}
radius="md"
clearable
/>
<Button onClick={handleSearch} radius="md" variant="light">Cari</Button>
</Group>
{state.findMany.loading ? (
<Group justify="center" py="xl"><Loader /></Group>
) : (
<Stack gap="md">
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Nama</Table.Th>
<Table.Th>JK</Table.Th>
<Table.Th>Tgl Lahir</Table.Th>
<Table.Th>Imunisasi</Table.Th>
<Table.Th>Gizi</Table.Th>
<Table.Th>Pemeriksaan</Table.Th>
<Table.Th>Stunting</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows && rows.length > 0 ? rows : (
<Table.Tr>
<Table.Td colSpan={8}>
<Text c="dimmed" ta="center" py="md">Tidak ada data</Text>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
{(state.findMany.totalPages ?? 1) > 1 && (
<Group justify="center">
<Pagination
total={state.findMany.totalPages}
value={state.findMany.page}
onChange={(p) => state.findMany.load(p, 10, search, statusFilter)}
/>
</Group>
)}
</Stack>
)}
</Box>
);
}

View File

@@ -1,170 +0,0 @@
'use client';
import colors from '@/con/colors';
import {
ActionIcon,
Badge,
Box,
Button,
Group,
Loader,
Pagination,
Select,
Stack,
Table,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import ibuHamilState from '../../_state/kesehatan/ibu-hamil/ibuHamil';
const STATUS_COLORS: Record<string, string> = {
AKTIF: 'green',
MELAHIRKAN: 'blue',
KEGUGURAN: 'gray',
NONAKTIF: 'red',
};
export default function IbuHamilPage() {
const router = useRouter();
const state = useProxy(ibuHamilState);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
useEffect(() => {
state.findMany.load(1, 10, search, statusFilter);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleSearch = () => {
state.findMany.load(1, 10, search, statusFilter);
};
const handleDelete = async (id: string, nama: string) => {
if (!confirm(`Hapus data ibu hamil "${nama}"?`)) return;
await state.delete.byId(id);
};
const rows = state.findMany.data?.map((d) => (
<Table.Tr key={d.id}>
<Table.Td>{d.nama}</Table.Td>
<Table.Td>{d.nik || '-'}</Table.Td>
<Table.Td>{d.usiaKehamilan} minggu</Table.Td>
<Table.Td>{d.noHp || '-'}</Table.Td>
<Table.Td>
<Badge color={STATUS_COLORS[d.status] ?? 'gray'} variant="light">
{d.status}
</Badge>
</Table.Td>
<Table.Td>
<Group gap="xs">
<ActionIcon
variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/ibu-hamil/edit/${d.id}`)}
>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon
variant="light"
color="red"
onClick={() => handleDelete(d.id, d.nama)}
loading={state.delete.loading}
>
<IconTrash size={16} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
));
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
<Group justify="space-between" mb="md">
<Title order={3} c="black">Ibu Hamil</Title>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => router.push('/admin/kesehatan/ibu-hamil/create')}
radius="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
}}
>
Tambah
</Button>
</Group>
<Group mb="md" gap="sm">
<TextInput
placeholder="Cari nama / NIK..."
leftSection={<IconSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
radius="md"
style={{ flex: 1, maxWidth: 300 }}
/>
<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 ?? '');
state.findMany.load(1, 10, search, v ?? '');
}}
radius="md"
clearable
/>
<Button onClick={handleSearch} radius="md" variant="light">Cari</Button>
</Group>
{state.findMany.loading ? (
<Group justify="center" py="xl"><Loader /></Group>
) : (
<Stack gap="md">
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Nama</Table.Th>
<Table.Th>NIK</Table.Th>
<Table.Th>Usia Kehamilan</Table.Th>
<Table.Th>No. HP</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows && rows.length > 0 ? rows : (
<Table.Tr>
<Table.Td colSpan={6}>
<Text c="dimmed" ta="center" py="md">Tidak ada data</Text>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
{(state.findMany.totalPages ?? 1) > 1 && (
<Group justify="center">
<Pagination
total={state.findMany.totalPages}
value={state.findMany.page}
onChange={(p) => state.findMany.load(p, 10, search, statusFilter)}
/>
</Group>
)}
</Stack>
)}
</Box>
);
}

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

@@ -1,5 +1,6 @@
'use client';
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
import colors from '@/con/colors';
import {
Box,
@@ -19,7 +20,7 @@ import {
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import balitaState from '../../../_state/kesehatan/balita/balita';
export default function BalitaCreatePage() {
const router = useRouter();
@@ -27,10 +28,10 @@ export default function BalitaCreatePage() {
const form = state.create.form;
const handleSubmit = async () => {
const ok = await state.create.submit();
const ok = await balitaState.create.submit();
if (ok) {
state.create.reset();
router.push('/admin/kesehatan/balita');
balitaState.create.reset();
router.push('/admin/kesehatan/posyandu/balita');
}
};
@@ -51,14 +52,14 @@ export default function BalitaCreatePage() {
required
placeholder="Nama balita"
value={form.nama}
onChange={(e) => { form.nama = e.currentTarget.value; }}
onChange={(e) => { balitaState.create.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { form.nik = e.currentTarget.value; }}
onChange={(e) => { balitaState.create.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
@@ -66,7 +67,7 @@ export default function BalitaCreatePage() {
required
type="date"
value={form.tanggalLahir}
onChange={(e) => { form.tanggalLahir = e.currentTarget.value; }}
onChange={(e) => { balitaState.create.form.tanggalLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
@@ -77,7 +78,7 @@ export default function BalitaCreatePage() {
{ value: 'P', label: 'Perempuan' },
]}
value={form.jenisKelamin}
onChange={(v) => { if (v) form.jenisKelamin = v as typeof form.jenisKelamin; }}
onChange={(v) => { if (v) balitaState.create.form.jenisKelamin = v as typeof form.jenisKelamin; }}
radius="md"
/>
<NumberInput
@@ -86,7 +87,7 @@ export default function BalitaCreatePage() {
min={0}
decimalScale={1}
value={form.beratBadanKg ?? ''}
onChange={(v) => { form.beratBadanKg = v === '' ? undefined : Number(v); }}
onChange={(v) => { balitaState.create.form.beratBadanKg = v === '' ? undefined : Number(v); }}
radius="md"
/>
<NumberInput
@@ -95,21 +96,21 @@ export default function BalitaCreatePage() {
min={0}
decimalScale={1}
value={form.tinggiBadanCm ?? ''}
onChange={(v) => { form.tinggiBadanCm = v === '' ? undefined : Number(v); }}
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) => { form.namaOrtu = e.currentTarget.value; }}
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) => { form.noHpOrtu = e.currentTarget.value; }}
onChange={(e) => { balitaState.create.form.noHpOrtu = e.currentTarget.value; }}
radius="md"
/>
<Select
@@ -121,7 +122,7 @@ export default function BalitaCreatePage() {
{ value: 'STUNTING', label: 'Stunting' },
]}
value={form.statusStunting}
onChange={(v) => { if (v) form.statusStunting = v as typeof form.statusStunting; }}
onChange={(v) => { if (v) balitaState.create.form.statusStunting = v as typeof form.statusStunting; }}
radius="md"
/>
</SimpleGrid>
@@ -130,7 +131,7 @@ export default function BalitaCreatePage() {
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { form.alamat = e.currentTarget.value; }}
onChange={(e) => { balitaState.create.form.alamat = e.currentTarget.value; }}
radius="md"
/>
@@ -138,17 +139,17 @@ export default function BalitaCreatePage() {
<Checkbox
label="Imunisasi Lengkap"
checked={form.imunisasiLengkap}
onChange={(e) => { form.imunisasiLengkap = e.currentTarget.checked; }}
onChange={(e) => { balitaState.create.form.imunisasiLengkap = e.currentTarget.checked; }}
/>
<Checkbox
label="Gizi Baik"
checked={form.giziBaik}
onChange={(e) => { form.giziBaik = e.currentTarget.checked; }}
onChange={(e) => { balitaState.create.form.giziBaik = e.currentTarget.checked; }}
/>
<Checkbox
label="Pemeriksaan Rutin"
checked={form.pemeriksaanRutin}
onChange={(e) => { form.pemeriksaanRutin = e.currentTarget.checked; }}
onChange={(e) => { balitaState.create.form.pemeriksaanRutin = e.currentTarget.checked; }}
/>
</Group>
@@ -156,7 +157,7 @@ export default function BalitaCreatePage() {
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { form.catatan = e.currentTarget.value; }}
onChange={(e) => { balitaState.create.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>

View File

@@ -1,5 +1,6 @@
'use client';
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
import colors from '@/con/colors';
import {
Box,
@@ -20,7 +21,7 @@ import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import balitaState from '../../../../_state/kesehatan/balita/balita';
export default function BalitaEditPage() {
const router = useRouter();
@@ -30,12 +31,12 @@ export default function BalitaEditPage() {
const form = state.edit.form;
useEffect(() => {
if (id) state.edit.load(id);
if (id) balitaState.edit.load(id);
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const handleSubmit = async () => {
const ok = await state.edit.update();
if (ok) router.push('/admin/kesehatan/balita');
const ok = await balitaState.edit.update();
if (ok) router.push('/admin/kesehatan/posyandu/balita');
};
return (
@@ -55,14 +56,14 @@ export default function BalitaEditPage() {
required
placeholder="Nama balita"
value={form.nama}
onChange={(e) => { form.nama = e.currentTarget.value; }}
onChange={(e) => { balitaState.edit.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { form.nik = e.currentTarget.value; }}
onChange={(e) => { balitaState.edit.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
@@ -70,7 +71,7 @@ export default function BalitaEditPage() {
required
type="date"
value={form.tanggalLahir}
onChange={(e) => { form.tanggalLahir = e.currentTarget.value; }}
onChange={(e) => { balitaState.edit.form.tanggalLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
@@ -81,7 +82,7 @@ export default function BalitaEditPage() {
{ value: 'P', label: 'Perempuan' },
]}
value={form.jenisKelamin}
onChange={(v) => { if (v) form.jenisKelamin = v as typeof form.jenisKelamin; }}
onChange={(v) => { if (v) balitaState.edit.form.jenisKelamin = v as typeof form.jenisKelamin; }}
radius="md"
/>
<NumberInput
@@ -90,7 +91,7 @@ export default function BalitaEditPage() {
min={0}
decimalScale={1}
value={form.beratBadanKg ?? ''}
onChange={(v) => { form.beratBadanKg = v === '' ? undefined : Number(v); }}
onChange={(v) => { balitaState.edit.form.beratBadanKg = v === '' ? undefined : Number(v); }}
radius="md"
/>
<NumberInput
@@ -99,21 +100,21 @@ export default function BalitaEditPage() {
min={0}
decimalScale={1}
value={form.tinggiBadanCm ?? ''}
onChange={(v) => { form.tinggiBadanCm = v === '' ? undefined : Number(v); }}
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) => { form.namaOrtu = e.currentTarget.value; }}
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) => { form.noHpOrtu = e.currentTarget.value; }}
onChange={(e) => { balitaState.edit.form.noHpOrtu = e.currentTarget.value; }}
radius="md"
/>
<Select
@@ -125,7 +126,7 @@ export default function BalitaEditPage() {
{ value: 'STUNTING', label: 'Stunting' },
]}
value={form.statusStunting}
onChange={(v) => { if (v) form.statusStunting = v as typeof form.statusStunting; }}
onChange={(v) => { if (v) balitaState.edit.form.statusStunting = v as typeof form.statusStunting; }}
radius="md"
/>
</SimpleGrid>
@@ -134,7 +135,7 @@ export default function BalitaEditPage() {
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { form.alamat = e.currentTarget.value; }}
onChange={(e) => { balitaState.edit.form.alamat = e.currentTarget.value; }}
radius="md"
/>
@@ -142,17 +143,17 @@ export default function BalitaEditPage() {
<Checkbox
label="Imunisasi Lengkap"
checked={form.imunisasiLengkap}
onChange={(e) => { form.imunisasiLengkap = e.currentTarget.checked; }}
onChange={(e) => { balitaState.edit.form.imunisasiLengkap = e.currentTarget.checked; }}
/>
<Checkbox
label="Gizi Baik"
checked={form.giziBaik}
onChange={(e) => { form.giziBaik = e.currentTarget.checked; }}
onChange={(e) => { balitaState.edit.form.giziBaik = e.currentTarget.checked; }}
/>
<Checkbox
label="Pemeriksaan Rutin"
checked={form.pemeriksaanRutin}
onChange={(e) => { form.pemeriksaanRutin = e.currentTarget.checked; }}
onChange={(e) => { balitaState.edit.form.pemeriksaanRutin = e.currentTarget.checked; }}
/>
</Group>
@@ -160,7 +161,7 @@ export default function BalitaEditPage() {
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { form.catatan = e.currentTarget.value; }}
onChange={(e) => { balitaState.edit.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>

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

@@ -1,5 +1,6 @@
'use client';
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
import colors from '@/con/colors';
import {
Box,
@@ -17,7 +18,7 @@ import {
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import ibuHamilState from '../../../_state/kesehatan/ibu-hamil/ibuHamil';
export default function IbuHamilCreatePage() {
const router = useRouter();
@@ -25,10 +26,10 @@ export default function IbuHamilCreatePage() {
const form = state.create.form;
const handleSubmit = async () => {
const ok = await state.create.submit();
const ok = await ibuHamilState.create.submit();
if (ok) {
state.create.reset();
router.push('/admin/kesehatan/ibu-hamil');
ibuHamilState.create.reset();
router.push('/admin/kesehatan/posyandu/ibu-hamil');
}
};
@@ -49,14 +50,14 @@ export default function IbuHamilCreatePage() {
required
placeholder="Nama ibu hamil"
value={form.nama}
onChange={(e) => { form.nama = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.create.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { form.nik = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.create.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
@@ -64,28 +65,28 @@ export default function IbuHamilCreatePage() {
type="number"
placeholder="0"
value={String(form.usiaKehamilan)}
onChange={(e) => { form.usiaKehamilan = Number(e.currentTarget.value) || 0; }}
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) => { form.noHp = e.currentTarget.value; }}
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) => { form.hpht = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.create.form.hpht = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Taksiran Persalinan"
type="date"
value={form.taksiranLahir}
onChange={(e) => { form.taksiranLahir = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.create.form.taksiranLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
@@ -98,7 +99,7 @@ export default function IbuHamilCreatePage() {
{ value: 'NONAKTIF', label: 'Nonaktif' },
]}
value={form.status}
onChange={(v) => { if (v) form.status = v as typeof form.status; }}
onChange={(v) => { if (v) ibuHamilState.create.form.status = v as typeof form.status; }}
radius="md"
/>
</SimpleGrid>
@@ -107,7 +108,7 @@ export default function IbuHamilCreatePage() {
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { form.alamat = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.create.form.alamat = e.currentTarget.value; }}
radius="md"
/>
@@ -115,7 +116,7 @@ export default function IbuHamilCreatePage() {
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { form.catatan = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.create.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>

View File

@@ -1,5 +1,6 @@
'use client';
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
import colors from '@/con/colors';
import {
Box,
@@ -18,7 +19,6 @@ import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import ibuHamilState from '../../../../_state/kesehatan/ibu-hamil/ibuHamil';
export default function IbuHamilEditPage() {
const router = useRouter();
@@ -28,12 +28,12 @@ export default function IbuHamilEditPage() {
const form = state.edit.form;
useEffect(() => {
if (id) state.edit.load(id);
if (id) ibuHamilState.edit.load(id);
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const handleSubmit = async () => {
const ok = await state.edit.update();
if (ok) router.push('/admin/kesehatan/ibu-hamil');
const ok = await ibuHamilState.edit.update();
if (ok) router.push('/admin/kesehatan/posyandu/ibu-hamil');
};
return (
@@ -53,14 +53,14 @@ export default function IbuHamilEditPage() {
required
placeholder="Nama ibu hamil"
value={form.nama}
onChange={(e) => { form.nama = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.nama = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="NIK"
placeholder="Nomor Induk Kependudukan"
value={form.nik}
onChange={(e) => { form.nik = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.nik = e.currentTarget.value; }}
radius="md"
/>
<TextInput
@@ -68,28 +68,28 @@ export default function IbuHamilEditPage() {
type="number"
placeholder="0"
value={String(form.usiaKehamilan)}
onChange={(e) => { form.usiaKehamilan = Number(e.currentTarget.value) || 0; }}
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) => { form.noHp = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.noHp = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="HPHT"
type="date"
value={form.hpht}
onChange={(e) => { form.hpht = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.hpht = e.currentTarget.value; }}
radius="md"
/>
<TextInput
label="Taksiran Persalinan"
type="date"
value={form.taksiranLahir}
onChange={(e) => { form.taksiranLahir = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.taksiranLahir = e.currentTarget.value; }}
radius="md"
/>
<Select
@@ -102,7 +102,7 @@ export default function IbuHamilEditPage() {
{ value: 'NONAKTIF', label: 'Nonaktif' },
]}
value={form.status}
onChange={(v) => { if (v) form.status = v as typeof form.status; }}
onChange={(v) => { if (v) ibuHamilState.edit.form.status = v as typeof form.status; }}
radius="md"
/>
</SimpleGrid>
@@ -111,7 +111,7 @@ export default function IbuHamilEditPage() {
label="Alamat"
placeholder="Alamat lengkap"
value={form.alamat}
onChange={(e) => { form.alamat = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.alamat = e.currentTarget.value; }}
radius="md"
/>
@@ -119,7 +119,7 @@ export default function IbuHamilEditPage() {
label="Catatan"
placeholder="Catatan tambahan"
value={form.catatan}
onChange={(e) => { form.catatan = e.currentTarget.value; }}
onChange={(e) => { ibuHamilState.edit.form.catatan = e.currentTarget.value; }}
radius="md"
rows={3}
/>

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

@@ -24,9 +24,10 @@ import {
IconAlertTriangle,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useCallback } from 'react';
import { useProxy } from 'valtio/utils';
import ringkasanKesehatanState from '../../_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan';
import ringkasanKesehatanState from '../../../_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan';
type StatCardProps = {
label: string;
@@ -71,13 +72,8 @@ export default function RingkasanKesehatanPage() {
const state = useProxy(ringkasanKesehatanState);
const stats = state.findStats.data;
useEffect(() => {
state.findStats.load();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleSaveTarget = async () => {
await state.update.submitTarget();
};
const loadStats = useCallback(() => { ringkasanKesehatanState.findStats.load(); }, []);
useEffect(() => { loadStats(); }, [loadStats]);
const isLoading = state.findStats.loading;
@@ -175,7 +171,7 @@ export default function RingkasanKesehatanPage() {
style={{ flex: 1 }}
/>
<Button
onClick={handleSaveTarget}
onClick={() => state.update.submitTarget()}
radius="md"
disabled={state.update.loading}
style={{
@@ -202,7 +198,7 @@ export default function RingkasanKesehatanPage() {
color="pink"
radius="md"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push('/admin/kesehatan/ibu-hamil')}
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil')}
>
Kelola Ibu Hamil
</Button>
@@ -211,7 +207,7 @@ export default function RingkasanKesehatanPage() {
color="blue"
radius="md"
rightSection={<IconArrowRight size={16} />}
onClick={() => router.push('/admin/kesehatan/balita')}
onClick={() => router.push('/admin/kesehatan/posyandu/balita')}
>
Kelola Balita
</Button>

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",
@@ -166,21 +171,6 @@ export const devBar = [
id: "Kesehatan_7",
name: "Info Wabah/Penyakit",
path: "/admin/kesehatan/info-wabah-penyakit"
},
{
id: "Kesehatan_8",
name: "Ringkasan Kesehatan",
path: "/admin/kesehatan/ringkasan-kesehatan"
},
{
id: "Kesehatan_9",
name: "Ibu Hamil",
path: "/admin/kesehatan/ibu-hamil"
},
{
id: "Kesehatan_10",
name: "Balita",
path: "/admin/kesehatan/balita"
}
]
},
@@ -574,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"
}
]
@@ -586,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",
@@ -617,21 +612,6 @@ export const navBar = [
id: "Kesehatan_7",
name: "Info Wabah/Penyakit",
path: "/admin/kesehatan/info-wabah-penyakit"
},
{
id: "Kesehatan_8",
name: "Ringkasan Kesehatan",
path: "/admin/kesehatan/ringkasan-kesehatan"
},
{
id: "Kesehatan_9",
name: "Ibu Hamil",
path: "/admin/kesehatan/ibu-hamil"
},
{
id: "Kesehatan_10",
name: "Balita",
path: "/admin/kesehatan/balita"
}
]
},
@@ -1040,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"
}
]
@@ -1228,7 +1213,7 @@ export const role1 = [
}
]
},
{
{
id: "Kependudukan",
name: "Kependudukan",
path: "",
@@ -1271,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",
@@ -1302,21 +1287,6 @@ export const role2 = [
id: "Kesehatan_7",
name: "Info Wabah/Penyakit",
path: "/admin/kesehatan/info-wabah-penyakit"
},
{
id: "Kesehatan_8",
name: "Ringkasan Kesehatan",
path: "/admin/kesehatan/ringkasan-kesehatan"
},
{
id: "Kesehatan_9",
name: "Ibu Hamil",
path: "/admin/kesehatan/ibu-hamil"
},
{
id: "Kesehatan_10",
name: "Balita",
path: "/admin/kesehatan/balita"
}
]
},

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

@@ -8,12 +8,6 @@ const RingkasanKesehatan = new Elysia({ prefix: "/ringkasankesehatan", tags: ["K
.get("/stats", ringkasanKesehatanStats)
.put("/update", ringkasanKesehatanUpdate, {
body: t.Object({
ibuHamilAkh: t.Number(),
balitaTerdaftar: t.Number(),
alertStunting: t.Number(),
imunisasiLengkapPct: t.Number({ minimum: 0, maximum: 100 }),
pemeriksaanRutinPct: t.Number({ minimum: 0, maximum: 100 }),
giziBaikPct: t.Number({ minimum: 0, maximum: 100 }),
targetStuntingPct: t.Number({ minimum: 0, maximum: 100 }),
}),
});

View File

@@ -1,45 +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),
imunisasiLengkapPct: Number(body.imunisasiLengkapPct),
pemeriksaanRutinPct: Number(body.pemeriksaanRutinPct),
giziBaikPct: Number(body.giziBaikPct),
targetStuntingPct: Number(body.targetStuntingPct),
},
data: { targetStuntingPct: Number(body.targetStuntingPct) },
})
: await prisma.ringkasanKesehatanDesa.create({
data: {
ibuHamilAkh: Number(body.ibuHamilAkh),
balitaTerdaftar: Number(body.balitaTerdaftar),
alertStunting: Number(body.alertStunting),
imunisasiLengkapPct: Number(body.imunisasiLengkapPct),
pemeriksaanRutinPct: Number(body.pemeriksaanRutinPct),
giziBaikPct: Number(body.giziBaikPct),
targetStuntingPct: Number(body.targetStuntingPct),
},
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"
>