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>
This commit is contained in:
706
STRUKTUR.md
706
STRUKTUR.md
@@ -6,25 +6,25 @@
|
|||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
|
|
||||||
| Kategori | Teknologi |
|
| Kategori | Teknologi |
|
||||||
|----------|-----------|
|
| -------------------- | ------------------------------------------ |
|
||||||
| **Framework** | Next.js 15 (App Router) |
|
| **Framework** | Next.js 15 (App Router) |
|
||||||
| **Language** | TypeScript (strict mode) |
|
| **Language** | TypeScript (strict mode) |
|
||||||
| **Runtime** | Bun |
|
| **Runtime** | Bun |
|
||||||
| **Backend API** | Elysia.js (high-performance HTTP server) |
|
| **Backend API** | Elysia.js (high-performance HTTP server) |
|
||||||
| **Database** | PostgreSQL |
|
| **Database** | PostgreSQL |
|
||||||
| **ORM** | Prisma 6.3.1 |
|
| **ORM** | Prisma 6.3.1 |
|
||||||
| **UI Framework** | Mantine UI v7-v8 |
|
| **UI Framework** | Mantine UI v7-v8 |
|
||||||
| **State Management** | Jotai + Valtio + SWR |
|
| **State Management** | Jotai + Valtio + SWR |
|
||||||
| **Authentication** | iron-session + JWT (@elysiajs/jwt) |
|
| **Authentication** | iron-session + JWT (@elysiajs/jwt) |
|
||||||
| **File Storage** | Seafile (self-hosted) |
|
| **File Storage** | Seafile (self-hosted) |
|
||||||
| **Text Editor** | Tiptap (Rich text editor) |
|
| **Text Editor** | Tiptap (Rich text editor) |
|
||||||
| **Charts** | Recharts + Chart.js |
|
| **Charts** | Recharts + Chart.js |
|
||||||
| **Maps** | Leaflet + react-leaflet |
|
| **Maps** | Leaflet + react-leaflet |
|
||||||
| **Testing** | Vitest (unit) + Playwright (E2E) |
|
| **Testing** | Vitest (unit) + Playwright (E2E) |
|
||||||
| **Styling** | Mantine + PostCSS + Framer Motion |
|
| **Styling** | Mantine + PostCSS + Framer Motion |
|
||||||
| **Deployment** | Docker + GHCR + Portainer + GitHub Actions |
|
| **Deployment** | Docker + GHCR + Portainer + GitHub Actions |
|
||||||
| **Version** | 0.1.11 |
|
| **Version** | 0.1.11 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -195,137 +195,148 @@ Browser
|
|||||||
## 4. Modul Domain
|
## 4. Modul Domain
|
||||||
|
|
||||||
### A. PPID (Pejabat Pengelola Informasi dan Dokumentasi)
|
### A. PPID (Pejabat Pengelola Informasi dan Dokumentasi)
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/ppid/` dan `src/app/darmasaba/(pages)/ppid/`
|
**Lokasi**: `src/app/admin/(dashboard)/ppid/` dan `src/app/darmasaba/(pages)/ppid/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| --------------------------- | ---------------------------------------------- |
|
||||||
| Profil PPID | Profil pejabat pengelola informasi |
|
| Profil PPID | Profil pejabat pengelola informasi |
|
||||||
| Struktur PPID | Struktur organisasi PPID dengan hierarki |
|
| Struktur PPID | Struktur organisasi PPID dengan hierarki |
|
||||||
| Visi & Misi PPID | Visi dan misi PPID desa |
|
| Visi & Misi PPID | Visi dan misi PPID desa |
|
||||||
| Daftar Informasi Publik | Katalog informasi publik yang tersedia |
|
| Daftar Informasi Publik | Katalog informasi publik yang tersedia |
|
||||||
| Dasar Hukum | Regulasi dan dasar hukum PPID |
|
| Dasar Hukum | Regulasi dan dasar hukum PPID |
|
||||||
| Permohonan Informasi Publik | Form permohonan informasi (NIK, kontak, jenis) |
|
| Permohonan Informasi Publik | Form permohonan informasi (NIK, kontak, jenis) |
|
||||||
| Permohonan Keberatan | Formulir keberatan informasi |
|
| Permohonan Keberatan | Formulir keberatan informasi |
|
||||||
| Indeks Kepuasan Masyarakat | Survey kepuasan dengan grafik demografis |
|
| Indeks Kepuasan Masyarakat | Survey kepuasan dengan grafik demografis |
|
||||||
|
|
||||||
### B. Desa (Landing Page & Umum)
|
### B. Desa (Landing Page & Umum)
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/desa/` dan `src/app/darmasaba/(pages)/desa/`
|
**Lokasi**: `src/app/admin/(dashboard)/desa/` dan `src/app/darmasaba/(pages)/desa/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| -------------------------- | ---------------------------------------------- |
|
||||||
| Profil Desa | Sejarah, visi-misi, lambang, maskot |
|
| Profil Desa | Sejarah, visi-misi, lambang, maskot |
|
||||||
| Profil Perbekel | Biodata, pengalaman, program unggulan perbekel |
|
| Profil Perbekel | Biodata, pengalaman, program unggulan perbekel |
|
||||||
| Perbekel dari Masa ke Masa | Historis perbekel per periode |
|
| Perbekel dari Masa ke Masa | Historis perbekel per periode |
|
||||||
| Berita | Artikel berita dengan kategori & multi-image |
|
| Berita | Artikel berita dengan kategori & multi-image |
|
||||||
| Gallery | Foto dan video galeri |
|
| Gallery | Foto dan video galeri |
|
||||||
| Pengumuman | Pengumuman desa dengan kategori |
|
| Pengumuman | Pengumuman desa dengan kategori |
|
||||||
| Potensi Desa | Potensi desa dengan kategori |
|
| Potensi Desa | Potensi desa dengan kategori |
|
||||||
| Layanan Desa | Surat keterangan, ajukan permohonan |
|
| Layanan Desa | Surat keterangan, ajukan permohonan |
|
||||||
| Penghargaan | Prestasi dan penghargaan desa |
|
| Penghargaan | Prestasi dan penghargaan desa |
|
||||||
| Desa Anti Korupsi | Transparansi anti-korupsi |
|
| Desa Anti Korupsi | Transparansi anti-korupsi |
|
||||||
| SDGs Desa | Sustainable Development Goals desa |
|
| SDGs Desa | Sustainable Development Goals desa |
|
||||||
| APBDes | Anggaran desa dengan hierarki item & realisasi |
|
| APBDes | Anggaran desa dengan hierarki item & realisasi |
|
||||||
| Prestasi Desa | Katalog prestasi |
|
| Prestasi Desa | Katalog prestasi |
|
||||||
|
|
||||||
### C. Kesehatan
|
### C. Kesehatan
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/kesehatan/` dan `src/app/darmasaba/(pages)/kesehatan/`
|
**Lokasi**: `src/app/admin/(dashboard)/kesehatan/` dan `src/app/darmasaba/(pages)/kesehatan/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| -------------------- | ---------------------------------------------- |
|
||||||
| Fasilitas Kesehatan | Info rumah sakit/klinik (jam, dokter, tarif) |
|
| Fasilitas Kesehatan | Info rumah sakit/klinik (jam, dokter, tarif) |
|
||||||
| Puskesmas | Data puskesmas dengan jam operasional & kontak |
|
| Puskesmas | Data puskesmas dengan jam operasional & kontak |
|
||||||
| Posyandu | Jadwal dan informasi posyandu |
|
| Posyandu | Jadwal dan informasi posyandu |
|
||||||
| Program Kesehatan | Program-program kesehatan desa |
|
| Program Kesehatan | Program-program kesehatan desa |
|
||||||
| Penanganan Darurat | Prosedur penanganan darurat |
|
| Penanganan Darurat | Prosedur penanganan darurat |
|
||||||
| Kontak Darurat | Kontak emergency dengan WhatsApp |
|
| Kontak Darurat | Kontak emergency dengan WhatsApp |
|
||||||
| Info Wabah Penyakit | Informasi wabah penyakit |
|
| Info Wabah Penyakit | Informasi wabah penyakit |
|
||||||
| Artikel Kesehatan | Artikel kesehatan lengkap |
|
| Artikel Kesehatan | Artikel kesehatan lengkap |
|
||||||
| Data Kesehatan Warga | Statistik kesehatan warga |
|
| Data Kesehatan Warga | Statistik kesehatan warga |
|
||||||
| Kelahiran & Kematian | Data vital statistik |
|
| Kelahiran & Kematian | Data vital statistik |
|
||||||
| Grafik Kepuasan | Grafik kepuasan layanan kesehatan |
|
| Grafik Kepuasan | Grafik kepuasan layanan kesehatan |
|
||||||
|
|
||||||
### D. Ekonomi
|
### D. Ekonomi
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/ekonomi/` dan `src/app/darmasaba/(pages)/ekonomi/`
|
**Lokasi**: `src/app/admin/(dashboard)/ekonomi/` dan `src/app/darmasaba/(pages)/ekonomi/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| ------------------------------ | ------------------------------------------ |
|
||||||
| Pasar Desa | Katalog pasar desa dengan produk & rating |
|
| Pasar Desa | Katalog pasar desa dengan produk & rating |
|
||||||
| Struktur BUMDes | Organisasi BUMDes dengan pengurus |
|
| Struktur BUMDes | Organisasi BUMDes dengan pengurus |
|
||||||
| APBDes (PADesa) | Pendapatan Asli Desa |
|
| APBDes (PADesa) | Pendapatan Asli Desa |
|
||||||
| Program Kemiskinan | Program dan statistik kemiskinan |
|
| Program Kemiskinan | Program dan statistik kemiskinan |
|
||||||
| Sektor Unggulan | Sektor ekonomi unggulan desa |
|
| Sektor Unggulan | Sektor ekonomi unggulan desa |
|
||||||
| Lowongan Kerja Lokal | Info lowongan pekerjaan |
|
| Lowongan Kerja Lokal | Info lowongan pekerjaan |
|
||||||
| Demografi Pekerjaan | Distribusi pekerjaan penduduk |
|
| Demografi Pekerjaan | Distribusi pekerjaan penduduk |
|
||||||
| Jumlah Pengangguran | Statistik pengangguran |
|
| Jumlah Pengangguran | Statistik pengangguran |
|
||||||
| Penduduk Usia Kerja Menganggur | Analisis pengangguran by usia & pendidikan |
|
| Penduduk Usia Kerja Menganggur | Analisis pengangguran by usia & pendidikan |
|
||||||
| Jumlah Penduduk Miskin | Tren kemiskinan tahunan |
|
| Jumlah Penduduk Miskin | Tren kemiskinan tahunan |
|
||||||
|
|
||||||
### E. Kependudukan
|
### E. Kependudukan
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/kependudukan/` dan `src/app/darmasaba/(pages)/kependudukan/`
|
**Lokasi**: `src/app/admin/(dashboard)/kependudukan/` dan `src/app/darmasaba/(pages)/kependudukan/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| ----------------- | -------------------------------------- |
|
||||||
| Data Banjar | Data penduduk per banjar |
|
| Data Banjar | Data penduduk per banjar |
|
||||||
| Distribusi Agama | Statistik agama penduduk |
|
| Distribusi Agama | Statistik agama penduduk |
|
||||||
| Distribusi Umur | Piramida umur penduduk |
|
| Distribusi Umur | Piramida umur penduduk |
|
||||||
| Migrasi Penduduk | Data migrasi masuk/keluar |
|
| Migrasi Penduduk | Data migrasi masuk/keluar |
|
||||||
| Dinamika Penduduk | Kelahiran, kematian, migrasi per tahun |
|
| Dinamika Penduduk | Kelahiran, kematian, migrasi per tahun |
|
||||||
|
|
||||||
### F. Pendidikan
|
### F. Pendidikan
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/pendidikan/` dan `src/app/darmasaba/(pages)/pendidikan/`
|
**Lokasi**: `src/app/admin/(dashboard)/pendidikan/` dan `src/app/darmasaba/(pages)/pendidikan/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| ----------------------- | ------------------------------------------- |
|
||||||
| Info Sekolah & PAUD | Data sekolah per jenjang (TK, SD, SMP, SMA) |
|
| Info Sekolah & PAUD | Data sekolah per jenjang (TK, SD, SMP, SMA) |
|
||||||
| Beasiswa Desa | Program beasiswa & pendaftar |
|
| Beasiswa Desa | Program beasiswa & pendaftar |
|
||||||
| Program Pendidikan Anak | Program pendidikan anak |
|
| Program Pendidikan Anak | Program pendidikan anak |
|
||||||
| Bimbingan Belajar | Informasi bimbingan belajar |
|
| Bimbingan Belajar | Informasi bimbingan belajar |
|
||||||
| Pendidikan Non Formal | Tempat & program non-formal |
|
| Pendidikan Non Formal | Tempat & program non-formal |
|
||||||
| Perpustakaan Digital | Katalog buku & peminjaman |
|
| Perpustakaan Digital | Katalog buku & peminjaman |
|
||||||
| Data Pendidikan | Statistik pendidikan |
|
| Data Pendidikan | Statistik pendidikan |
|
||||||
|
|
||||||
### G. Keamanan
|
### G. Keamanan
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/keamanan/` dan `src/app/darmasaba/(pages)/keamanan/`
|
**Lokasi**: `src/app/admin/(dashboard)/keamanan/` dan `src/app/darmasaba/(pages)/keamanan/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| ------------------------------------- | ----------------------------------------- |
|
||||||
| Keamanan Lingkungan (Pecalang/Patwal) | Sistem keamanan tradisional Bali |
|
| Keamanan Lingkungan (Pecalang/Patwal) | Sistem keamanan tradisional Bali |
|
||||||
| Polsek Terdekat | Data polsek dengan layanan & map |
|
| Polsek Terdekat | Data polsek dengan layanan & map |
|
||||||
| Kontak Darurat | Kontak darurat keamanan |
|
| Kontak Darurat | Kontak darurat keamanan |
|
||||||
| Pencegahan Kriminalitas | Info pencegahan kriminal |
|
| Pencegahan Kriminalitas | Info pencegahan kriminal |
|
||||||
| Laporan Publik | Laporan masyarakat dengan tracking status |
|
| Laporan Publik | Laporan masyarakat dengan tracking status |
|
||||||
| Tips Keamanan | Tips dan panduan keamanan |
|
| Tips Keamanan | Tips dan panduan keamanan |
|
||||||
|
|
||||||
### H. Lingkungan
|
### H. Lingkungan
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/lingkungan/` dan `src/app/darmasaba/(pages)/lingkungan/`
|
**Lokasi**: `src/app/admin/(dashboard)/lingkungan/` dan `src/app/darmasaba/(pages)/lingkungan/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| -------------------- | --------------------------------- |
|
||||||
| Pengelolaan Sampah | Bank sampah & pengelolaan |
|
| Pengelolaan Sampah | Bank sampah & pengelolaan |
|
||||||
| Program Penghijauan | Program penghijauan desa |
|
| Program Penghijauan | Program penghijauan desa |
|
||||||
| Data Lingkungan | Data lingkungan desa |
|
| Data Lingkungan | Data lingkungan desa |
|
||||||
| Gotong Royong | Kegiatan gotong royong |
|
| Gotong Royong | Kegiatan gotong royong |
|
||||||
| Edukasi Lingkungan | Edukasi lingkungan hidup |
|
| Edukasi Lingkungan | Edukasi lingkungan hidup |
|
||||||
| Konservasi Adat Bali | Tri Hita Karana & konservasi adat |
|
| Konservasi Adat Bali | Tri Hita Karana & konservasi adat |
|
||||||
|
|
||||||
### I. Inovasi
|
### I. Inovasi
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/inovasi/` dan `src/app/darmasaba/(pages)/inovasi/`
|
**Lokasi**: `src/app/admin/(dashboard)/inovasi/` dan `src/app/darmasaba/(pages)/inovasi/`
|
||||||
|
|
||||||
| Sub-modul | Deskripsi |
|
| Sub-modul | Deskripsi |
|
||||||
|-----------|-----------|
|
| ---------------------------- | ----------------------------- |
|
||||||
| Desa Digital (Smart Village) | Transformasi digital desa |
|
| Desa Digital (Smart Village) | Transformasi digital desa |
|
||||||
| Program Kreatif Desa | Program kreatif & inovatif |
|
| Program Kreatif Desa | Program kreatif & inovatif |
|
||||||
| Kolaborasi Inovasi | Kolaborasi dengan mitra |
|
| Kolaborasi Inovasi | Kolaborasi dengan mitra |
|
||||||
| Info Teknologi Tepat Guna | Info teknologi untuk desa |
|
| Info Teknologi Tepat Guna | Info teknologi untuk desa |
|
||||||
| Ajukan Ide Inovatif | Form pengajuan ide dari warga |
|
| Ajukan Ide Inovatif | Form pengajuan ide dari warga |
|
||||||
| Layanan Online Desa | Layanan administrasi online |
|
| Layanan Online Desa | Layanan administrasi online |
|
||||||
|
|
||||||
### J. Musik Desa
|
### J. Musik Desa
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/musik/` dan `src/app/darmasaba/(pages)/musik/`
|
**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.
|
Model `MusikDesa` dengan audio file, cover image, genre, dan durasi. Dilengkapi dengan `FixedPlayerBar` di layout publik.
|
||||||
|
|
||||||
### K. User & Role (Admin)
|
### K. User & Role (Admin)
|
||||||
|
|
||||||
**Lokasi**: `src/app/admin/(dashboard)/user&role/`
|
**Lokasi**: `src/app/admin/(dashboard)/user&role/`
|
||||||
|
|
||||||
- **Role-based Access Control**: Role dengan permission JSON
|
- **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
|
### Core Models
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| -------------------------------------------------- | ----------------------------------------------- |
|
||||||
| `FileStorage` | Central file storage untuk semua uploaded files |
|
| `FileStorage` | Central file storage untuk semua uploaded files |
|
||||||
| `AppMenu` / `AppMenuChild` | Menu navigasi aplikasi |
|
| `AppMenu` / `AppMenuChild` | Menu navigasi aplikasi |
|
||||||
| `User` / `Role` / `UserSession` / `UserMenuAccess` | Sistem autentikasi & otorisasi |
|
| `User` / `Role` / `UserSession` / `UserMenuAccess` | Sistem autentikasi & otorisasi |
|
||||||
| `KodeOtp` | OTP codes untuk login |
|
| `KodeOtp` | OTP codes untuk login |
|
||||||
|
|
||||||
### Landing Page & Desa
|
### Landing Page & Desa
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| --------------------------------------------- | ---------------------------------------------- |
|
||||||
| `PejabatDesa` | Pejabat desa dengan foto |
|
| `PejabatDesa` | Pejabat desa dengan foto |
|
||||||
| `ProfilPerbekel` | Profil perbekel (biodata, pengalaman, program) |
|
| `ProfilPerbekel` | Profil perbekel (biodata, pengalaman, program) |
|
||||||
| `PerbekelDariMasaKeMasa` | Historis perbekel |
|
| `PerbekelDariMasaKeMasa` | Historis perbekel |
|
||||||
| `Berita` / `KategoriBerita` | Berita desa |
|
| `Berita` / `KategoriBerita` | Berita desa |
|
||||||
| `PotensiDesa` / `KategoriPotensi` | Potensi desa |
|
| `PotensiDesa` / `KategoriPotensi` | Potensi desa |
|
||||||
| `Pengumuman` / `CategoryPengumuman` | Pengumuman |
|
| `Pengumuman` / `CategoryPengumuman` | Pengumuman |
|
||||||
| `GalleryFoto` / `GalleryVideo` | Gallery media |
|
| `GalleryFoto` / `GalleryVideo` | Gallery media |
|
||||||
| `Penghargaan` | Penghargaan desa |
|
| `Penghargaan` | Penghargaan desa |
|
||||||
| `APBDes` / `APBDesItem` / `RealisasiItem` | Anggaran dengan realisasi |
|
| `APBDes` / `APBDesItem` / `RealisasiItem` | Anggaran dengan realisasi |
|
||||||
| `DesaAntiKorupsi` / `KategoriDesaAntiKorupsi` | Transparansi |
|
| `DesaAntiKorupsi` / `KategoriDesaAntiKorupsi` | Transparansi |
|
||||||
| `SdgsDesa` | SDGs desa |
|
| `SdgsDesa` | SDGs desa |
|
||||||
| `PrestasiDesa` / `KategoriPrestasiDesa` | Prestasi |
|
| `PrestasiDesa` / `KategoriPrestasiDesa` | Prestasi |
|
||||||
| `MusikDesa` | Musik desa |
|
| `MusikDesa` | Musik desa |
|
||||||
|
|
||||||
### PPID
|
### PPID
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ------------------------------------------------------- | -------------------------- |
|
||||||
| `StrukturPPID` / `PosisiOrganisasiPPID` / `PegawaiPPID` | Struktur organisasi |
|
| `StrukturPPID` / `PosisiOrganisasiPPID` / `PegawaiPPID` | Struktur organisasi |
|
||||||
| `VisiMisiPPID` | Visi misi |
|
| `VisiMisiPPID` | Visi misi |
|
||||||
| `ProfilePPID` | Profil pejabat |
|
| `ProfilePPID` | Profil pejabat |
|
||||||
| `DasarHukumPPID` | Regulasi |
|
| `DasarHukumPPID` | Regulasi |
|
||||||
| `DaftarInformasiPublik` | Katalog informasi |
|
| `DaftarInformasiPublik` | Katalog informasi |
|
||||||
| `PermohonanInformasiPublik` | Permohonan + lookup tables |
|
| `PermohonanInformasiPublik` | Permohonan + lookup tables |
|
||||||
| `FormulirPermohonanKeberatan` | Keberatan |
|
| `FormulirPermohonanKeberatan` | Keberatan |
|
||||||
| `IndeksKepuasanMasyarakat` + grafik breakdown | Survey kepuasan |
|
| `IndeksKepuasanMasyarakat` + grafik breakdown | Survey kepuasan |
|
||||||
|
|
||||||
### Kesehatan
|
### Kesehatan
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| --------------------------------------------------- | ---------------------------------------------- |
|
||||||
| `FasilitasKesehatan` | Fasilitas lengkap (dokter, tarif, prosedur) |
|
| `FasilitasKesehatan` | Fasilitas lengkap (dokter, tarif, prosedur) |
|
||||||
| `Puskesmas` / `JamOperasional` / `KontakPuskesmas` | Puskesmas |
|
| `Puskesmas` / `JamOperasional` / `KontakPuskesmas` | Puskesmas |
|
||||||
| `Posyandu` | Pos pelayanan terpadu |
|
| `Posyandu` | Pos pelayanan terpadu |
|
||||||
| `ProgramKesehatan` | Program kesehatan |
|
| `ProgramKesehatan` | Program kesehatan |
|
||||||
| `ArtikelKesehatan` | Artikel lengkap (gejala, pencegahan, P3K, dll) |
|
| `ArtikelKesehatan` | Artikel lengkap (gejala, pencegahan, P3K, dll) |
|
||||||
| `PenangananDarurat` / `KontakDarurat` | Darurat |
|
| `PenangananDarurat` / `KontakDarurat` | Darurat |
|
||||||
| `InfoWabahPenyakit` | Wabah |
|
| `InfoWabahPenyakit` | Wabah |
|
||||||
| `DataKematian_Kelahiran` / `Kelahiran` / `Kematian` | Vital statistik |
|
| `DataKematian_Kelahiran` / `Kelahiran` / `Kematian` | Vital statistik |
|
||||||
| `GrafikKepuasan` | Kepuasan |
|
| `GrafikKepuasan` | Kepuasan |
|
||||||
|
|
||||||
### Ekonomi
|
### Ekonomi
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ------------------------------------------------------------- | ------------------- |
|
||||||
| `PasarDesa` / `KategoriProduk` / `KategoriToPasar` | Pasar desa |
|
| `PasarDesa` / `KategoriProduk` / `KategoriToPasar` | Pasar desa |
|
||||||
| `StrukturBumDes` / `PosisiOrganisasiBumDes` / `PegawaiBumDes` | BUMDes |
|
| `StrukturBumDes` / `PosisiOrganisasiBumDes` / `PegawaiBumDes` | BUMDes |
|
||||||
| `ProgramKemiskinan` / `StatistikKemiskinan` | Kemiskinan |
|
| `ProgramKemiskinan` / `StatistikKemiskinan` | Kemiskinan |
|
||||||
| `SektorUnggulanDesa` | Sektor unggulan |
|
| `SektorUnggulanDesa` | Sektor unggulan |
|
||||||
| `LowonganPekerjaan` | Lowongan |
|
| `LowonganPekerjaan` | Lowongan |
|
||||||
| `DataDemografiPekerjaan` | Demografi pekerjaan |
|
| `DataDemografiPekerjaan` | Demografi pekerjaan |
|
||||||
| `DetailDataPengangguran` | Pengangguran |
|
| `DetailDataPengangguran` | Pengangguran |
|
||||||
| `GrafikJumlahPendudukMiskin` | Tren kemiskinan |
|
| `GrafikJumlahPendudukMiskin` | Tren kemiskinan |
|
||||||
|
|
||||||
### Kependudukan
|
### Kependudukan
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ------------------ | ---------------------- |
|
||||||
| `DataBanjar` | Data per banjar |
|
| `DataBanjar` | Data per banjar |
|
||||||
| `DistribusiAgama` | Distribusi agama |
|
| `DistribusiAgama` | Distribusi agama |
|
||||||
| `DistribusiUmur` | Distribusi umur |
|
| `DistribusiUmur` | Distribusi umur |
|
||||||
| `MigrasiPenduduk` | Migrasi (MASUK/KELUAR) |
|
| `MigrasiPenduduk` | Migrasi (MASUK/KELUAR) |
|
||||||
| `DinamikaPenduduk` | Dinamika tahunan |
|
| `DinamikaPenduduk` | Dinamika tahunan |
|
||||||
|
|
||||||
### Pendidikan
|
### Pendidikan
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ------------------------------------------------------ | ------------------------------ |
|
||||||
| `JenjangPendidikan` / `Lembaga` / `Siswa` / `Pengajar` | Data sekolah |
|
| `JenjangPendidikan` / `Lembaga` / `Siswa` / `Pengajar` | Data sekolah |
|
||||||
| `BeasiswaPendaftar` | Beasiswa (dengan enum lengkap) |
|
| `BeasiswaPendaftar` | Beasiswa (dengan enum lengkap) |
|
||||||
| `DataPerpustakaan` / `KategoriBuku` / `PeminjamanBuku` | Perpustakaan |
|
| `DataPerpustakaan` / `KategoriBuku` / `PeminjamanBuku` | Perpustakaan |
|
||||||
| `DataPendidikan` | Statistik |
|
| `DataPendidikan` | Statistik |
|
||||||
|
|
||||||
### Keamanan
|
### Keamanan
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ---------------------------------------------------------------- | ------------------- |
|
||||||
| `KeamananLingkungan` | Keamanan lingkungan |
|
| `KeamananLingkungan` | Keamanan lingkungan |
|
||||||
| `PolsekTerdekat` / `LayananPolsek` / `LayananToPolsek` | Polsek |
|
| `PolsekTerdekat` / `LayananPolsek` / `LayananToPolsek` | Polsek |
|
||||||
| `KontakDaruratKeamanan` / `KontakItem` | Kontak darurat |
|
| `KontakDaruratKeamanan` / `KontakItem` | Kontak darurat |
|
||||||
| `PencegahanKriminalitas` | Pencegahan |
|
| `PencegahanKriminalitas` | Pencegahan |
|
||||||
| `LaporanPublik` / `PenangananLaporanPublik` (enum StatusLaporan) | Laporan |
|
| `LaporanPublik` / `PenangananLaporanPublik` (enum StatusLaporan) | Laporan |
|
||||||
| `Pelapor` | Pelapor |
|
| `Pelapor` | Pelapor |
|
||||||
| `MenuTipsKeamanan` | Tips |
|
| `MenuTipsKeamanan` | Tips |
|
||||||
|
|
||||||
### Lingkungan
|
### Lingkungan
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ----------------------------------------------------- | ------------------ |
|
||||||
| `PengelolaanSampah` | Pengelolaan sampah |
|
| `PengelolaanSampah` | Pengelolaan sampah |
|
||||||
| `KeteranganBankSampahTerdekat` | Bank sampah |
|
| `KeteranganBankSampahTerdekat` | Bank sampah |
|
||||||
| `ProgramPenghijauan` | Penghijauan |
|
| `ProgramPenghijauan` | Penghijauan |
|
||||||
| `DataLingkunganDesa` | Data lingkungan |
|
| `DataLingkunganDesa` | Data lingkungan |
|
||||||
| `KegiatanDesa` / `KategoriKegiatan` | Gotong royong |
|
| `KegiatanDesa` / `KategoriKegiatan` | Gotong royong |
|
||||||
| `FilosofiTriHita` / `BentukKonservasiBerdasarkanAdat` | Konservasi Bali |
|
| `FilosofiTriHita` / `BentukKonservasiBerdasarkanAdat` | Konservasi Bali |
|
||||||
|
|
||||||
### Inovasi
|
### Inovasi
|
||||||
|
|
||||||
| Model | Keterangan |
|
| Model | Keterangan |
|
||||||
|-------|-----------|
|
| ---------------------------------------- | -------------------- |
|
||||||
| `DesaDigital` | Smart village |
|
| `DesaDigital` | Smart village |
|
||||||
| `ProgramKreatif` | Program kreatif |
|
| `ProgramKreatif` | Program kreatif |
|
||||||
| `KolaborasiInovasi` / `MitraKolaborasi` | Kolaborasi |
|
| `KolaborasiInovasi` / `MitraKolaborasi` | Kolaborasi |
|
||||||
| `InfoTekno` | Teknologi tepat guna |
|
| `InfoTekno` | Teknologi tepat guna |
|
||||||
| `AjukanIdeInovatif` | Ide dari warga |
|
| `AjukanIdeInovatif` | Ide dari warga |
|
||||||
| `AdministrasiOnline` / `JenisLayanan` | Layanan online |
|
| `AdministrasiOnline` / `JenisLayanan` | Layanan online |
|
||||||
| `PengaduanMasyarakat` / `JenisPengaduan` | Pengaduan |
|
| `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`:
|
Semua API ditangani oleh **Elysia.js** di `src/app/api/[[...slugs]]/route.ts`:
|
||||||
|
|
||||||
| Endpoint Group | Prefix | Deskripsi |
|
| Endpoint Group | Prefix | Deskripsi |
|
||||||
|---------------|--------|-----------|
|
| ---------------- | -------------------- | ---------------------------------------------- |
|
||||||
| **File Storage** | `/api/file-storage` | CRUD file storage |
|
| **File Storage** | `/api/file-storage` | CRUD file storage |
|
||||||
| **Landing Page** | `/api/landing-page` | Profil, prestasi, anti-korupsi, SDGs, APBDes |
|
| **Landing Page** | `/api/landing-page` | Profil, prestasi, anti-korupsi, SDGs, APBDes |
|
||||||
| **Desa** | `/api/desa` | Berita, gallery, potensi, pengumuman, layanan |
|
| **Desa** | `/api/desa` | Berita, gallery, potensi, pengumuman, layanan |
|
||||||
| **PPID** | `/api/ppid` | Semua endpoint PPID |
|
| **PPID** | `/api/ppid` | Semua endpoint PPID |
|
||||||
| **Kesehatan** | `/api/kesehatan` | Fasilitas, puskesmas, posyandu, artikel, wabah |
|
| **Kesehatan** | `/api/kesehatan` | Fasilitas, puskesmas, posyandu, artikel, wabah |
|
||||||
| **Ekonomi** | `/api/ekonomi` | Pasar desa, BUMDes, APBDes, pengangguran |
|
| **Ekonomi** | `/api/ekonomi` | Pasar desa, BUMDes, APBDes, pengangguran |
|
||||||
| **Keamanan** | `/api/keamanan` | Keamanan, polsek, laporan, kriminalitas |
|
| **Keamanan** | `/api/keamanan` | Keamanan, polsek, laporan, kriminalitas |
|
||||||
| **Lingkungan** | `/api/lingkungan` | Sampah, penghijauan, gotong royong |
|
| **Lingkungan** | `/api/lingkungan` | Sampah, penghijauan, gotong royong |
|
||||||
| **Pendidikan** | `/api/pendidikan` | Sekolah, beasiswa, perpustakaan |
|
| **Pendidikan** | `/api/pendidikan` | Sekolah, beasiswa, perpustakaan |
|
||||||
| **Kependudukan** | `/api/kependudukan` | Banjar, agama, umur, migrasi |
|
| **Kependudukan** | `/api/kependudukan` | Banjar, agama, umur, migrasi |
|
||||||
| **Inovasi** | `/api/inovasi` | Desa digital, kolaborasi, pengaduan |
|
| **Inovasi** | `/api/inovasi` | Desa digital, kolaborasi, pengaduan |
|
||||||
| **User** | `/api/admin/user` | CRUD user |
|
| **User** | `/api/admin/user` | CRUD user |
|
||||||
| **Role** | `/api/admin/role` | CRUD role |
|
| **Role** | `/api/admin/role` | CRUD role |
|
||||||
| **Search** | `/api/search` | Global search |
|
| **Search** | `/api/search` | Global search |
|
||||||
| **Utils** | `/api/utils/version` | Version info |
|
| **Utils** | `/api/utils/version` | Version info |
|
||||||
|
|
||||||
### Utility Endpoints
|
### Utility Endpoints
|
||||||
|
|
||||||
| Endpoint | Method | Deskripsi |
|
| Endpoint | Method | Deskripsi |
|
||||||
|----------|--------|-----------|
|
| --------------------- | ------ | ----------------------------- |
|
||||||
| `/api/img/:name` | GET | Serve image dengan resize |
|
| `/api/img/:name` | GET | Serve image dengan resize |
|
||||||
| `/api/img/:name` | DELETE | Delete image |
|
| `/api/img/:name` | DELETE | Delete image |
|
||||||
| `/api/imgs` | GET | List images dengan pagination |
|
| `/api/imgs` | GET | List images dengan pagination |
|
||||||
| `/api/upl-img` | POST | Upload multiple images |
|
| `/api/upl-img` | POST | Upload multiple images |
|
||||||
| `/api/upl-img-single` | POST | Upload single image |
|
| `/api/upl-img-single` | POST | Upload single image |
|
||||||
| `/api/upl-csv` | POST | Upload CSV multiple |
|
| `/api/upl-csv` | POST | Upload CSV multiple |
|
||||||
| `/api/upl-csv-single` | POST | Upload single CSV |
|
| `/api/upl-csv-single` | POST | Upload single CSV |
|
||||||
|
|
||||||
### Auth Endpoints
|
### Auth Endpoints
|
||||||
|
|
||||||
| Endpoint | Method | Deskripsi |
|
| Endpoint | Method | Deskripsi |
|
||||||
|----------|--------|-----------|
|
| ------------------ | ------ | ---------------- |
|
||||||
| `/api/auth/login` | POST | Login dengan OTP |
|
| `/api/auth/login` | POST | Login dengan OTP |
|
||||||
| `/api/auth/logout` | POST | Logout |
|
| `/api/auth/logout` | POST | Logout |
|
||||||
| `/api/auth/me` | GET | Get current user |
|
| `/api/auth/me` | GET | Get current user |
|
||||||
|
|
||||||
**Swagger Documentation**: Tersedia di `/api/docs`
|
**Swagger Documentation**: Tersedia di `/api/docs`
|
||||||
|
|
||||||
@@ -514,22 +525,23 @@ Admin dashboard menggunakan **Mantine AppShell** dengan sidebar navigasi dinamis
|
|||||||
|
|
||||||
### Route Group: `/admin`
|
### Route Group: `/admin`
|
||||||
|
|
||||||
| Section | Path | Deskripsi |
|
| Section | Path | Deskripsi |
|
||||||
|---------|------|-----------|
|
| ---------------- | ---------------------- | ------------------------------------------------------------------ |
|
||||||
| **Landing Page** | `/admin/landing-page/` | Profil desa, prestasi, anti-korupsi, SDGs, media sosial |
|
| **Landing Page** | `/admin/landing-page/` | Profil desa, prestasi, anti-korupsi, SDGs, media sosial |
|
||||||
| **Desa** | `/admin/desa/` | Berita, gallery, layanan, penghargaan, pengumuman, potensi, profil |
|
| **Desa** | `/admin/desa/` | Berita, gallery, layanan, penghargaan, pengumuman, potensi, profil |
|
||||||
| **PPID** | `/admin/ppid/` | 8 sub-modul PPID lengkap |
|
| **PPID** | `/admin/ppid/` | 8 sub-modul PPID lengkap |
|
||||||
| **Kesehatan** | `/admin/kesehatan/` | 8 sub-modul kesehatan |
|
| **Kesehatan** | `/admin/kesehatan/` | 8 sub-modul kesehatan |
|
||||||
| **Ekonomi** | `/admin/ekonomi/` | 10 sub-modul ekonomi |
|
| **Ekonomi** | `/admin/ekonomi/` | 10 sub-modul ekonomi |
|
||||||
| **Kependudukan** | `/admin/kependudukan/` | 4 sub-modul kependudukan |
|
| **Kependudukan** | `/admin/kependudukan/` | 4 sub-modul kependudukan |
|
||||||
| **Pendidikan** | `/admin/pendidikan/` | 7 sub-modul pendidikan |
|
| **Pendidikan** | `/admin/pendidikan/` | 7 sub-modul pendidikan |
|
||||||
| **Keamanan** | `/admin/keamanan/` | 6 sub-modul keamanan |
|
| **Keamanan** | `/admin/keamanan/` | 6 sub-modul keamanan |
|
||||||
| **Lingkungan** | `/admin/lingkungan/` | 6 sub-modul lingkungan |
|
| **Lingkungan** | `/admin/lingkungan/` | 6 sub-modul lingkungan |
|
||||||
| **Inovasi** | `/admin/inovasi/` | 6 sub-modul inovasi |
|
| **Inovasi** | `/admin/inovasi/` | 6 sub-modul inovasi |
|
||||||
| **Musik** | `/admin/musik/` | Manajemen musik desa |
|
| **Musik** | `/admin/musik/` | Manajemen musik desa |
|
||||||
| **User & Role** | `/admin/user&role/` | Manajemen user, role, menu access |
|
| **User & Role** | `/admin/user&role/` | Manajemen user, role, menu access |
|
||||||
|
|
||||||
### Fitur Admin:
|
### Fitur Admin:
|
||||||
|
|
||||||
- **Role-based Dynamic Navbar**: Navbar berubah berdasarkan roleId user
|
- **Role-based Dynamic Navbar**: Navbar berubah berdasarkan roleId user
|
||||||
- **Dark Mode Toggle**: Tema gelap/terang
|
- **Dark Mode Toggle**: Tema gelap/terang
|
||||||
- **OTP Login**: Login dengan nomor telepon + kode OTP
|
- **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
|
- **Rich Text Editor**: Tiptap untuk konten HTML
|
||||||
|
|
||||||
### Role-Based Redirect:
|
### Role-Based Redirect:
|
||||||
| roleId | Role | Default Redirect |
|
|
||||||
|--------|------|-----------------|
|
| roleId | Role | Default Redirect |
|
||||||
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
|
| ------- | ------------------------ | --------------------------------------------------- |
|
||||||
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
|
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
|
||||||
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
|
| 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`
|
### Route Group: `/darmasaba`
|
||||||
|
|
||||||
| Section | Path | Deskripsi |
|
| Section | Path | Deskripsi |
|
||||||
|---------|------|-----------|
|
| ---------------- | ------------------------- | ----------------------------------------------------------------- |
|
||||||
| **Home** | `/darmasaba` | Landing page utama |
|
| **Home** | `/darmasaba` | Landing page utama |
|
||||||
| **Desa** | `/darmasaba/desa` | Profil, berita, gallery, layanan, pengumuman, potensi |
|
| **Desa** | `/darmasaba/desa` | Profil, berita, gallery, layanan, pengumuman, potensi |
|
||||||
| **PPID** | `/darmasaba/ppid` | 7 sub-halaman PPID publik |
|
| **PPID** | `/darmasaba/ppid` | 7 sub-halaman PPID publik |
|
||||||
| **Kesehatan** | `/darmasaba/kesehatan` | Info kesehatan publik |
|
| **Kesehatan** | `/darmasaba/kesehatan` | Info kesehatan publik |
|
||||||
| **Ekonomi** | `/darmasaba/ekonomi` | Info ekonomi desa |
|
| **Ekonomi** | `/darmasaba/ekonomi` | Info ekonomi desa |
|
||||||
| **Kependudukan** | `/darmasaba/kependudukan` | Data kependudukan |
|
| **Kependudukan** | `/darmasaba/kependudukan` | Data kependudukan |
|
||||||
| **Pendidikan** | `/darmasaba/pendidikan` | Info pendidikan |
|
| **Pendidikan** | `/darmasaba/pendidikan` | Info pendidikan |
|
||||||
| **Keamanan** | `/darmasaba/keamanan` | Info keamanan |
|
| **Keamanan** | `/darmasaba/keamanan` | Info keamanan |
|
||||||
| **Lingkungan** | `/darmasaba/lingkungan` | Info lingkungan |
|
| **Lingkungan** | `/darmasaba/lingkungan` | Info lingkungan |
|
||||||
| **Inovasi** | `/darmasaba/inovasi` | Info inovasi |
|
| **Inovasi** | `/darmasaba/inovasi` | Info inovasi |
|
||||||
| **Musik** | `/darmasaba/musik` | Musik desa |
|
| **Musik** | `/darmasaba/musik` | Musik desa |
|
||||||
| **Module** | `/darmasaba/module/*` | Link ke modul eksternal (DAVES, MANGAN, Bicara-Darma, BARES, dll) |
|
| **Module** | `/darmasaba/module/*` | Link ke modul eksternal (DAVES, MANGAN, Bicara-Darma, BARES, dll) |
|
||||||
|
|
||||||
### Fitur Publik:
|
### Fitur Publik:
|
||||||
|
|
||||||
- **Fixed Music Player Bar**: Player musik yang selalu tampil di bottom
|
- **Fixed Music Player Bar**: Player musik yang selalu tampil di bottom
|
||||||
- **Global Search**: Pencarian global
|
- **Global Search**: Pencarian global
|
||||||
- **News Reader**: Notifikasi berita modern
|
- **News Reader**: Notifikasi berita modern
|
||||||
@@ -581,33 +595,33 @@ Public website di `/darmasaba/` dengan layout yang mencakup **Navbar**, **Footer
|
|||||||
|
|
||||||
### Admin Components (`src/components/admin/`)
|
### Admin Components (`src/components/admin/`)
|
||||||
|
|
||||||
| Komponen | Deskripsi |
|
| Komponen | Deskripsi |
|
||||||
|----------|-----------|
|
| ------------------------ | --------------------------------- |
|
||||||
| `AdminThemeProvider.tsx` | Theme provider untuk admin |
|
| `AdminThemeProvider.tsx` | Theme provider untuk admin |
|
||||||
| `DarkModeToggle.tsx` | Toggle dark/light mode |
|
| `DarkModeToggle.tsx` | Toggle dark/light mode |
|
||||||
| `UnifiedSurface.tsx` | Consistent surface/card component |
|
| `UnifiedSurface.tsx` | Consistent surface/card component |
|
||||||
| `UnifiedTypography.tsx` | Consistent typography system |
|
| `UnifiedTypography.tsx` | Consistent typography system |
|
||||||
|
|
||||||
### Public Shared Components (`src/app/darmasaba/_com/`)
|
### Public Shared Components (`src/app/darmasaba/_com/`)
|
||||||
|
|
||||||
| Komponen | Deskripsi |
|
| Komponen | Deskripsi |
|
||||||
|----------|-----------|
|
| ---------------------------- | -------------------------------- |
|
||||||
| `Navbar.tsx` | Main navigation bar |
|
| `Navbar.tsx` | Main navigation bar |
|
||||||
| `NavbarMainMenu.tsx` | Main menu dengan kategori |
|
| `NavbarMainMenu.tsx` | Main menu dengan kategori |
|
||||||
| `NavbarSubMenu.tsx` | Submenu dropdown |
|
| `NavbarSubMenu.tsx` | Submenu dropdown |
|
||||||
| `Footer.tsx` | Footer dengan info desa |
|
| `Footer.tsx` | Footer dengan info desa |
|
||||||
| `FixedPlayerBar.tsx` | Music player bar fixed di bottom |
|
| `FixedPlayerBar.tsx` | Music player bar fixed di bottom |
|
||||||
| `LoadDataFirstClient.tsx` | Client-side data preloader |
|
| `LoadDataFirstClient.tsx` | Client-side data preloader |
|
||||||
| `globalSearch.tsx` | Global search component |
|
| `globalSearch.tsx` | Global search component |
|
||||||
| `NewsReader.tsx` | News notification reader |
|
| `NewsReader.tsx` | News notification reader |
|
||||||
| `ModernNewsNotification.tsx` | News toast notifications |
|
| `ModernNewsNotification.tsx` | News toast notifications |
|
||||||
|
|
||||||
### Global Components (`src/app/_com/`)
|
### Global Components (`src/app/_com/`)
|
||||||
|
|
||||||
| Komponen | Deskripsi |
|
| Komponen | Deskripsi |
|
||||||
|----------|-----------|
|
| ----------------- | --------------------- |
|
||||||
| `SpashScreen.tsx` | Splash screen on load |
|
| `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**:
|
Proyek menggunakan **multi-layer state management**:
|
||||||
|
|
||||||
| Library | Penggunaan | Lokasi |
|
| Library | Penggunaan | Lokasi |
|
||||||
|---------|-----------|--------|
|
| ------------------ | ----------------------------------------- | ---------------------------------- |
|
||||||
| **Jotai** | Auth state (`authStore`) | `src/store/authStore.ts` |
|
| **Jotai** | Auth state (`authStore`) | `src/store/authStore.ts` |
|
||||||
| **Valtio** | Dark mode, layanan, image list, nav state | `src/state/*.ts` |
|
| **Valtio** | Dark mode, layanan, image list, nav state | `src/state/*.ts` |
|
||||||
| **SWR** | Server state fetching & caching | Digunakan di components |
|
| **SWR** | Server state fetching & caching | Digunakan di components |
|
||||||
| **React Context** | Music player context | `src/app/context/MusicContext.tsx` |
|
| **React Context** | Music player context | `src/app/context/MusicContext.tsx` |
|
||||||
| **React useState** | Local component state | Di components |
|
| **React useState** | Local component state | Di components |
|
||||||
|
|
||||||
### State Files:
|
### State Files:
|
||||||
|
|
||||||
@@ -643,6 +657,7 @@ src/store/
|
|||||||
Sistem autentikasi menggunakan **OTP (One-Time Password)** via WhatsApp/Telepon dengan **iron-session** untuk session management.
|
Sistem autentikasi menggunakan **OTP (One-Time Password)** via WhatsApp/Telepon dengan **iron-session** untuk session management.
|
||||||
|
|
||||||
### Flow Autentikasi:
|
### Flow Autentikasi:
|
||||||
|
|
||||||
1. User memasukkan **nomor telepon** di `/login`
|
1. User memasukkan **nomor telepon** di `/login`
|
||||||
2. Sistem mengirim **kode OTP** via WhatsApp Server
|
2. Sistem mengirim **kode OTP** via WhatsApp Server
|
||||||
3. OTP disimpan di model `KodeOtp`
|
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
|
6. Session disimpan di `UserSession` model dengan expiry
|
||||||
|
|
||||||
### Session Structure:
|
### Session Structure:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/lib/session.ts
|
// src/lib/session.ts
|
||||||
type SessionData = {
|
type SessionData = {
|
||||||
@@ -665,13 +681,15 @@ type SessionData = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Role-Based Access:
|
### Role-Based Access:
|
||||||
| roleId | Role | Default Redirect |
|
|
||||||
|--------|------|-----------------|
|
| roleId | Role | Default Redirect |
|
||||||
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
|
| ------- | ------------------------ | --------------------------------------------------- |
|
||||||
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
|
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
|
||||||
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
|
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu/list-posyandu` |
|
||||||
|
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
|
||||||
|
|
||||||
### Authorization:
|
### Authorization:
|
||||||
|
|
||||||
- **UserMenuAccess**: Mapping user ke menu yang boleh diakses
|
- **UserMenuAccess**: Mapping user ke menu yang boleh diakses
|
||||||
- **Dynamic Navbar**: Navbar dirender berdasarkan `menuIds` user
|
- **Dynamic Navbar**: Navbar dirender berdasarkan `menuIds` user
|
||||||
- **Inactive Users**: Dialihkan ke `/waiting-room`
|
- **Inactive Users**: Dialihkan ke `/waiting-room`
|
||||||
@@ -698,6 +716,7 @@ Stage 2: Runner
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Entry Point (`docker-entrypoint.sh`):
|
### Entry Point (`docker-entrypoint.sh`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bunx prisma migrate deploy # Run migrations
|
bunx prisma migrate deploy # Run migrations
|
||||||
exec bun start # Start Next.js production server
|
exec bun start # Start Next.js production server
|
||||||
@@ -707,11 +726,11 @@ exec bun start # Start Next.js production server
|
|||||||
|
|
||||||
Terdapat **3 workflow**:
|
Terdapat **3 workflow**:
|
||||||
|
|
||||||
| Workflow | Trigger | Fungsi |
|
| Workflow | Trigger | Fungsi |
|
||||||
|----------|---------|--------|
|
| -------------------- | -------------------------- | ------------------------------------------------------------------ |
|
||||||
| `docker-publish.yml` | Push tag `v*` | Auto build & push ke GHCR |
|
| `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` |
|
| `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` |
|
| `re-pull.yml` | Manual (workflow_dispatch) | Re-pull image di Portainer dengan input `stack_name` + `stack_env` |
|
||||||
|
|
||||||
### Deployment Workflow (Sequential):
|
### Deployment Workflow (Sequential):
|
||||||
|
|
||||||
@@ -730,32 +749,35 @@ Terdapat **3 workflow**:
|
|||||||
**PENTING**: `publish.yml` dan `re-pull.yml` TIDAK boleh dijalankan bersamaan (race condition).
|
**PENTING**: `publish.yml` dan `re-pull.yml` TIDAK boleh dijalankan bersamaan (race condition).
|
||||||
|
|
||||||
### Environments:
|
### Environments:
|
||||||
|
|
||||||
- **dev**: Development
|
- **dev**: Development
|
||||||
- **stg**: Staging (`desa-darmasaba-stg.wibudev.com`)
|
- **stg**: Staging (`desa-darmasaba-stg.wibudev.com`)
|
||||||
- **prod**: Production
|
- **prod**: Production
|
||||||
|
|
||||||
### Notification:
|
### Notification:
|
||||||
|
|
||||||
- Telegram notification via `notify.sh` script setelah setiap workflow
|
- Telegram notification via `notify.sh` script setelah setiap workflow
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Scripts
|
## 13. Scripts
|
||||||
|
|
||||||
| Script | Command | Deskripsi |
|
| Script | Command | Deskripsi |
|
||||||
|--------|---------|-----------|
|
| ----------------- | -------------------------------------- | -------------------------------- |
|
||||||
| `dev` | `next dev` | Development server |
|
| `dev` | `next dev` | Development server |
|
||||||
| `build` | `next build` | Production build |
|
| `build` | `next build` | Production build |
|
||||||
| `start` | `next start` | Production server |
|
| `start` | `next start` | Production server |
|
||||||
| `test:api` | `vitest run` | Run API unit tests |
|
| `test:api` | `vitest run` | Run API unit tests |
|
||||||
| `test:e2e` | `playwright test` | Run E2E tests |
|
| `test:e2e` | `playwright test` | Run E2E tests |
|
||||||
| `test` | `bun run test:api && bun run test:e2e` | Run all tests |
|
| `test` | `bun run test:api && bun run test:e2e` | Run all tests |
|
||||||
| `seed` | `bun run prisma/seed.ts` | Seed database |
|
| `seed` | `bun run prisma/seed.ts` | Seed database |
|
||||||
| `prisma:generate` | `bunx prisma generate` | Generate Prisma client |
|
| `prisma:generate` | `bunx prisma generate` | Generate Prisma client |
|
||||||
| `prisma:push` | `bunx prisma db push` | Push schema to database |
|
| `prisma:push` | `bunx prisma db push` | Push schema to database |
|
||||||
| `prisma:studio` | `bunx prisma studio` | Open Prisma Studio GUI |
|
| `prisma:studio` | `bunx prisma studio` | Open Prisma Studio GUI |
|
||||||
| `gen:api` | *(empty)* | Generate API types (placeholder) |
|
| `gen:api` | _(empty)_ | Generate API types (placeholder) |
|
||||||
|
|
||||||
### Prisma Seed Configuration:
|
### Prisma Seed Configuration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// package.json
|
// package.json
|
||||||
{
|
{
|
||||||
@@ -771,35 +793,37 @@ Terdapat **3 workflow**:
|
|||||||
|
|
||||||
File: `.env.example`
|
File: `.env.example`
|
||||||
|
|
||||||
| Variable | Deskripsi | Contoh |
|
| Variable | Deskripsi | Contoh |
|
||||||
|----------|-----------|--------|
|
| ---------------------------- | ------------------------------------ | ------------------------------------------------------ |
|
||||||
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/desa-darmasaba` |
|
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/desa-darmasaba` |
|
||||||
| `SEAFILE_TOKEN` | Seafile API token | `your_seafile_token` |
|
| `SEAFILE_TOKEN` | Seafile API token | `your_seafile_token` |
|
||||||
| `SEAFILE_REPO_ID` | Seafile repository ID | `your_repo_id` |
|
| `SEAFILE_REPO_ID` | Seafile repository ID | `your_repo_id` |
|
||||||
| `SEAFILE_URL` | Seafile instance URL | `https://seafile.example.com` |
|
| `SEAFILE_URL` | Seafile instance URL | `https://seafile.example.com` |
|
||||||
| `SEAFILE_PUBLIC_SHARE_TOKEN` | Token untuk public share | `your_share_token` |
|
| `SEAFILE_PUBLIC_SHARE_TOKEN` | Token untuk public share | `your_share_token` |
|
||||||
| `WIBU_UPLOAD_DIR` | Upload directory path | `uploads` |
|
| `WIBU_UPLOAD_DIR` | Upload directory path | `uploads` |
|
||||||
| `WA_SERVER_TOKEN` | WhatsApp server token | `your_wa_token` |
|
| `WA_SERVER_TOKEN` | WhatsApp server token | `your_wa_token` |
|
||||||
| `NEXT_PUBLIC_BASE_URL` | Base URL aplikasi | `/` (relative) |
|
| `NEXT_PUBLIC_BASE_URL` | Base URL aplikasi | `/` (relative) |
|
||||||
| `EMAIL_USER` | Email untuk notifikasi | `your_email@gmail.com` |
|
| `EMAIL_USER` | Email untuk notifikasi | `your_email@gmail.com` |
|
||||||
| `EMAIL_PASS` | Email app password | `your_app_password` |
|
| `EMAIL_PASS` | Email app password | `your_app_password` |
|
||||||
| `BASE_TOKEN_KEY` | JWT secret key | `your_jwt_secret` |
|
| `BASE_TOKEN_KEY` | JWT secret key | `your_jwt_secret` |
|
||||||
| `BOT_TOKEN` | Telegram bot token | `your_bot_token` |
|
| `BOT_TOKEN` | Telegram bot token | `your_bot_token` |
|
||||||
| `CHAT_ID` | Telegram chat ID | `your_chat_id` |
|
| `CHAT_ID` | Telegram chat ID | `your_chat_id` |
|
||||||
| `SESSION_PASSWORD` | iron-session password (min 32 chars) | `secure_32_char_password` |
|
| `SESSION_PASSWORD` | iron-session password (min 32 chars) | `secure_32_char_password` |
|
||||||
| `ELEVENLABS_API_KEY` | ElevenLabs API (TTS - optional) | `your_elevenlabs_key` |
|
| `ELEVENLABS_API_KEY` | ElevenLabs API (TTS - optional) | `your_elevenlabs_key` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Layanan Eksternal
|
## 15. Layanan Eksternal
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
|
|
||||||
- **Provider**: PostgreSQL via Prisma ORM
|
- **Provider**: PostgreSQL via Prisma ORM
|
||||||
- **Schema**: `public`
|
- **Schema**: `public`
|
||||||
- **Connection**: Via `DATABASE_URL` environment variable
|
- **Connection**: Via `DATABASE_URL` environment variable
|
||||||
- **Migrations**: `prisma migrate deploy` di docker entrypoint
|
- **Migrations**: `prisma migrate deploy` di docker entrypoint
|
||||||
|
|
||||||
### Seafile (File Storage)
|
### Seafile (File Storage)
|
||||||
|
|
||||||
- **Tipe**: Self-hosted file sync & share
|
- **Tipe**: Self-hosted file sync & share
|
||||||
- **Penggunaan**: Storage untuk images, documents, audio files
|
- **Penggunaan**: Storage untuk images, documents, audio files
|
||||||
- **Integrasi**: `src/lib/seafile-auth-service.ts`
|
- **Integrasi**: `src/lib/seafile-auth-service.ts`
|
||||||
@@ -807,19 +831,23 @@ File: `.env.example`
|
|||||||
- **Config**: Token, repo ID, base URL
|
- **Config**: Token, repo ID, base URL
|
||||||
|
|
||||||
### WhatsApp Server
|
### WhatsApp Server
|
||||||
|
|
||||||
- **Penggunaan**: Kirim OTP codes saat login
|
- **Penggunaan**: Kirim OTP codes saat login
|
||||||
- **Config**: `WA_SERVER_TOKEN`
|
- **Config**: `WA_SERVER_TOKEN`
|
||||||
|
|
||||||
### Telegram Bot
|
### Telegram Bot
|
||||||
|
|
||||||
- **Penggunaan**: Notifikasi deployment & sistem
|
- **Penggunaan**: Notifikasi deployment & sistem
|
||||||
- **Config**: `BOT_TOKEN` + `CHAT_ID`
|
- **Config**: `BOT_TOKEN` + `CHAT_ID`
|
||||||
- **Integration**: `notify.sh` script di GitHub Actions
|
- **Integration**: `notify.sh` script di GitHub Actions
|
||||||
|
|
||||||
### ElevenLabs (Optional)
|
### ElevenLabs (Optional)
|
||||||
|
|
||||||
- **Penggunaan**: Text-to-Speech (TTS) features
|
- **Penggunaan**: Text-to-Speech (TTS) features
|
||||||
- **Config**: `ELEVENLABS_API_KEY`
|
- **Config**: `ELEVENLABS_API_KEY`
|
||||||
|
|
||||||
### Email (Nodemailer)
|
### Email (Nodemailer)
|
||||||
|
|
||||||
- **Penggunaan**: Notifikasi email untuk subscription/pengumuman
|
- **Penggunaan**: Notifikasi email untuk subscription/pengumuman
|
||||||
- **Config**: `EMAIL_USER` + `EMAIL_PASS`
|
- **Config**: `EMAIL_USER` + `EMAIL_PASS`
|
||||||
- **Provider**: Gmail (app password)
|
- **Provider**: Gmail (app password)
|
||||||
@@ -828,15 +856,15 @@ File: `.env.example`
|
|||||||
|
|
||||||
## Ringkasan Cepat
|
## Ringkasan Cepat
|
||||||
|
|
||||||
| Aspek | Detail |
|
| Aspek | Detail |
|
||||||
|-------|--------|
|
| ------------- | ------------------------------------------ |
|
||||||
| **Framework** | Next.js 15 (App Router) + Elysia.js |
|
| **Framework** | Next.js 15 (App Router) + Elysia.js |
|
||||||
| **Database** | PostgreSQL + Prisma (100+ models) |
|
| **Database** | PostgreSQL + Prisma (100+ models) |
|
||||||
| **Auth** | OTP + iron-session + JWT |
|
| **Auth** | OTP + iron-session + JWT |
|
||||||
| **Storage** | Seafile + local uploads |
|
| **Storage** | Seafile + local uploads |
|
||||||
| **UI** | Mantine UI + Tiptap + Framer Motion |
|
| **UI** | Mantine UI + Tiptap + Framer Motion |
|
||||||
| **State** | Jotai + Valtio + SWR |
|
| **State** | Jotai + Valtio + SWR |
|
||||||
| **Deploy** | Docker + GHCR + Portainer + GitHub Actions |
|
| **Deploy** | Docker + GHCR + Portainer + GitHub Actions |
|
||||||
| **Runtime** | Bun |
|
| **Runtime** | Bun |
|
||||||
| **Testing** | Vitest + Playwright |
|
| **Testing** | Vitest + Playwright |
|
||||||
| **Version** | 0.1.11 |
|
| **Version** | 0.1.11 |
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export default function Validasi() {
|
|||||||
case 2:
|
case 2:
|
||||||
return '/admin/landing-page/profil/program-inovasi';
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
case 3:
|
case 3:
|
||||||
return '/admin/kesehatan/posyandu';
|
return '/admin/kesehatan/posyandu/list-posyandu';
|
||||||
case 4:
|
case 4:
|
||||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Center,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { IconDeviceImacCog, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import balitaState from '../../../_state/kesehatan/balita/balita';
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
|
||||||
|
|
||||||
const STUNTING_COLORS: Record<string, string> = {
|
const STUNTING_COLORS: Record<string, string> = {
|
||||||
NORMAL: 'green',
|
NORMAL: 'green',
|
||||||
@@ -29,161 +36,272 @@ const STUNTING_COLORS: Record<string, string> = {
|
|||||||
STUNTING: 'red',
|
STUNTING: 'red',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BalitaPage() {
|
function BalitaPage() {
|
||||||
const router = useRouter();
|
|
||||||
const state = useProxy(balitaState);
|
|
||||||
const [search, setSearch] = useState('');
|
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 [statusFilter, setStatusFilter] = useState('');
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
state.findMany.load(1, 10, search, statusFilter);
|
load(page, 10, debouncedSearch, statusFilter);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [page, debouncedSearch, statusFilter]);
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
state.findMany.load(1, 10, search, statusFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string, nama: string) => {
|
const handleDelete = async (id: string, nama: string) => {
|
||||||
if (!confirm(`Hapus data balita "${nama}"?`)) return;
|
if (!confirm(`Hapus data balita "${nama}"?`)) return;
|
||||||
await state.delete.byId(id);
|
await state.delete.byId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rows = state.findMany.data?.map((d) => (
|
const filteredData = data || [];
|
||||||
<Table.Tr key={d.id}>
|
|
||||||
<Table.Td>{d.nama}</Table.Td>
|
if (loading || !data) {
|
||||||
<Table.Td>{d.jenisKelamin}</Table.Td>
|
return (
|
||||||
<Table.Td>
|
<Stack py="md">
|
||||||
{d.tanggalLahir
|
<Skeleton h={600} radius="md" />
|
||||||
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
|
</Stack>
|
||||||
: '-'}
|
);
|
||||||
</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/posyandu/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 (
|
return (
|
||||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
<Box py="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Title order={3} c="black">Balita Terdaftar</Title>
|
<Group justify="space-between" mb="lg">
|
||||||
<Button
|
<Title order={4}>List Balita</Title>
|
||||||
leftSection={<IconPlus size={16} />}
|
<Button
|
||||||
onClick={() => router.push('/admin/kesehatan/posyandu/balita/create')}
|
leftSection={<IconPlus size={18} />}
|
||||||
radius="md"
|
color="blue"
|
||||||
style={{
|
variant="light"
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
onClick={() => router.push('/admin/kesehatan/posyandu/balita/create')}
|
||||||
color: '#fff',
|
>
|
||||||
}}
|
Tambah Baru
|
||||||
>
|
</Button>
|
||||||
Tambah
|
</Group>
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group mb="md" gap="sm">
|
<Group mb="md">
|
||||||
<TextInput
|
<Select
|
||||||
placeholder="Cari nama / NIK / ortu..."
|
placeholder="Filter stunting"
|
||||||
leftSection={<IconSearch size={16} />}
|
data={[
|
||||||
value={search}
|
{ value: '', label: 'Semua' },
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
{ value: 'NORMAL', label: 'Normal' },
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
{ value: 'ALERT', label: 'Alert' },
|
||||||
radius="md"
|
{ value: 'STUNTING', label: 'Stunting' },
|
||||||
style={{ flex: 1, maxWidth: 300 }}
|
]}
|
||||||
/>
|
value={statusFilter}
|
||||||
<Select
|
onChange={(v) => setStatusFilter(v ?? '')}
|
||||||
placeholder="Filter stunting"
|
radius="md"
|
||||||
data={[
|
clearable
|
||||||
{ value: '', label: 'Semua' },
|
/>
|
||||||
{ value: 'NORMAL', label: 'Normal' },
|
</Group>
|
||||||
{ 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 ? (
|
{/* Desktop Table */}
|
||||||
<Group justify="center" py="xl"><Loader /></Group>
|
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||||
) : (
|
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
|
||||||
<Stack gap="md">
|
<TableThead>
|
||||||
<Table striped highlightOnHover withTableBorder>
|
<TableTr>
|
||||||
<Table.Thead>
|
<TableTh w="22%">Nama</TableTh>
|
||||||
<Table.Tr>
|
<TableTh w="7%">JK</TableTh>
|
||||||
<Table.Th>Nama</Table.Th>
|
<TableTh w="12%">Tgl Lahir</TableTh>
|
||||||
<Table.Th>JK</Table.Th>
|
<TableTh w="12%">Imunisasi</TableTh>
|
||||||
<Table.Th>Tgl Lahir</Table.Th>
|
<TableTh w="10%">Gizi</TableTh>
|
||||||
<Table.Th>Imunisasi</Table.Th>
|
<TableTh w="12%">Pemeriksaan</TableTh>
|
||||||
<Table.Th>Gizi</Table.Th>
|
<TableTh w="11%">Stunting</TableTh>
|
||||||
<Table.Th>Pemeriksaan</Table.Th>
|
<TableTh w="14%">Aksi</TableTh>
|
||||||
<Table.Th>Stunting</Table.Th>
|
</TableTr>
|
||||||
<Table.Th>Aksi</Table.Th>
|
</TableThead>
|
||||||
</Table.Tr>
|
<TableTbody>
|
||||||
</Table.Thead>
|
{filteredData.length > 0 ? (
|
||||||
<Table.Tbody>
|
filteredData.map((d) => (
|
||||||
{rows && rows.length > 0 ? rows : (
|
<TableTr key={d.id}>
|
||||||
<Table.Tr>
|
<TableTd>{d.nama}</TableTd>
|
||||||
<Table.Td colSpan={8}>
|
<TableTd>{d.jenisKelamin}</TableTd>
|
||||||
<Text c="dimmed" ta="center" py="md">Tidak ada data</Text>
|
<TableTd>
|
||||||
</Table.Td>
|
{d.tanggalLahir
|
||||||
</Table.Tr>
|
? 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>
|
||||||
)}
|
)}
|
||||||
</Table.Tbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{(state.findMany.totalPages ?? 1) > 1 && (
|
{/* Mobile Cards */}
|
||||||
<Group justify="center">
|
<Stack gap="sm" hiddenFrom="md">
|
||||||
<Pagination
|
{filteredData.length > 0 ? (
|
||||||
total={state.findMany.totalPages}
|
filteredData.map((d) => (
|
||||||
value={state.findMany.page}
|
<Paper key={d.id} withBorder p="md" radius="md">
|
||||||
onChange={(p) => state.findMany.load(p, 10, search, statusFilter)}
|
<Box>
|
||||||
/>
|
<Text fz="sm" fw={600} mb={4}>
|
||||||
</Group>
|
{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>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default BalitaPage;
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Center,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { IconDeviceImacCog, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import ibuHamilState from '../../../_state/kesehatan/ibu-hamil/ibuHamil';
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
AKTIF: 'green',
|
AKTIF: 'green',
|
||||||
@@ -30,142 +37,242 @@ const STATUS_COLORS: Record<string, string> = {
|
|||||||
NONAKTIF: 'red',
|
NONAKTIF: 'red',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function IbuHamilPage() {
|
function IbuHamilPage() {
|
||||||
const router = useRouter();
|
|
||||||
const state = useProxy(ibuHamilState);
|
|
||||||
const [search, setSearch] = useState('');
|
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 [statusFilter, setStatusFilter] = useState('');
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
state.findMany.load(1, 10, search, statusFilter);
|
load(page, 10, debouncedSearch, statusFilter);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [page, debouncedSearch, statusFilter]);
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
state.findMany.load(1, 10, search, statusFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string, nama: string) => {
|
const handleDelete = async (id: string, nama: string) => {
|
||||||
if (!confirm(`Hapus data ibu hamil "${nama}"?`)) return;
|
if (!confirm(`Hapus data ibu hamil "${nama}"?`)) return;
|
||||||
await state.delete.byId(id);
|
await state.delete.byId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rows = state.findMany.data?.map((d) => (
|
const filteredData = data || [];
|
||||||
<Table.Tr key={d.id}>
|
|
||||||
<Table.Td>{d.nama}</Table.Td>
|
if (loading || !data) {
|
||||||
<Table.Td>{d.nik || '-'}</Table.Td>
|
return (
|
||||||
<Table.Td>{d.usiaKehamilan} minggu</Table.Td>
|
<Stack py="md">
|
||||||
<Table.Td>{d.noHp || '-'}</Table.Td>
|
<Skeleton h={600} radius="md" />
|
||||||
<Table.Td>
|
</Stack>
|
||||||
<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/posyandu/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 (
|
return (
|
||||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
<Box py="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Title order={3} c="black">Ibu Hamil</Title>
|
<Group justify="space-between" mb="lg">
|
||||||
<Button
|
<Title order={4}>List Ibu Hamil</Title>
|
||||||
leftSection={<IconPlus size={16} />}
|
<Button
|
||||||
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil/create')}
|
leftSection={<IconPlus size={18} />}
|
||||||
radius="md"
|
color="blue"
|
||||||
style={{
|
variant="light"
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil/create')}
|
||||||
color: '#fff',
|
>
|
||||||
}}
|
Tambah Baru
|
||||||
>
|
</Button>
|
||||||
Tambah
|
</Group>
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group mb="md" gap="sm">
|
<Group mb="md">
|
||||||
<TextInput
|
<Select
|
||||||
placeholder="Cari nama / NIK..."
|
placeholder="Filter status"
|
||||||
leftSection={<IconSearch size={16} />}
|
data={[
|
||||||
value={search}
|
{ value: '', label: 'Semua Status' },
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
{ value: 'AKTIF', label: 'Aktif' },
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
|
||||||
radius="md"
|
{ value: 'KEGUGURAN', label: 'Keguguran' },
|
||||||
style={{ flex: 1, maxWidth: 300 }}
|
{ value: 'NONAKTIF', label: 'Nonaktif' },
|
||||||
/>
|
]}
|
||||||
<Select
|
value={statusFilter}
|
||||||
placeholder="Filter status"
|
onChange={(v) => setStatusFilter(v ?? '')}
|
||||||
data={[
|
radius="md"
|
||||||
{ value: '', label: 'Semua Status' },
|
clearable
|
||||||
{ value: 'AKTIF', label: 'Aktif' },
|
/>
|
||||||
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
|
</Group>
|
||||||
{ 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 ? (
|
{/* Desktop Table */}
|
||||||
<Group justify="center" py="xl"><Loader /></Group>
|
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||||
) : (
|
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
|
||||||
<Stack gap="md">
|
<TableThead>
|
||||||
<Table striped highlightOnHover withTableBorder>
|
<TableTr>
|
||||||
<Table.Thead>
|
<TableTh w="25%">Nama</TableTh>
|
||||||
<Table.Tr>
|
<TableTh w="18%">NIK</TableTh>
|
||||||
<Table.Th>Nama</Table.Th>
|
<TableTh w="17%">Usia Kehamilan</TableTh>
|
||||||
<Table.Th>NIK</Table.Th>
|
<TableTh w="15%">No. HP</TableTh>
|
||||||
<Table.Th>Usia Kehamilan</Table.Th>
|
<TableTh w="12%">Status</TableTh>
|
||||||
<Table.Th>No. HP</Table.Th>
|
<TableTh w="13%">Aksi</TableTh>
|
||||||
<Table.Th>Status</Table.Th>
|
</TableTr>
|
||||||
<Table.Th>Aksi</Table.Th>
|
</TableThead>
|
||||||
</Table.Tr>
|
<TableTbody>
|
||||||
</Table.Thead>
|
{filteredData.length > 0 ? (
|
||||||
<Table.Tbody>
|
filteredData.map((d) => (
|
||||||
{rows && rows.length > 0 ? rows : (
|
<TableTr key={d.id}>
|
||||||
<Table.Tr>
|
<TableTd>{d.nama}</TableTd>
|
||||||
<Table.Td colSpan={6}>
|
<TableTd>{d.nik || '-'}</TableTd>
|
||||||
<Text c="dimmed" ta="center" py="md">Tidak ada data</Text>
|
<TableTd>{d.usiaKehamilan} minggu</TableTd>
|
||||||
</Table.Td>
|
<TableTd>{d.noHp || '-'}</TableTd>
|
||||||
</Table.Tr>
|
<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>
|
||||||
)}
|
)}
|
||||||
</Table.Tbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{(state.findMany.totalPages ?? 1) > 1 && (
|
{/* Mobile Cards */}
|
||||||
<Group justify="center">
|
<Stack gap="sm" hiddenFrom="md">
|
||||||
<Pagination
|
{filteredData.length > 0 ? (
|
||||||
total={state.findMany.totalPages}
|
filteredData.map((d) => (
|
||||||
value={state.findMany.page}
|
<Paper key={d.id} withBorder p="md" radius="md">
|
||||||
onChange={(p) => state.findMany.load(p, 10, search, statusFilter)}
|
<Box>
|
||||||
/>
|
<Text fz="sm" fw={600} mb={4}>
|
||||||
</Group>
|
{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>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default IbuHamilPage;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
|||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const { isDark } = useDarkMode();
|
const { isDark } = useDarkMode();
|
||||||
const tokens = themeTokens(isDark);
|
const tokens = themeTokens(isDark);
|
||||||
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [opened, { toggle, close }] = useDisclosure();
|
const [opened, { toggle, close }] = useDisclosure();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -114,7 +114,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
case 2:
|
case 2:
|
||||||
return '/admin/landing-page/profil/program-inovasi';
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
case 3:
|
case 3:
|
||||||
return '/admin/kesehatan/posyandu';
|
return '/admin/kesehatan/posyandu/list-posyandu';
|
||||||
case 4:
|
case 4:
|
||||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function WaitingRoom() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
const [retryCount, setRetryCount] = useState(0);
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
|
|
||||||
// ⏱️ Countdown timer
|
// ⏱️ Countdown timer
|
||||||
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
|
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
|
||||||
const [hasTimedOut, setHasTimedOut] = useState(false);
|
const [hasTimedOut, setHasTimedOut] = useState(false);
|
||||||
@@ -128,7 +128,7 @@ export default function WaitingRoom() {
|
|||||||
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||||
break;
|
break;
|
||||||
case "3":
|
case "3":
|
||||||
redirectPath = '/admin/kesehatan/posyandu';
|
redirectPath = '/admin/kesehatan/posyandu/list-posyandu';
|
||||||
break;
|
break;
|
||||||
case "4":
|
case "4":
|
||||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
@@ -200,9 +200,9 @@ export default function WaitingRoom() {
|
|||||||
Silakan hubungi Superadmin atau coba login ulang nanti.
|
Silakan hubungi Superadmin atau coba login ulang nanti.
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
>
|
>
|
||||||
Kembali ke Login
|
Kembali ke Login
|
||||||
@@ -243,11 +243,11 @@ export default function WaitingRoom() {
|
|||||||
<Title order={2} c={colors['blue-button']} ta="center">
|
<Title order={2} c={colors['blue-button']} ta="center">
|
||||||
⏳ Menunggu Persetujuan
|
⏳ Menunggu Persetujuan
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text ta="center" c="dimmed">
|
<Text ta="center" c="dimmed">
|
||||||
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text ta="center" size="sm" fw={500}>
|
<Text ta="center" size="sm" fw={500}>
|
||||||
Nomor: {user?.nomor || '...'}
|
Nomor: {user?.nomor || '...'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -260,8 +260,8 @@ export default function WaitingRoom() {
|
|||||||
{formatTime(timeLeft)}
|
{formatTime(timeLeft)}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Progress
|
<Progress
|
||||||
value={progressValue}
|
value={progressValue}
|
||||||
color={timeLeft < 60 ? 'red' : colors['blue-button']}
|
color={timeLeft < 60 ? 'red' : colors['blue-button']}
|
||||||
size="sm"
|
size="sm"
|
||||||
animated
|
animated
|
||||||
@@ -269,15 +269,15 @@ export default function WaitingRoom() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Loader size="sm" color={colors['blue-button']} />
|
<Loader size="sm" color={colors['blue-button']} />
|
||||||
|
|
||||||
<Text ta="center" size="xs" c="dimmed">
|
<Text ta="center" size="xs" c="dimmed">
|
||||||
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 🚪 Tombol Keluar */}
|
{/* 🚪 Tombol Keluar */}
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user