Compare commits
30 Commits
5cd6e3aa99
...
stg
| Author | SHA1 | Date | |
|---|---|---|---|
| 16b9b74a73 | |||
| c0b08f4f69 | |||
| 7b14923620 | |||
| 3cc09c83d8 | |||
| 5658063f68 | |||
| d7e1192ab0 | |||
| 8857853baf | |||
| ce26bc7cc8 | |||
| b479991c27 | |||
| e71c938b2f | |||
| ff25ead2df | |||
| 2497298703 | |||
| ba632f9d39 | |||
| f1ee53a7b9 | |||
| fd2060405f | |||
| afe0d9d04b | |||
| dccba1f82b | |||
| fc6846f7a1 | |||
| 9ef5773cc2 | |||
| 68a2a6390b | |||
| ba2b90be75 | |||
| 3cc30bf0ff | |||
| 0a5d17f45e | |||
| 83a2dece57 | |||
| e0a5177257 | |||
| 23c955597e | |||
| 28a22e8d77 | |||
| 67efe6ce35 | |||
| b39f9b39da | |||
| 6041cdf552 |
@@ -8,7 +8,7 @@
|
||||
- **UI**: Mantine UI v7-8
|
||||
- **State**: Jotai (atoms), Valtio (proxies), SWR (data fetching)
|
||||
- **Auth**: iron-session + JWT
|
||||
- **File storage**: Local uploads + Seafile (self-hosted)
|
||||
- **File storage**: Local uploads + MinIO (object storage) + Seafile (self-hosted fallback)
|
||||
|
||||
## Request Flow
|
||||
|
||||
@@ -20,14 +20,16 @@ Browser → Next.js middleware (src/middleware.ts)
|
||||
└── _lib/*.ts (domain modules)
|
||||
```
|
||||
|
||||
The Elysia server is a single entry point with domain-specific modules: `desa.ts`, `kesehatan.ts`, `ekonomi.ts`, `keamanan.ts`, `lingkungan.ts`, `pendidikan.ts`, `kependudukan.ts`, `ppid.ts`, `inovasi.ts`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`.
|
||||
The Elysia server is a single entry point with domain-specific modules: `desa/`, `kesehatan/`, `ekonomi/`, `keamanan/`, `lingkungan/`, `pendidikan/`, `kependudukan/`, `ppid/`, `inovasi/`, `landing_page/`, `search/`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`.
|
||||
|
||||
## Domain Modules
|
||||
Each domain (desa, kesehatan, ekonomi, etc.) has:
|
||||
- API handler in `src/app/api/[[...slugs]]/_lib/<domain>.ts`
|
||||
- API handler in `src/app/api/[[...slugs]]/_lib/<domain>/`
|
||||
- Admin CMS pages in `src/app/admin/(dashboard)/<domain>/`
|
||||
- Public pages in `src/app/darmasaba/(pages)/<domain>/`
|
||||
|
||||
Active domains: `desa`, `ekonomi`, `inovasi`, `keamanan`, `kependudukan`, `kesehatan`, `lingkungan`, `musik`, `pendidikan`, `ppid` — plus `landing_page` and `search` (API-only, no public/admin pages).
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|
||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -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:
|
||||
|
||||
321
AI-CONTRACT.md
Normal file
321
AI-CONTRACT.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# AI-CONTRACT.md
|
||||
|
||||
Kontrak kerja antara **manusia (developer)** dan **AI assistant** (Claude Code,
|
||||
Cursor, Copilot, atau agent coding lainnya) di repo ini. Tujuannya satu:
|
||||
mencegah perbaikan 1 bug berubah jadi 3 bug baru (bug eksponensial). AI
|
||||
**wajib** baca file ini sebelum menulis/menghapus kode.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prinsip Dasar
|
||||
|
||||
1. **Minimal diff, maximal pemahaman.** Baca kode sebelum ubah. Jangan
|
||||
refactor yang tidak diminta. Jangan "rapikan" kode di sekitar bug.
|
||||
2. **Fix akar, bukan gejala.** Kalau error muncul di layer A tapi penyebab
|
||||
di layer B, perbaiki B. Jangan tambal di A.
|
||||
3. **Satu masalah = satu perubahan logis.** Jangan campur fix bug dengan
|
||||
refactor, rename, atau fitur baru dalam satu sesi tanpa izin.
|
||||
4. **Tidak ada asumsi diam-diam.** Kalau butuh info (nama field, endpoint,
|
||||
flow, schema), tanya atau baca kode — jangan tebak.
|
||||
5. **Setiap perubahan harus reversible.** Diff kecil, commit jelas, bisa
|
||||
di-revert tanpa efek samping.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sebelum Menulis Kode
|
||||
|
||||
Checklist wajib sebelum edit file:
|
||||
|
||||
- [ ] Sudah baca file target (bukan cuma potongan)
|
||||
- [ ] Tahu siapa yang memanggil fungsi/komponen yang akan diubah
|
||||
- [ ] Tahu apakah ada test/konsumer lain yang bergantung padanya
|
||||
- [ ] Tahu layer yang benar (route / controller / component / hook /
|
||||
service / repository / lib / util — sesuai arsitektur project)
|
||||
- [ ] Cek dokumen panduan project (mis. `CLAUDE.md`, `CONTRIBUTING.md`,
|
||||
`ARCHITECTURE.md`, ADR) untuk aturan spesifik
|
||||
- [ ] Kalau ubah tipe/kontrak (API, function signature, schema), cek
|
||||
semua pemakai
|
||||
|
||||
Jika salah satu tidak jelas: **berhenti, baca lagi, atau tanya user.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Saat Fix Bug
|
||||
|
||||
1. **Reproduksi dulu di kepala.** Jelaskan (minimal ke diri sendiri)
|
||||
kenapa bug terjadi sebelum menyentuh kode.
|
||||
2. **Temukan akar sebenarnya.** "Karena field X `undefined`/`null`/empty"
|
||||
bukan akar — akarnya kenapa X bisa kosong.
|
||||
3. **Perbaiki sekecil mungkin.** Kalau cukup 3 baris, jangan ubah 30.
|
||||
4. **Jangan tambah try/catch hanya untuk menyembunyikan error** — itu
|
||||
melahirkan bug baru yang lebih sulit dilacak.
|
||||
5. **Jangan tambah fallback/default value spekulatif.** Kalau field
|
||||
seharusnya selalu ada, perbaiki kenapa bisa kosong.
|
||||
6. **Jangan rename, reorder, atau reformat** di file yang sama kecuali
|
||||
langsung terkait fix.
|
||||
7. **Setelah fix, verifikasi**: minimal jalankan typecheck/lint sesuai
|
||||
tooling project (mis. `tsc`, `eslint`, `ruff`, `mypy`, `cargo check`,
|
||||
`go vet`, `rspec`, dll). Idealnya jalankan test suite yang relevan.
|
||||
|
||||
---
|
||||
|
||||
## 4. Yang Dilarang (Akar Bug Eksponensial)
|
||||
|
||||
- ❌ **Silent catch**: `catch (e) {}`, `except: pass`, `_ = err`, atau
|
||||
pola serupa — tanpa alasan yang didokumentasi di komentar.
|
||||
- ❌ **Comment-out kode** sebagai "backup". Hapus atau kembalikan, jangan
|
||||
biarkan mayat — git sudah jadi backup.
|
||||
- ❌ **Copy-paste antar file**. Extract ke shared module/util/helper.
|
||||
- ❌ **Duplikasi util/helper/hook/service** yang sudah ada — cek dulu
|
||||
sebelum bikin baru.
|
||||
- ❌ **Tambah flag/opsi/parameter baru** hanya untuk menghindari break
|
||||
konsumer lama — fix konsumernya sekalian.
|
||||
- ❌ **Destructive git command** (`reset --hard`, `push --force`,
|
||||
`branch -D`, `clean -fdx`) tanpa instruksi eksplisit.
|
||||
- ❌ **Skip hook** (`--no-verify`, `--no-gpg-sign`) tanpa izin.
|
||||
- ❌ **Ubah schema/migrasi database** tanpa migration file yang sesuai.
|
||||
- ❌ **Tambah dependency baru** tanpa izin user.
|
||||
- ❌ **Hardcode credential, secret, URL produksi, atau data user**.
|
||||
- ❌ **Ubah konfigurasi CI/CD, environment, atau infra** tanpa diskusi.
|
||||
|
||||
---
|
||||
|
||||
## 5. Saat Menambah Fitur
|
||||
|
||||
- Baca panduan arsitektur project sebelum mulai.
|
||||
- Tentukan layer sebelum menulis. Jangan taruh bisnis logika di route,
|
||||
controller, atau komponen presentasi.
|
||||
- Jangan buat abstraksi untuk kebutuhan hipotetis. Tulis kode yang
|
||||
diminta sekarang (YAGNI — *You Aren't Gonna Need It*).
|
||||
- Hormati batas ukuran file yang sudah disepakati di project. Kalau
|
||||
belum ada, gunakan rule of thumb: file >500 baris = sinyal untuk
|
||||
pisah; fungsi >50 baris = sinyal untuk extract.
|
||||
- Ikuti konvensi naming, struktur folder, dan pattern yang sudah ada —
|
||||
konsistensi lebih penting dari preferensi pribadi.
|
||||
|
||||
---
|
||||
|
||||
## 6. Saat Ragu
|
||||
|
||||
Urutan tindakan:
|
||||
|
||||
1. Baca kode terkait lebih dalam.
|
||||
2. Cek dokumen panduan project (`CLAUDE.md`, `README.md`, ADR, dll).
|
||||
3. Cek git history (`git log -p`, `git blame`) kalau pertanyaannya soal
|
||||
"kenapa ini begini".
|
||||
4. **Tanya user** — lebih baik tanya 1 pertanyaan daripada menulis 100
|
||||
baris yang harus dibuang.
|
||||
|
||||
Jangan pernah "pokoknya coba dulu, kalau salah revert". Revert itu murah
|
||||
di local, tapi mahal kalau sudah merusak state (DB, session, file
|
||||
sistem, deployment, dll).
|
||||
|
||||
---
|
||||
|
||||
## 7. Saat Selesai
|
||||
|
||||
- Jelaskan perubahan **secara singkat**: apa, di mana (file:line), kenapa.
|
||||
- Sebutkan efek samping kalau ada (perubahan kontrak, breaking change,
|
||||
perlu migrasi, perlu restart service, dll).
|
||||
- Jangan ringkas diff yang user sudah lihat — user baca kode langsung.
|
||||
- Kalau project punya channel notifikasi atau workflow report
|
||||
(Slack/Discord/Telegram/email), kirim sesuai konvensi.
|
||||
|
||||
---
|
||||
|
||||
## 8. Eskalasi
|
||||
|
||||
Hentikan pekerjaan dan tanya user kalau:
|
||||
|
||||
- Fix butuh ubah >5 file untuk bug yang kelihatannya kecil.
|
||||
- Ketemu bug lain di tengah jalan yang tidak diminta.
|
||||
- Perubahan berpotensi mengenai data produksi, session aktif, atau
|
||||
user nyata.
|
||||
- User memberi instruksi yang bertentangan dengan dokumen panduan
|
||||
project — konfirmasi dulu sebelum melanggar aturan.
|
||||
|
||||
---
|
||||
|
||||
## 9. Tools sebagai Mata dan Tangan AI
|
||||
|
||||
AI **wajib** memakai tools yang tersedia (MCP server, CLI commands,
|
||||
debugger, browser automation, log inspector, DB query tool, dll) sebagai
|
||||
**mata dan tangan**-nya.
|
||||
|
||||
- **Mata**: sebelum menebak state sistem, AI harus lihat langsung. Cek
|
||||
log, query DB read-only, baca file config, jalankan health check, atau
|
||||
pakai tool inspeksi yang relevan. Jangan berasumsi tentang data,
|
||||
konfigurasi, atau tampilan — **cek dulu**.
|
||||
- **Tangan**: gunakan tools untuk verifikasi end-to-end setelah
|
||||
perubahan. Contoh: setelah fix UI, jalankan/preview halamannya dan
|
||||
pastikan render + console bersih. Setelah fix logic, jalankan test
|
||||
atau panggil endpoint yang relevan.
|
||||
- **Maksimalkan pemakaian.** Kalau ada tool yang relevan, pakai — jangan
|
||||
memilih jalan manual yang lebih rapuh. Semakin sering tools dipakai
|
||||
untuk verifikasi, semakin solid project ini.
|
||||
- **Ajukan tool baru kalau perlu.** Kalau AI merasa butuh tool yang
|
||||
belum ada, AI **boleh dan didorong** untuk mengajukan pembuatannya
|
||||
ke user. Format pengajuan:
|
||||
1. Nama tool + signature (input/output)
|
||||
2. Kenapa dibutuhkan (masalah konkret yang sedang dihadapi)
|
||||
3. Sumber data (tabel DB / cache key / endpoint / file system)
|
||||
4. Estimasi dampak ke kualitas investigasi/perbaikan
|
||||
- **Jangan buat tool baru tanpa izin.** Ajukan dulu, tunggu persetujuan
|
||||
user, baru implementasi (+ update dokumentasi).
|
||||
- **Tools adalah sumber kebenaran runtime.** Kalau memory/log mengatakan
|
||||
X tapi tool inspeksi langsung mengatakan Y, percayai tool.
|
||||
|
||||
Tujuan: AI tidak buta terhadap state sistem nyata, dan setiap perbaikan
|
||||
diverifikasi secara nyata — bukan "harusnya sudah jalan".
|
||||
|
||||
---
|
||||
|
||||
## 10. Kontrak Public API / Interface (Wajib Dijaga)
|
||||
|
||||
Setiap interface yang dipakai oleh konsumen eksternal — REST/GraphQL
|
||||
endpoint, MCP tool, library export, CLI command, webhook payload, event
|
||||
schema, dll — adalah **kontrak publik**. Begitu konsumen (termasuk AI
|
||||
agent dengan memory) tahu bentuk kontraknya, perubahan diam-diam bisa
|
||||
bikin mereka bertindak berdasarkan asumsi yang sudah tidak valid — dan
|
||||
kamu **tidak akan tahu** sampai terjadi kejadian aneh di prod.
|
||||
|
||||
### Apa yang dianggap kontrak (freeze)
|
||||
|
||||
| Kategori | Contoh | Aturan |
|
||||
| ----------------- | --------------------------------------- | -------------------------------------------------- |
|
||||
| Nama interface | endpoint path, tool name, function name | Tidak boleh rename tanpa bump versi |
|
||||
| Parameter input | nama field, tipe, posisi | Nama & tipe tidak boleh berubah |
|
||||
| Required flag | field wajib | Tidak boleh naik (optional → required) tanpa versi |
|
||||
| Enum values | nilai yang valid | Tidak boleh dihapus/diganti |
|
||||
| Error mode | format error response, exception type | Pola error harus konsisten |
|
||||
| Field output | bentuk response | Tidak boleh dihapus/diganti tipenya |
|
||||
|
||||
### Apa yang boleh berubah (additive)
|
||||
|
||||
- Tambah interface baru
|
||||
- Tambah parameter **optional** baru
|
||||
- Tambah field output baru (konsumen lama akan mengabaikan yang tidak
|
||||
mereka tahu, asal parsing-nya tolerant)
|
||||
- Perbaiki pesan error (tanpa ubah polanya)
|
||||
- Refactor implementasi internal (query, helper, dll)
|
||||
|
||||
### Cara kerja penjaga kontrak
|
||||
|
||||
1. **Contract test**: snapshot bentuk kontrak (nama, required,
|
||||
properties, enum) untuk setiap interface publik. Letakkan di folder
|
||||
khusus mis. `tests/contract/`.
|
||||
2. **Kalau contract test merah karena perubahan yang disengaja**:
|
||||
1. Update dokumentasi kontrak
|
||||
2. Bump versi (semver, tag, atau version field)
|
||||
3. Update snapshot di contract test
|
||||
4. Jelaskan migrasinya di commit message + changelog
|
||||
3. **Kalau contract test merah karena refactor yang tidak disengaja**:
|
||||
**Jangan update snapshot untuk menghijaukan test.** Balikkan refactor
|
||||
atau perbaiki supaya kontrak tetap sama. Snapshot bukan sampah yang
|
||||
bisa di-regenerate seenaknya — dia alarm kebakaran.
|
||||
|
||||
### Larangan spesifik
|
||||
|
||||
- ❌ **Jangan rename** interface publik tanpa migration plan + bump versi
|
||||
- ❌ **Jangan hapus enum value** — konsumen bisa punya kode/memory yang
|
||||
memanggil nilai itu
|
||||
- ❌ **Jangan naikkan param dari optional → required** tanpa bump versi
|
||||
- ❌ **Jangan ubah bentuk error** (format response ↔ throw exception) —
|
||||
ini mengubah handler logic di sisi konsumen
|
||||
- ❌ **Jangan update snapshot contract test** tanpa update dokumentasi
|
||||
|
||||
### Apa yang BUKAN tugas contract test
|
||||
|
||||
- Memverifikasi logika bisnis (itu unit test biasa)
|
||||
- Memverifikasi integrasi DB/external service (itu integration test)
|
||||
- Memastikan data yang di-return benar (itu QA / staging)
|
||||
|
||||
Contract test **hanya** menjaga bentuk kontrak — cepat, deterministic,
|
||||
tanpa dependency eksternal.
|
||||
|
||||
---
|
||||
|
||||
## 11. Hygiene Dokumen Panduan AI
|
||||
|
||||
Dokumen panduan AI (`CLAUDE.md`, `AI-CONTRACT.md`, `.cursorrules`,
|
||||
`.github/copilot-instructions.md`, dll) di-load **setiap turn** percakapan.
|
||||
Semakin gemuk file utamanya, semakin banyak token terbuang setiap turn —
|
||||
dan ironisnya, AI jadi lebih sulit menemukan info penting karena tertimbun
|
||||
detail. File panduan yang gemuk **bukan** tanda dokumentasi yang baik;
|
||||
sering justru sebaliknya.
|
||||
|
||||
### Pecah, jangan tumpuk
|
||||
|
||||
Gunakan **referensi file** alih-alih menumpuk semua di satu file. Banyak
|
||||
AI agent (termasuk Claude Code) auto-load file yang di-reference dengan
|
||||
sintaks `@path/to/file.md`. Contoh struktur `CLAUDE.md` yang sehat:
|
||||
|
||||
````markdown
|
||||
## Architecture
|
||||
See @docs/ARCHITECTURE.md
|
||||
|
||||
## Agent Specs
|
||||
See @docs/AGENTIC_OVERVIEW.md
|
||||
|
||||
## ADR History
|
||||
See @docs/adr/README.md
|
||||
````
|
||||
|
||||
`CLAUDE.md` utama tetap ramping, tapi info detail tetap accessible saat
|
||||
dibutuhkan.
|
||||
|
||||
### Apa yang WAJIB tetap di CLAUDE.md (load setiap turn)
|
||||
|
||||
- Konvensi coding inti (naming, formatting, import order)
|
||||
- Perintah build/test/lint yang sering dipakai
|
||||
- Aturan komunikasi (bahasa, gaya, format response)
|
||||
- Struktur folder high-level (1-2 level)
|
||||
- Larangan absolut (jangan commit ke main, jangan touch folder X, dll)
|
||||
- **Pointer** (`@path/...`) ke file detail lainnya
|
||||
|
||||
### Apa yang DIPINDAH ke file terpisah
|
||||
|
||||
- Spec arsitektur lengkap → `docs/ARCHITECTURE.md`
|
||||
- Detail flow / sequence diagram → `docs/flows/*.md`
|
||||
- ADR history (Architecture Decision Records) → `docs/adr/`
|
||||
- Contoh kode panjang → `docs/examples/`
|
||||
- API/interface reference lengkap → `docs/api/`
|
||||
- Onboarding & setup detail → `docs/SETUP.md`
|
||||
- Glossary domain terminology → `docs/GLOSSARY.md`
|
||||
- Catatan investigasi/post-mortem → `docs/incidents/`
|
||||
|
||||
Lokasi alternatif: `.claude/` atau `.ai/` kalau tim ingin memisahkan
|
||||
khusus untuk AI tooling, di luar dokumentasi developer biasa.
|
||||
|
||||
### Cek duplikasi secara rutin
|
||||
|
||||
Info yang sama sering muncul di beberapa section seiring waktu — biasanya
|
||||
karena ditambahkan saat debugging tanpa cek file dulu. Audit berkala:
|
||||
|
||||
- [ ] Sama-sama dijelaskan di `README.md` dan `CLAUDE.md`? Pilih satu,
|
||||
yang lain referensikan.
|
||||
- [ ] Aturan yang sama disebut di 2-3 section? Konsolidasi ke satu section
|
||||
kanonik, section lain tinggal pointer.
|
||||
- [ ] Contoh kode panjang muncul inline? Pindah ke `docs/examples/`.
|
||||
- [ ] Konvensi yang sudah jadi default di linter/formatter masih ditulis
|
||||
manual? Hapus — biarkan tooling yang jaga, dokumen tidak perlu
|
||||
mengulang.
|
||||
- [ ] Info yang sudah usang (refer ke file/fitur yang dihapus)? Bersihkan
|
||||
— dokumen yang setengah benar lebih merusak daripada tidak ada.
|
||||
|
||||
### Rule of thumb ukuran
|
||||
|
||||
Kalau `CLAUDE.md` (atau equivalent) sudah > 300 baris, itu **sinyal kuat**
|
||||
untuk pecah file. Dokumen panduan AI yang ideal: cukup pendek untuk dibaca
|
||||
ulang dalam 1 menit oleh manusia, dengan pointer ke detail untuk AI yang
|
||||
butuh konteks lebih dalam.
|
||||
|
||||
---
|
||||
|
||||
## 12. Aturan Emas
|
||||
|
||||
> **Lebih baik tidak melakukan apa-apa daripada memperburuk kode.**
|
||||
>
|
||||
> Kalau setelah 2 kali percobaan fix masih memunculkan bug baru, **stop**.
|
||||
> Laporkan ke user, jelaskan apa yang sudah dicoba dan kenapa gagal.
|
||||
> Jangan tambal terus — itu cara bug beranak eksponensial.
|
||||
@@ -30,6 +30,7 @@ bun eslint . --fix
|
||||
- Architecture, request flow, domain modules, key files: @.claude/ARCHITECTURE.md
|
||||
- Database conventions, auth flow, file handling: @.claude/DATABASE.md
|
||||
- Env vars, Docker, CI/CD, releasing: @.claude/DEPLOYMENT.md
|
||||
- AI collaboration contract, rules, and guidelines: @AI-CONTRACT.md
|
||||
|
||||
### Workflow for Code Changes
|
||||
1. **Commit** existing changes before starting new work
|
||||
|
||||
19
MIND/PLAN/ai-collaboration-contract.md
Normal file
19
MIND/PLAN/ai-collaboration-contract.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Plan: Add AI Collaboration Contract and Fix UI Issues
|
||||
|
||||
## Background
|
||||
- Need a clear contract for AI collaboration to prevent "exponential bugs".
|
||||
- UI fix for `KegiatanCard` to handle missing images gracefully.
|
||||
|
||||
## Objectives
|
||||
- Add `AI-CONTRACT.md` with guidelines.
|
||||
- Link `AI-CONTRACT.md` in `CLAUDE.md`.
|
||||
- Fix `KegiatanCard.tsx` image rendering.
|
||||
- Bump version to 0.1.48.
|
||||
|
||||
## Implementation Steps
|
||||
1. Create `AI-CONTRACT.md`.
|
||||
2. Update `CLAUDE.md`.
|
||||
3. Update `KegiatanCard.tsx`.
|
||||
4. Bump version in `package.json`.
|
||||
5. Verify build.
|
||||
6. Commit and push.
|
||||
11
MIND/PLAN/task-ai-collaboration-contract.md
Normal file
11
MIND/PLAN/task-ai-collaboration-contract.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Task: Add AI Collaboration Contract and Fix UI Issues
|
||||
|
||||
## Status
|
||||
- [x] Create `AI-CONTRACT.md`
|
||||
- [x] Update `CLAUDE.md` to reference the contract
|
||||
- [x] Fix `KegiatanCard.tsx` image rendering logic
|
||||
- [x] Bump version in `package.json` to 0.1.48
|
||||
- [x] Verify build successful
|
||||
- [x] Commit changes
|
||||
- [x] Create branch and push
|
||||
- [x] Merge to `stg`
|
||||
246
MIND/PLAN/task-statistik-kesehatan-ringkasan.md
Normal file
246
MIND/PLAN/task-statistik-kesehatan-ringkasan.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# Task: Refactor Ringkasan Kesehatan → Dashboard Auto-Derived
|
||||
|
||||
## Latar Belakang
|
||||
|
||||
Page `/admin/kesehatan/ringkasan-kesehatan` saat ini = form `NumberInput`
|
||||
manual (7 angka diketik operator). Itu **salah konsep**: "Ringkasan" harus
|
||||
**hasil agregasi** dari data sumber, bukan input bebas.
|
||||
|
||||
Solusi: bikin 2 entitas data sumber (`IbuHamil`, `Balita`), lalu derive
|
||||
KPI + statistik kesehatan dari query agregat. Page ringkasan jadi
|
||||
**dashboard read-only** + tombol "Kelola Ibu Hamil" / "Kelola Balita".
|
||||
|
||||
## Mapping Field → Sumber Data
|
||||
|
||||
| Field di Ringkasan | Sumber |
|
||||
|---|---|
|
||||
| `ibuHamilAktif` | `count(IbuHamil where status=AKTIF, isActive=true)` |
|
||||
| `balitaTerdaftar` | `count(Balita where isActive=true)` |
|
||||
| `alertStunting` | `count(Balita where statusStunting IN [ALERT, STUNTING])` |
|
||||
| `imunisasiLengkapPct` | `count(imunisasiLengkap=true) / total balita * 100` |
|
||||
| `pemeriksaanRutinPct` | `count(pemeriksaanRutin=true) / total balita * 100` |
|
||||
| `giziBaikPct` | `count(giziBaik=true) / total balita * 100` |
|
||||
| `targetStuntingPct` | tetap di `RingkasanKesehatanDesa` (policy target, bukan derived) |
|
||||
|
||||
## Kontrak Lama (jaga, AI-CONTRACT §10)
|
||||
|
||||
- `GET /api/kesehatan/ringkasankesehatan/find` — tetap return shape lama
|
||||
- `PUT /api/kesehatan/ringkasankesehatan/update` — tetap menerima 7 field lama
|
||||
- Tambah baru: `GET /api/kesehatan/ringkasankesehatan/stats` (agregat)
|
||||
- Tambah baru: `/api/kesehatan/ibuhamil/*`, `/api/kesehatan/balita/*`
|
||||
|
||||
> Alasan: ada konsumer eksternal (mobile/landing) yang mungkin sudah
|
||||
> baca shape lama. Refactor admin tidak boleh memutus kontrak existing.
|
||||
> Field manual lama akan ditandai deprecated di komentar schema saat
|
||||
> tidak lagi dipakai admin UI.
|
||||
|
||||
---
|
||||
|
||||
## Step A — Model `IbuHamil`
|
||||
|
||||
### Schema
|
||||
|
||||
```prisma
|
||||
enum IbuHamilStatus {
|
||||
AKTIF
|
||||
MELAHIRKAN
|
||||
KEGUGURAN
|
||||
NONAKTIF
|
||||
}
|
||||
|
||||
model IbuHamil {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
nik String?
|
||||
usiaKehamilan Int @default(0) // minggu
|
||||
hpht DateTime? // hari pertama haid terakhir
|
||||
taksiranLahir DateTime?
|
||||
alamat String?
|
||||
noHp String?
|
||||
catatan String?
|
||||
posyanduId String?
|
||||
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
||||
status IbuHamilStatus @default(AKTIF)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
### API `src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/`
|
||||
|
||||
Pattern mirror `posyandu/`:
|
||||
- `index.ts` — Elysia routes (prefix `/ibuhamil`)
|
||||
- `create.ts` — POST `/create`
|
||||
- `find-many.ts` — GET `/find-many?page=&limit=&search=&status=`
|
||||
- `find-by-id.ts` — GET `/:id`
|
||||
- `updt.ts` — PUT `/:id`
|
||||
- `del.ts` — DELETE `/del/:id` (soft delete `isActive=false`)
|
||||
|
||||
Daftarkan di `kesehatan/index.ts`: `.use(IbuHamil)`
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 1. Tambah enum `IbuHamilStatus` + model `IbuHamil` ke `prisma/schema.prisma`
|
||||
- [ ] 2. Tambah `@relation` reverse di model `Posyandu` (`ibuHamil IbuHamil[]`)
|
||||
- [ ] 3. Generate migration `add_ibu_hamil` (manual SQL bila sandbox)
|
||||
- [ ] 4. Buat 6 file API (index/create/find-many/find-by-id/updt/del)
|
||||
- [ ] 5. Register di `kesehatan/index.ts`
|
||||
- [ ] 6. State file `src/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil.ts`
|
||||
pattern Valtio + zod (mirror `posyandu/posyandu.ts`)
|
||||
- [ ] 7. Admin pages:
|
||||
- `src/app/admin/(dashboard)/kesehatan/ibu-hamil/page.tsx` (list)
|
||||
- `src/app/admin/(dashboard)/kesehatan/ibu-hamil/create/page.tsx`
|
||||
- `src/app/admin/(dashboard)/kesehatan/ibu-hamil/edit/[id]/page.tsx`
|
||||
- [ ] 8. Sidebar entry: tambah "Ibu Hamil" di group Kesehatan
|
||||
- [ ] 9. `bun run build` exit 0
|
||||
- [ ] 10. Bump version + commit + push
|
||||
|
||||
---
|
||||
|
||||
## Step B — Model `Balita`
|
||||
|
||||
### Schema
|
||||
|
||||
```prisma
|
||||
enum JenisKelaminBalita {
|
||||
L
|
||||
P
|
||||
}
|
||||
|
||||
enum StatusStunting {
|
||||
NORMAL
|
||||
ALERT
|
||||
STUNTING
|
||||
}
|
||||
|
||||
model Balita {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
nik String?
|
||||
tanggalLahir DateTime
|
||||
jenisKelamin JenisKelaminBalita
|
||||
beratBadanKg Float?
|
||||
tinggiBadanCm Float?
|
||||
namaOrtu String?
|
||||
alamat String?
|
||||
noHpOrtu String?
|
||||
posyanduId String?
|
||||
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
||||
imunisasiLengkap Boolean @default(false)
|
||||
giziBaik Boolean @default(true)
|
||||
pemeriksaanRutin Boolean @default(true)
|
||||
statusStunting StatusStunting @default(NORMAL)
|
||||
catatan String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
### API `src/app/api/[[...slugs]]/_lib/kesehatan/balita/`
|
||||
|
||||
Sama pattern dengan ibu-hamil (6 file). Body validator Elysia:
|
||||
|
||||
```ts
|
||||
t.Object({
|
||||
nama: t.String(),
|
||||
nik: t.Optional(t.String()),
|
||||
tanggalLahir: t.String(), // ISO string
|
||||
jenisKelamin: t.Union([t.Literal('L'), t.Literal('P')]),
|
||||
beratBadanKg: t.Optional(t.Number()),
|
||||
tinggiBadanCm: t.Optional(t.Number()),
|
||||
namaOrtu: t.Optional(t.String()),
|
||||
alamat: t.Optional(t.String()),
|
||||
noHpOrtu: t.Optional(t.String()),
|
||||
posyanduId: t.Optional(t.String()),
|
||||
imunisasiLengkap: t.Boolean(),
|
||||
giziBaik: t.Boolean(),
|
||||
pemeriksaanRutin: t.Boolean(),
|
||||
statusStunting: t.Union([t.Literal('NORMAL'), t.Literal('ALERT'), t.Literal('STUNTING')]),
|
||||
catatan: t.Optional(t.String()),
|
||||
})
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 1. Tambah enum + model `Balita` ke schema
|
||||
- [ ] 2. `@relation` reverse di Posyandu (`balita Balita[]`)
|
||||
- [ ] 3. Migration `add_balita`
|
||||
- [ ] 4. 6 file API
|
||||
- [ ] 5. Register di `kesehatan/index.ts`
|
||||
- [ ] 6. State file `_state/kesehatan/balita/balita.ts`
|
||||
- [ ] 7. Admin pages list / create / edit (form dengan 4 toggle:
|
||||
`imunisasiLengkap`, `giziBaik`, `pemeriksaanRutin` + Select
|
||||
`statusStunting`)
|
||||
- [ ] 8. Sidebar entry "Balita" di group Kesehatan
|
||||
- [ ] 9. `bun run build` exit 0
|
||||
- [ ] 10. Bump version + commit + push
|
||||
|
||||
---
|
||||
|
||||
## Step C — Endpoint Stats Agregat
|
||||
|
||||
### File baru `src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/stats.ts`
|
||||
|
||||
Return:
|
||||
|
||||
```ts
|
||||
{
|
||||
ibuHamilAktif: number,
|
||||
balitaTerdaftar: number,
|
||||
alertStunting: number,
|
||||
imunisasiLengkapPct: number, // round
|
||||
pemeriksaanRutinPct: number,
|
||||
giziBaikPct: number,
|
||||
targetStuntingPct: number, // dari RingkasanKesehatanDesa
|
||||
}
|
||||
```
|
||||
|
||||
Implementasi (Prisma):
|
||||
- 1 `count` IbuHamil + 1 `groupBy` Balita (status flags)
|
||||
- Hitung pct di JS: `Math.round((n / total) * 100)`
|
||||
- Edge case `total=0` → semua pct = 0
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 1. Bikin `stats.ts` handler
|
||||
- [ ] 2. Daftarkan route `GET /stats` di `ringkasan-kesehatan/index.ts`
|
||||
- [ ] 3. Update state `ringkasanKesehatan.ts`:
|
||||
- tambah `findStats: { data, loading, load() }`
|
||||
- method lama `findUnique` + `update` → tetap (kontrak)
|
||||
- [ ] 4. Refactor `page.tsx` ringkasan:
|
||||
- section atas: 7 stat card read-only dari `findStats.data`
|
||||
- section bawah: form kecil hanya `targetStuntingPct` (policy target)
|
||||
- 2 tombol: "Kelola Ibu Hamil" → push `/admin/kesehatan/ibu-hamil`
|
||||
"Kelola Balita" → push `/admin/kesehatan/balita`
|
||||
- [ ] 5. `bun run build` exit 0
|
||||
- [ ] 6. Bump version + commit + push
|
||||
|
||||
---
|
||||
|
||||
## Step D — Cleanup (opsional, tunggu user konfirmasi)
|
||||
|
||||
Field manual `ibuHamilAkh`, `balitaTerdaftar`, `alertStunting`,
|
||||
`imunisasiLengkapPct`, `pemeriksaanRutinPct`, `giziBaikPct` di
|
||||
`RingkasanKesehatanDesa` jadi **deprecated**.
|
||||
|
||||
- Opsi 1 (aman): tinggalkan, tandai `// DEPRECATED — derived from /stats`
|
||||
- Opsi 2 (bersih): hapus + bump major version + update kontrak konsumer
|
||||
|
||||
**Tunggu input user** — jangan eksekusi tanpa izin.
|
||||
|
||||
---
|
||||
|
||||
## Pending Manual
|
||||
|
||||
- [ ] User jalankan `bunx prisma migrate deploy` di lokal setelah Step A & B
|
||||
- [ ] User trigger publish + re-pull workflow bila mau deploy STG
|
||||
|
||||
## Prinsip
|
||||
|
||||
- **Additive**: kontrak `/find` + `/update` ringkasankesehatan tetap
|
||||
- **Single source of truth**: KPI = derived dari IbuHamil + Balita
|
||||
- **Soft delete** pakai `isActive=false`, jangan hard delete
|
||||
- **YAGNI**: belum bikin chart/grafik tren; cukup angka snapshot
|
||||
- **No breaking change**: konsumer landing/mobile aman selama migrasi
|
||||
19
MIND/SUMMARY/ai-collaboration-contract-summary.md
Normal file
19
MIND/SUMMARY/ai-collaboration-contract-summary.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Summary: Add AI Collaboration Contract and Fix UI Issues
|
||||
|
||||
## Work Completed
|
||||
- **AI Collaboration Contract**: Added `AI-CONTRACT.md` which defines the working agreement between human developers and AI assistants to prevent exponential bugs.
|
||||
- **Documentation Update**: Linked `AI-CONTRACT.md` in `CLAUDE.md`.
|
||||
- **UI Fix**: Modified `KegiatanCard.tsx` to handle cases where an activity item might not have an image, preventing layout issues or broken image icons.
|
||||
- **State Management**: Committed several new state files for Desa Profile (Lambang, Mantan Perbekel, Maskot, Profil Perbekel, Sejarah, Visi Misi).
|
||||
- **Version Bump**: Incremented project version to `0.1.48`.
|
||||
- **Verification**: Ran `bun run build` successfully to ensure no regressions.
|
||||
|
||||
## Branching & Deployment
|
||||
- Created branch: `tasks/ai-collaboration-contract/add-contract-and-fix-ui/2026-04-30-15-45`
|
||||
- Pushed to remotes: `origin` and `deploy`.
|
||||
- Merged to `stg` branch and pushed to all remotes.
|
||||
|
||||
## Impact
|
||||
- Better AI-Human collaboration guidelines to ensure code quality.
|
||||
- Improved robustness of the public `Kegiatan Desa` page.
|
||||
- Completed several state management modules for the admin panel.
|
||||
62
MIND/SUMMARY/statistik-kesehatan-ringkasan-summary.md
Normal file
62
MIND/SUMMARY/statistik-kesehatan-ringkasan-summary.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Summary: Statistik Persentase Kesehatan di RingkasanKesehatanDesa
|
||||
|
||||
## Konteks
|
||||
|
||||
Dashboard admin punya 4 progress bar (Imunisasi Lengkap, Pemeriksaan Rutin,
|
||||
Gizi Baik, Target Stunting) yang sebelumnya belum ada backend-nya. Schema
|
||||
`RingkasanKesehatanDesa` hanya menyimpan 3 field count (`ibuHamilAkh`,
|
||||
`balitaTerdaftar`, `alertStunting`).
|
||||
|
||||
## Yang Dikerjakan
|
||||
|
||||
### Step A — Schema + API (v0.1.49)
|
||||
1. **Schema** — tambah 4 field `Int @default(0)` di `RingkasanKesehatanDesa`:
|
||||
`imunisasiLengkapPct`, `pemeriksaanRutinPct`, `giziBaikPct`, `targetStuntingPct`.
|
||||
2. **Migration** — buat manual SQL di
|
||||
`prisma/migrations/20260504000000_add_statistik_pct_ringkasan_kesehatan/migration.sql`
|
||||
(sandbox non-interaktif, jadi `migrate dev` di-bypass dengan SQL langsung).
|
||||
3. **API** —
|
||||
- `kesehatan/ringkasan-kesehatan/updt.ts` — handler create + update terima 4 field baru.
|
||||
- `kesehatan/ringkasan-kesehatan/index.ts` — Elysia body validation `t.Number({ minimum: 0, maximum: 100 })`.
|
||||
- `findUnique.ts` tidak perlu diubah — `findFirst` tanpa `select` otomatis bawa field baru.
|
||||
4. Build clean, push ke 2 remote (bipprojectbali + nicoarya20), merge ke `stg`.
|
||||
|
||||
### Step B — State Admin (v0.1.50)
|
||||
- File baru `_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan.ts` (Valtio proxy):
|
||||
- `findUnique.load()` — GET `/api/kesehatan/ringkasankesehatan/find`, isi `data` + sync ke `update.form`.
|
||||
- `update.submit()` — zod validate (count ≥ 0, Pct 0-100) → PUT `/api/kesehatan/ringkasankesehatan/update` → refresh.
|
||||
- `update.reset()` — reset form ke default.
|
||||
|
||||
## Decision Log
|
||||
|
||||
- **Gabung ke `RingkasanKesehatanDesa`** (bukan model baru `StatistikKesehatan`)
|
||||
alasan: domain sama (ringkasan kesehatan desa), pattern existing single-row config
|
||||
cocok, single fetch untuk dashboard, migration ringan additive.
|
||||
- **Tipe `Int` (0–100)** — UI render integer %, tidak butuh desimal.
|
||||
- **Suffix `Pct`** — eksplisit semantik persentase (count vs persentase di model yang sama).
|
||||
- **Field lama tidak disentuh** — additive only, sesuai AI-CONTRACT §10.
|
||||
|
||||
## Pending Manual
|
||||
|
||||
- Jalankan `bunx prisma migrate deploy` di terminal lokal → apply kolom baru ke DB.
|
||||
- Trigger GitHub Workflow (publish + re-pull) bila mau deploy STG.
|
||||
|
||||
## Affected Files
|
||||
|
||||
```
|
||||
prisma/schema.prisma
|
||||
prisma/migrations/20260504000000_add_statistik_pct_ringkasan_kesehatan/migration.sql [NEW]
|
||||
src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/updt.ts
|
||||
src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/index.ts
|
||||
src/app/admin/(dashboard)/_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan.ts [NEW]
|
||||
src/app/admin/(dashboard)/kesehatan/ringkasan-kesehatan/page.tsx [NEW]
|
||||
src/app/admin/_com/list_PageAdmin.tsx (3 sidebar instances → tambah Kesehatan_8)
|
||||
package.json (0.1.48 → 0.1.51)
|
||||
MIND/PLAN/task-statistik-kesehatan-ringkasan.md [NEW]
|
||||
```
|
||||
|
||||
## Commits
|
||||
|
||||
- `ba2b90be` feat(kesehatan): tambah 4 field statistik pct ke RingkasanKesehatanDesa - bump ke 0.1.49
|
||||
- `68a2a639` feat(kesehatan): tambah state file ringkasanKesehatan + bump ke 0.1.50
|
||||
- (next) feat(kesehatan): admin page ringkasan-kesehatan + sidebar entry - bump ke 0.1.51
|
||||
706
STRUKTUR.md
706
STRUKTUR.md
@@ -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 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.43",
|
||||
"version": "0.1.56",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
30
prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts
Normal file
30
prisma/_seeder_list/desa/event-budaya/seed_event_budaya.ts
Normal 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");
|
||||
}
|
||||
48
prisma/_seeder_list/desa/seed_kegiatan_desa.ts
Normal file
48
prisma/_seeder_list/desa/seed_kegiatan_desa.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kategoriKegiatanJson = loadJsonData("desa/kegiatan-desa/kategori-kegiatan.json");
|
||||
const kegiatanDesaJson = loadJsonData("desa/kegiatan-desa/kegiatan-desa.json");
|
||||
|
||||
export async function seedKegiatanDesa() {
|
||||
console.log("🔄 Seeding Kategori Kegiatan Desa...");
|
||||
|
||||
for (const k of kategoriKegiatanJson) {
|
||||
await prisma.kategoriKegiatan.upsert({
|
||||
where: { id: k.id },
|
||||
update: { nama: k.nama },
|
||||
create: { id: k.id, nama: k.nama },
|
||||
});
|
||||
console.log(` ✅ Kategori: ${k.nama}`);
|
||||
}
|
||||
|
||||
console.log("🔄 Seeding Kegiatan Desa...");
|
||||
|
||||
for (const item of kegiatanDesaJson) {
|
||||
await prisma.kegiatanDesa.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
judul: item.judul,
|
||||
deskripsiSingkat: item.deskripsiSingkat,
|
||||
deskripsiLengkap: item.deskripsiLengkap,
|
||||
tanggal: new Date(item.tanggal),
|
||||
lokasi: item.lokasi,
|
||||
partisipan: item.partisipan,
|
||||
kategoriKegiatanId: item.kategoriKegiatanId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
judul: item.judul,
|
||||
deskripsiSingkat: item.deskripsiSingkat,
|
||||
deskripsiLengkap: item.deskripsiLengkap,
|
||||
tanggal: new Date(item.tanggal),
|
||||
lokasi: item.lokasi,
|
||||
partisipan: item.partisipan,
|
||||
kategoriKegiatanId: item.kategoriKegiatanId,
|
||||
},
|
||||
});
|
||||
console.log(` ✅ Kegiatan: ${item.judul}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Kegiatan Desa seed selesai");
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export async function seedProgramKesehatan() {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
persentase: p.persentase ?? 0,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
@@ -35,6 +36,7 @@ export async function seedProgramKesehatan() {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
persentase: p.persentase ?? 0,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
502
prisma/_seeder_list/kesehatan/seed_balita.ts
Normal file
502
prisma/_seeder_list/kesehatan/seed_balita.ts
Normal 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");
|
||||
}
|
||||
222
prisma/_seeder_list/kesehatan/seed_ibu_hamil.ts
Normal file
222
prisma/_seeder_list/kesehatan/seed_ibu_hamil.ts
Normal 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");
|
||||
}
|
||||
15
prisma/_seeder_list/kesehatan/seed_ringkasan_kesehatan.ts
Normal file
15
prisma/_seeder_list/kesehatan/seed_ringkasan_kesehatan.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const SINGLETON_ID = "ringkasan_kesehatan_desa_001";
|
||||
|
||||
export async function seedRingkasanKesehatan() {
|
||||
console.log("🔄 Seeding Ringkasan Kesehatan Desa...");
|
||||
|
||||
await prisma.ringkasanKesehatanDesa.upsert({
|
||||
where: { id: SINGLETON_ID },
|
||||
update: { targetStuntingPct: 10 },
|
||||
create: { id: SINGLETON_ID, targetStuntingPct: 10 },
|
||||
});
|
||||
|
||||
console.log("✅ Ringkasan Kesehatan Desa seeded");
|
||||
}
|
||||
22
prisma/_seeder_list/pendidikan/seed_beasiswa_config.ts
Normal file
22
prisma/_seeder_list/pendidikan/seed_beasiswa_config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const SINGLETON_ID = "beasiswa_config_desa_001";
|
||||
|
||||
export async function seedBeasiswaConfig() {
|
||||
console.log("🔄 Seeding Beasiswa Config...");
|
||||
|
||||
await prisma.beasiswaConfig.upsert({
|
||||
where: { id: SINGLETON_ID },
|
||||
update: {
|
||||
tahunAjaran: "2025/2026",
|
||||
danaTersalurkan: BigInt(1200000000),
|
||||
},
|
||||
create: {
|
||||
id: SINGLETON_ID,
|
||||
tahunAjaran: "2025/2026",
|
||||
danaTersalurkan: BigInt(1200000000),
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ Beasiswa Config seeded");
|
||||
}
|
||||
58
prisma/data/desa/event-budaya/event-budaya.json
Normal file
58
prisma/data/desa/event-budaya/event-budaya.json
Normal 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."
|
||||
}
|
||||
]
|
||||
22
prisma/data/desa/kegiatan-desa/kategori-kegiatan.json
Normal file
22
prisma/data/desa/kegiatan-desa/kategori-kegiatan.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"id": "katbudaya000001",
|
||||
"nama": "Budaya"
|
||||
},
|
||||
{
|
||||
"id": "katsosia000001",
|
||||
"nama": "Sosial"
|
||||
},
|
||||
{
|
||||
"id": "katolahraga0001",
|
||||
"nama": "Olahraga"
|
||||
},
|
||||
{
|
||||
"id": "katkeagamaan01",
|
||||
"nama": "Keagamaan"
|
||||
},
|
||||
{
|
||||
"id": "katpemberday01",
|
||||
"nama": "Pemberdayaan Masyarakat"
|
||||
}
|
||||
]
|
||||
52
prisma/data/desa/kegiatan-desa/kegiatan-desa.json
Normal file
52
prisma/data/desa/kegiatan-desa/kegiatan-desa.json
Normal file
@@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567801",
|
||||
"judul": "Hari Kesaktian Pancasila",
|
||||
"deskripsiSingkat": "Peringatan Hari Kesaktian Pancasila di Balai Desa Darmasaba sebagai momentum menguatkan nilai-nilai Pancasila dalam kehidupan bermasyarakat.",
|
||||
"deskripsiLengkap": "Pemerintah Desa Darmasaba menyelenggarakan upacara peringatan Hari Kesaktian Pancasila yang dihadiri oleh perangkat desa, tokoh masyarakat, perwakilan pemuda, dan warga desa. Kegiatan ini sebagai bentuk penghormatan atas perjuangan bangsa dan komitmen untuk terus mengamalkan nilai-nilai Pancasila dalam kehidupan sehari-hari di lingkungan desa.",
|
||||
"tanggal": "2025-10-01T00:00:00.000Z",
|
||||
"lokasi": "Balai Desa",
|
||||
"partisipan": 250,
|
||||
"kategoriKegiatanId": "katbudaya000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567802",
|
||||
"judul": "Festival Budaya Desa",
|
||||
"deskripsiSingkat": "Festival tahunan menampilkan kesenian dan budaya lokal Bali sebagai upaya pelestarian warisan budaya Desa Darmasaba.",
|
||||
"deskripsiLengkap": "Festival Budaya Desa Darmasaba merupakan agenda tahunan yang menampilkan berbagai pertunjukan seni tradisional Bali seperti tari Kecak, Legong, Barong, serta pameran kerajinan tangan dan kuliner khas desa. Festival ini bertujuan untuk melestarikan warisan budaya leluhur, memperkenalkan kekayaan budaya kepada generasi muda, dan meningkatkan daya tarik wisata budaya Desa Darmasaba.",
|
||||
"tanggal": "2026-05-20T00:00:00.000Z",
|
||||
"lokasi": "Lapangan Desa",
|
||||
"partisipan": 500,
|
||||
"kategoriKegiatanId": "katbudaya000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567803",
|
||||
"judul": "Perayaan HUT Desa",
|
||||
"deskripsiSingkat": "Perayaan hari ulang tahun Desa Darmasaba dengan berbagai kegiatan seni budaya dan olahraga yang melibatkan seluruh lapisan masyarakat.",
|
||||
"deskripsiLengkap": "Hari Ulang Tahun Desa Darmasaba dirayakan dengan serangkaian kegiatan meriah meliputi upacara adat, pertunjukan seni budaya Bali, lomba olahraga tradisional, dan pameran produk unggulan desa. Perayaan ini menjadi ajang mempererat kebersamaan warga, mengenang sejarah desa, dan merayakan pencapaian pembangunan yang telah diraih bersama.",
|
||||
"tanggal": "2026-08-17T00:00:00.000Z",
|
||||
"lokasi": "Balai Desa",
|
||||
"partisipan": 600,
|
||||
"kategoriKegiatanId": "katbudaya000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567804",
|
||||
"judul": "Gotong Royong Pembersihan Desa",
|
||||
"deskripsiSingkat": "Kegiatan gotong royong rutin membersihkan lingkungan desa sebagai wujud kebersamaan dan kepedulian masyarakat terhadap kebersihan.",
|
||||
"deskripsiLengkap": "Kegiatan gotong royong pembersihan lingkungan desa dilaksanakan secara rutin setiap bulan dengan melibatkan seluruh warga, kader PKK, karang taruna, dan perangkat desa. Kegiatan ini mencakup pembersihan jalan desa, sungai, tempat ibadah, dan fasilitas umum sebagai bentuk nyata kepedulian masyarakat terhadap kebersihan dan keindahan lingkungan.",
|
||||
"tanggal": "2026-03-15T00:00:00.000Z",
|
||||
"lokasi": "Seluruh Wilayah Desa",
|
||||
"partisipan": 400,
|
||||
"kategoriKegiatanId": "katsosia000001"
|
||||
},
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567805",
|
||||
"judul": "Turnamen Olahraga Antar Banjar",
|
||||
"deskripsiSingkat": "Turnamen olahraga tahunan antar banjar untuk mempererat tali persaudaraan dan mendorong gaya hidup sehat masyarakat desa.",
|
||||
"deskripsiLengkap": "Turnamen olahraga antar banjar di Desa Darmasaba menampilkan berbagai cabang olahraga seperti bola voli, bulu tangkis, tenis meja, dan sepak bola. Kegiatan ini diikuti oleh perwakilan dari setiap banjar di desa dan bertujuan untuk menumbuhkan semangat sportivitas, mempererat silaturahmi antar warga, serta mendorong pola hidup sehat melalui olahraga.",
|
||||
"tanggal": "2026-06-10T00:00:00.000Z",
|
||||
"lokasi": "Lapangan Desa",
|
||||
"partisipan": 350,
|
||||
"kategoriKegiatanId": "katolahraga0001"
|
||||
}
|
||||
]
|
||||
@@ -3,8 +3,57 @@
|
||||
"id": "cmkanjnmx0006vntz1cn7owpb",
|
||||
"name": "Posyandu Pudak Amara",
|
||||
"nomor": "(0361) 8463263",
|
||||
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p><p>Kegiatan ini melibatkan kader posyandu serta didampingi pihak desa dan puskesmas setempat untuk meningkatkan pelayanan kesehatan ibu dan anak.</p>",
|
||||
"jadwalPelayanan": "<p>Setiap bulan pada satu hari tertentu (mis. minggu ke-2): 08:00 – 12:00 WITA (posyandu balita & ibu hamil)</p>",
|
||||
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
||||
"jadwalPelayanan": "Senin, 10 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"imageName": "TDQReg1lQ73s39crXW0ra-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_mawar_001",
|
||||
"name": "Posyandu Mawar",
|
||||
"nomor": "(0361) 8463264",
|
||||
"deskripsi": "<p>Posyandu Mawar melayani kesehatan ibu dan anak di wilayah Banjar Mawar, Desa Darmasaba, dengan fokus pada pemantauan tumbuh kembang balita dan kesehatan ibu hamil.</p>",
|
||||
"jadwalPelayanan": "Senin, 15 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_melati_001",
|
||||
"name": "Posyandu Melati",
|
||||
"nomor": "(0361) 8463265",
|
||||
"deskripsi": "<p>Posyandu Melati berperan aktif dalam pelayanan kesehatan dasar masyarakat di Banjar Melati, meliputi imunisasi, penimbangan balita, dan konsultasi gizi.</p>",
|
||||
"jadwalPelayanan": "Selasa, 16 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_dahlia_001",
|
||||
"name": "Posyandu Dahlia",
|
||||
"nomor": "(0361) 8463266",
|
||||
"deskripsi": "<p>Posyandu Dahlia aktif melayani masyarakat Banjar Dahlia dengan program unggulan pemantauan stunting dan pemberian makanan tambahan bagi balita berisiko.</p>",
|
||||
"jadwalPelayanan": "Rabu, 17 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_anggrek_001",
|
||||
"name": "Posyandu Anggrek",
|
||||
"nomor": "(0361) 8463267",
|
||||
"deskripsi": "<p>Posyandu Anggrek melayani ibu hamil, ibu menyusui, dan balita di wilayah Banjar Anggrek dengan dukungan tenaga kesehatan dari Puskesmas Abiansemal 3.</p>",
|
||||
"jadwalPelayanan": "Kamis, 18 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_kamboja_001",
|
||||
"name": "Posyandu Kamboja",
|
||||
"nomor": "(0361) 8463268",
|
||||
"deskripsi": "<p>Posyandu Kamboja hadir untuk mendukung kesehatan masyarakat Banjar Kamboja melalui layanan pemeriksaan rutin, imunisasi lengkap, dan edukasi gizi keluarga.</p>",
|
||||
"jadwalPelayanan": "Jumat, 19 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_melur_001",
|
||||
"name": "Posyandu Melur",
|
||||
"nomor": "(0361) 8463269",
|
||||
"deskripsi": "<p>Posyandu Melur aktif memberikan layanan kesehatan preventif bagi ibu dan anak di Banjar Melur, termasuk deteksi dini stunting dan pemantauan gizi balita.</p>",
|
||||
"jadwalPelayanan": "Sabtu, 20 Feb 2026, 08:00 - 11:00 WITA"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_kenanga_001",
|
||||
"name": "Posyandu Kenanga",
|
||||
"nomor": "(0361) 8463270",
|
||||
"deskripsi": "<p>Posyandu Kenanga melayani masyarakat Banjar Kenanga dengan program kesehatan ibu dan anak, pemberian vitamin A, dan konseling laktasi bagi ibu menyusui.</p>",
|
||||
"jadwalPelayanan": "Senin, 23 Feb 2026, 08:00 - 11:00 WITA"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": "prog_kes_imunisasi_001",
|
||||
"name": "Imunisasi Lengkap",
|
||||
"deskripsiSingkat": "<p>Persentase balita yang mendapatkan imunisasi lengkap sesuai jadwal di Desa Darmasaba.</p>",
|
||||
"deskripsi": "<p>Program imunisasi lengkap mencakup vaksin BCG, DPT-HB-Hib, Polio, Campak, dan PCV yang diberikan kepada seluruh balita di Desa Darmasaba melalui posyandu dan puskesmas setempat untuk membangun kekebalan komunitas.</p>",
|
||||
"persentase": 92
|
||||
},
|
||||
{
|
||||
"id": "prog_kes_pemeriksaan_001",
|
||||
"name": "Pemeriksaan Rutin",
|
||||
"deskripsiSingkat": "<p>Persentase ibu hamil dan balita yang melakukan pemeriksaan kesehatan rutin secara berkala.</p>",
|
||||
"deskripsi": "<p>Pemeriksaan kesehatan rutin dilakukan setiap bulan di posyandu dan puskesmas, mencakup penimbangan berat badan, pengukuran tinggi badan, pemeriksaan tekanan darah ibu hamil, dan konsultasi gizi untuk memantau perkembangan kesehatan masyarakat.</p>",
|
||||
"persentase": 88
|
||||
},
|
||||
{
|
||||
"id": "prog_kes_gizi_001",
|
||||
"name": "Gizi Baik",
|
||||
"deskripsiSingkat": "<p>Persentase balita dengan status gizi baik berdasarkan hasil pemantauan tumbuh kembang.</p>",
|
||||
"deskripsi": "<p>Program pemantauan gizi balita dilaksanakan melalui posyandu dengan penimbangan bulanan dan pengukuran tinggi badan. Balita dengan status gizi baik menunjukkan berat dan tinggi badan sesuai standar WHO, didukung dengan program pemberian makanan tambahan bagi balita berisiko.</p>",
|
||||
"persentase": 86
|
||||
},
|
||||
{
|
||||
"id": "prog_kes_stunting_001",
|
||||
"name": "Target Stunting",
|
||||
"deskripsiSingkat": "<p>Persentase balita yang teridentifikasi berisiko stunting dan perlu penanganan khusus.</p>",
|
||||
"deskripsi": "<p>Penanganan stunting di Desa Darmasaba dilakukan melalui deteksi dini, intervensi gizi spesifik, dan intervensi gizi sensitif. Program ini melibatkan kader posyandu, bidan desa, dan puskesmas untuk memantau pertumbuhan balita secara berkala dan memberikan penanganan tepat.</p>",
|
||||
"persentase": 14
|
||||
},
|
||||
{
|
||||
"id": "cmkawkji50002vn6yzyrlqhh1",
|
||||
"name": "Gerakan Kulkul PKK dan Posyandu Desa Darmasaba",
|
||||
"deskripsiSingkat": "<p>Kegiatan bersama PKK dan Posyandu untuk meningkatkan pelayanan kesehatan masyarakat.</p>",
|
||||
"deskripsi": "<p>Pada hari Minggu, 11 Januari 2025, Pemerintah Desa Darmasaba melalui TP PKK dan TP Posyandu melaksanakan kegiatan Gerakan Kulkul PKK dan Posyandu yang berlangsung serentak di seluruh wilayah Desa Darmasaba untuk memperkuat pelayanan kesehatan dasar dan peningkatan partisipasi masyarakat dalam program Posyandu.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "hLeF0GRFZqDUngZnDMAAk-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -11,6 +40,7 @@
|
||||
"name": "Pendampingan Kunjungan Rumah oleh Puskesmas Abiansemal 3",
|
||||
"deskripsiSingkat": "<p>Pendataan kesehatan penyandang disabilitas lewat kunjungan rumah di Desa Darmasaba.</p>",
|
||||
"deskripsi": "<p>Pemerintah Desa Darmasaba bersama Kelian Banjar Dinas dan kader kesehatan mendampingi kegiatan kunjungan rumah yang dilaksanakan oleh Puskesmas Abiansemal 3 pada 21 Juli 2025, difokuskan pada pendataan dan pemantauan kondisi kesehatan penyandang disabilitas di Banjar Bersih, Desa Darmasaba.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "hyyTFi8EApjzFEZ9EvJgB-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -18,6 +48,7 @@
|
||||
"name": "Kegiatan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali di Desa Darmasaba",
|
||||
"deskripsiSingkat": "<p>Aksi sosial TP Posyandu Bali untuk memperkuat pelayanan posyandu di desa.</p>",
|
||||
"deskripsi": "<p>Pada 10 Desember 2025, Desa Darmasaba menjadi lokasi pelaksanaan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali yang bertujuan memperkuat pelayanan Posyandu serta meningkatkan kesejahteraan masyarakat, khususnya keluarga dan balita.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "l4qsUEw2JiclGAkkrXp9g-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -25,6 +56,7 @@
|
||||
"name": "Inovasi BAJRA dalam Penanggulangan Rabies",
|
||||
"deskripsiSingkat": "<p>Program BAJRA untuk penanggulangan rabies di Desa Darmasaba.</p>",
|
||||
"deskripsi": "<p>Desa Darmasaba mengembangkan inovasi BAJRA (Bersama Jaga Rabies), sebuah program berbasis komunitas untuk penanggulangan rabies yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan dan koordinasi lintas sektor antara kesehatan hewan, manusia, dan pemerintahan desa.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "Gc79mlIlGuoRQuTqskFj--mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -32,6 +64,7 @@
|
||||
"name": "Posyandu Pudak Amara Berkompetisi",
|
||||
"deskripsiSingkat": "<p>Partisipasi Posyandu Pudak Amara dalam lomba prestasi Posyandu tingkat provinsi.</p>",
|
||||
"deskripsi": "<p>Kader Posyandu Pudak Amara Br. Cabe mendapat pendampingan dari Perbekel Darmasaba, Dinas Kesehatan Kab. Badung, Puskesmas Abiansemal III, dan Pustu Desa Darmasaba dalam ajang lomba kader dan Posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "OsMY3AYPyGC_CoN1xUjOn-mobile.webp"
|
||||
},
|
||||
{
|
||||
@@ -39,13 +72,15 @@
|
||||
"name": "Outbound Kader Posyandu Darmasaba",
|
||||
"deskripsiSingkat": "<p>Program pembinaan dan pengembangan kapasitas kader Posyandu.</p>",
|
||||
"deskripsi": "<p>Pemdes Darmasaba melaksanakan kegiatan Outbound Posyandu untuk meningkatkan kapasitas dan wawasan Kader Posyandu se-Desa Darmasaba sebagai bagian dari upaya peningkatan kualitas pelayanan kesehatan dasar di masyarakat.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "M9QlgVKIEfCdY3g4F_tRZ-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmkdu8ki10004vn4lpbxm2zqo",
|
||||
"name": "PEMBANGUNAN JAMBAN BAGI MASYARAKAT",
|
||||
"deskripsiSingkat": "<p>Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p>",
|
||||
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat. Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p><p style=\"text-align: justify\">Pemberian bantuan jamban ini dilaksanakan di 11 banjar dengan menyasar 22 keluarga yang memang belum memiliki jamban yang sumber dananya sepenuhnya dari APBDes Darmasaba T. A. 2023. Pembangunan Jamban bagi Masyarakat ini juga menjadi bukti komitmen Pemerintah Desa Darmasaba dalam melaksanakan salah satu visi mewujudkan masyarakat yang sejahtera dan berbudaya untuk menjaga lingkungan yang bersih dan sehat.</p>",
|
||||
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat.</p>",
|
||||
"persentase": 0,
|
||||
"imageName": "6DQbAvn0St-xHdPGW3vpY-mobile.webp"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Add persentase field to ProgramKesehatan (untuk Statistik Kesehatan bar chart)
|
||||
ALTER TABLE "ProgramKesehatan" ADD COLUMN "persentase" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Create BeasiswaConfig (untuk dana tersalurkan + tahun ajaran beasiswa desa)
|
||||
CREATE TABLE "BeasiswaConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tahunAjaran" TEXT NOT NULL,
|
||||
"danaTersalurkan" BIGINT NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "BeasiswaConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Create RingkasanKesehatanDesa (untuk stat cards: ibu hamil, balita, stunting)
|
||||
CREATE TABLE "RingkasanKesehatanDesa" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ibuHamilAkh" INTEGER NOT NULL DEFAULT 0,
|
||||
"balitaTerdaftar" INTEGER NOT NULL DEFAULT 0,
|
||||
"alertStunting" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "RingkasanKesehatanDesa_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "RingkasanKesehatanDesa"
|
||||
ADD COLUMN "imunisasiLengkapPct" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "pemeriksaanRutinPct" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "giziBaikPct" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "targetStuntingPct" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,59 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IbuHamilStatus" AS ENUM ('AKTIF', 'MELAHIRKAN', 'KEGUGURAN', 'NONAKTIF');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JenisKelaminBalita" AS ENUM ('L', 'P');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StatusStunting" AS ENUM ('NORMAL', 'ALERT', 'STUNTING');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IbuHamil" (
|
||||
"id" TEXT NOT NULL,
|
||||
"nama" TEXT NOT NULL,
|
||||
"nik" TEXT,
|
||||
"usiaKehamilan" INTEGER NOT NULL DEFAULT 0,
|
||||
"hpht" TIMESTAMP(3),
|
||||
"taksiranLahir" TIMESTAMP(3),
|
||||
"alamat" TEXT,
|
||||
"noHp" TEXT,
|
||||
"catatan" TEXT,
|
||||
"posyanduId" TEXT,
|
||||
"status" "IbuHamilStatus" NOT NULL DEFAULT 'AKTIF',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "IbuHamil_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Balita" (
|
||||
"id" TEXT NOT NULL,
|
||||
"nama" TEXT NOT NULL,
|
||||
"nik" TEXT,
|
||||
"tanggalLahir" TIMESTAMP(3) NOT NULL,
|
||||
"jenisKelamin" "JenisKelaminBalita" NOT NULL,
|
||||
"beratBadanKg" DOUBLE PRECISION,
|
||||
"tinggiBadanCm" DOUBLE PRECISION,
|
||||
"namaOrtu" TEXT,
|
||||
"alamat" TEXT,
|
||||
"noHpOrtu" TEXT,
|
||||
"posyanduId" TEXT,
|
||||
"imunisasiLengkap" BOOLEAN NOT NULL DEFAULT false,
|
||||
"giziBaik" BOOLEAN NOT NULL DEFAULT true,
|
||||
"pemeriksaanRutin" BOOLEAN NOT NULL DEFAULT true,
|
||||
"statusStunting" "StatusStunting" NOT NULL DEFAULT 'NORMAL',
|
||||
"catatan" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "Balita_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IbuHamil" ADD CONSTRAINT "IbuHamil_posyanduId_fkey" FOREIGN KEY ("posyanduId") REFERENCES "Posyandu"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Balita" ADD CONSTRAINT "Balita_posyanduId_fkey" FOREIGN KEY ("posyanduId") REFERENCES "Posyandu"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
@@ -1159,6 +1159,8 @@ model Posyandu {
|
||||
jadwalPelayanan String
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
ibuHamil IbuHamil[]
|
||||
balita Balita[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
@@ -1213,6 +1215,7 @@ model ProgramKesehatan {
|
||||
name String
|
||||
deskripsiSingkat String
|
||||
deskripsi String
|
||||
persentase Int @default(0)
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -2470,3 +2473,95 @@ model PenjualanProduk {
|
||||
@@index([tanggal])
|
||||
}
|
||||
|
||||
// ========================================= BEASISWA CONFIG ========================================= //
|
||||
model BeasiswaConfig {
|
||||
id String @id @default(cuid())
|
||||
tahunAjaran String
|
||||
danaTersalurkan BigInt @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= IBU HAMIL ========================================= //
|
||||
enum IbuHamilStatus {
|
||||
AKTIF
|
||||
MELAHIRKAN
|
||||
KEGUGURAN
|
||||
NONAKTIF
|
||||
}
|
||||
|
||||
model IbuHamil {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
nik String?
|
||||
usiaKehamilan Int @default(0)
|
||||
hpht DateTime?
|
||||
taksiranLahir DateTime?
|
||||
alamat String?
|
||||
noHp String?
|
||||
catatan String?
|
||||
posyanduId String?
|
||||
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
||||
status IbuHamilStatus @default(AKTIF)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= BALITA ========================================= //
|
||||
enum JenisKelaminBalita {
|
||||
L
|
||||
P
|
||||
}
|
||||
|
||||
enum StatusStunting {
|
||||
NORMAL
|
||||
ALERT
|
||||
STUNTING
|
||||
}
|
||||
|
||||
model Balita {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
nik String?
|
||||
tanggalLahir DateTime
|
||||
jenisKelamin JenisKelaminBalita
|
||||
beratBadanKg Float?
|
||||
tinggiBadanCm Float?
|
||||
namaOrtu String?
|
||||
alamat String?
|
||||
noHpOrtu String?
|
||||
posyanduId String?
|
||||
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
||||
imunisasiLengkap Boolean @default(false)
|
||||
giziBaik Boolean @default(true)
|
||||
pemeriksaanRutin Boolean @default(true)
|
||||
statusStunting StatusStunting @default(NORMAL)
|
||||
catatan String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
|
||||
model RingkasanKesehatanDesa {
|
||||
id String @id @default(cuid())
|
||||
targetStuntingPct Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= EVENT BUDAYA ========================================= //
|
||||
model EventBudaya {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
tanggal DateTime
|
||||
lokasi String
|
||||
deskripsi String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
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";
|
||||
@@ -46,6 +48,9 @@ import { seedProgramKesehatan } from "./_seeder_list/kesehatan/program-kesehatan
|
||||
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";
|
||||
@@ -59,6 +64,7 @@ import { seedKonservasiAdatBali } from "./_seeder_list/lingkungan/seed_konservas
|
||||
import { seedPengelolaanSampah } from "./_seeder_list/lingkungan/seed_pengelolaan_sampah";
|
||||
import { seedProgramPenghijauan } from "./_seeder_list/lingkungan/seed_program_penghijauan";
|
||||
import { seedBeasiswaPendaftar } from "./_seeder_list/pendidikan/seed_beasiswa_pendaftar";
|
||||
import { seedBeasiswaConfig } from "./_seeder_list/pendidikan/seed_beasiswa_config";
|
||||
import { seedBimbinganBelajar } from "./_seeder_list/pendidikan/seed_bimbingan_belajar";
|
||||
import { seedDataPendidikan } from "./_seeder_list/pendidikan/seed_data_pendidikan";
|
||||
import { seedDataPerpustakaan } from "./_seeder_list/pendidikan/seed_data_perpustakaan";
|
||||
@@ -241,6 +247,10 @@ import seedAssets from "./seed_assets";
|
||||
// // ==================== SUBMENU POSYANDU =========================
|
||||
await seedPosyandu();
|
||||
|
||||
// // ==================== SUBMENU IBU HAMIL + BALITA =========================
|
||||
await seedIbuHamil();
|
||||
await seedBalita();
|
||||
|
||||
// // ==================== SUBMENU PUSKESMAS =========================
|
||||
await seedPuskesmas();
|
||||
|
||||
@@ -378,6 +388,12 @@ import seedAssets from "./seed_assets";
|
||||
// ===== PENDIDIKAN =====
|
||||
await seedKeunggulanProgram();
|
||||
await seedBeasiswaPendaftar();
|
||||
await seedBeasiswaConfig();
|
||||
|
||||
// ===== SOSIAL DASHBOARD =====
|
||||
await seedRingkasanKesehatan();
|
||||
await seedKegiatanDesa();
|
||||
await seedEventBudaya();
|
||||
|
||||
// ===== DESA =====
|
||||
await seedMusikDesa();
|
||||
|
||||
211
src/app/admin/(dashboard)/_state/desa/eventBudaya.ts
Normal file
211
src/app/admin/(dashboard)/_state/desa/eventBudaya.ts
Normal 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;
|
||||
409
src/app/admin/(dashboard)/_state/desa/kegiatanDesa.ts
Normal file
409
src/app/admin/(dashboard)/_state/desa/kegiatanDesa.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
// ========================================= SCHEMAS ========================================= //
|
||||
|
||||
const templateForm = z.object({
|
||||
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||
deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"),
|
||||
deskripsiLengkap: z.string().min(3, "Deskripsi lengkap minimal 3 karakter"),
|
||||
tanggal: z.string().nonempty("Tanggal harus diisi"),
|
||||
lokasi: z.string().min(3, "Lokasi minimal 3 karakter"),
|
||||
partisipan: z.number().optional().default(0),
|
||||
kategoriKegiatanId: z.string().nonempty("Kategori kegiatan harus dipilih"),
|
||||
imageId: z.string().optional(),
|
||||
});
|
||||
|
||||
const defaultForm = {
|
||||
judul: "",
|
||||
deskripsiSingkat: "",
|
||||
deskripsiLengkap: "",
|
||||
tanggal: "",
|
||||
lokasi: "",
|
||||
partisipan: 0,
|
||||
kategoriKegiatanId: "",
|
||||
imageId: "" as string | undefined,
|
||||
};
|
||||
|
||||
const templateKategori = z.object({
|
||||
nama: z.string().min(1, "Nama kategori harus diisi"),
|
||||
});
|
||||
|
||||
const defaultKategori = {
|
||||
nama: "",
|
||||
};
|
||||
|
||||
// ========================================= KEGIATAN DESA ========================================= //
|
||||
|
||||
const kegiatan = proxy({
|
||||
create: {
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(kegiatan.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
kegiatan.create.loading = true;
|
||||
const res = await ApiFetch.api.desa["kegiatandesa"]["create"].post(
|
||||
kegiatan.create.form
|
||||
);
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kegiatan.findMany.load();
|
||||
toast.success("Kegiatan desa berhasil disimpan!");
|
||||
return true;
|
||||
}
|
||||
|
||||
toast.error(res.data?.message || "Gagal menyimpan kegiatan desa");
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error creating kegiatan:", error);
|
||||
toast.error("Terjadi kesalahan saat menyimpan kegiatan");
|
||||
return false;
|
||||
} finally {
|
||||
kegiatan.create.loading = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
kegiatan.create.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
const startTime = Date.now();
|
||||
kegiatan.findMany.loading = true;
|
||||
kegiatan.findMany.page = page;
|
||||
kegiatan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res = await ApiFetch.api.desa["kegiatandesa"]["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kegiatan.findMany.data = res.data.data ?? [];
|
||||
kegiatan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
kegiatan.findMany.data = [];
|
||||
kegiatan.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch kegiatan paginated:", err);
|
||||
kegiatan.findMany.data = [];
|
||||
kegiatan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const minDelay = 300;
|
||||
const delay = elapsed < minDelay ? minDelay - elapsed : 0;
|
||||
|
||||
setTimeout(() => {
|
||||
kegiatan.findMany.loading = false;
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
data: null as any | null,
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
if (!id) return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/desa/kegiatandesa/${id}`); // Assuming unique endpoint follows standard
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
kegiatan.findUnique.data = result.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching unique kegiatan:", error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
kegiatan.delete.loading = true;
|
||||
const response = await fetch(`/api/desa/kegiatandesa/del/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Kegiatan berhasil dihapus");
|
||||
await kegiatan.findMany.load();
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus kegiatan");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus kegiatan");
|
||||
} finally {
|
||||
kegiatan.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) return null;
|
||||
try {
|
||||
kegiatan.edit.loading = true;
|
||||
const response = await fetch(`/api/desa/kegiatandesa/${id}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
judul: data.judul,
|
||||
deskripsiSingkat: data.deskripsiSingkat,
|
||||
deskripsiLengkap: data.deskripsiLengkap,
|
||||
tanggal: data.tanggal ? new Date(data.tanggal).toISOString().split('T')[0] : "",
|
||||
lokasi: data.lokasi,
|
||||
partisipan: data.partisipan || 0,
|
||||
kategoriKegiatanId: data.kategoriKegiatanId || "",
|
||||
imageId: data.imageId || undefined,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading kegiatan for edit:", error);
|
||||
} finally {
|
||||
kegiatan.edit.loading = false;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async update() {
|
||||
const cek = templateForm.safeParse(kegiatan.edit.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
kegiatan.edit.loading = true;
|
||||
const response = await fetch(`/api/desa/kegiatandesa/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(kegiatan.edit.form),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update kegiatan");
|
||||
await kegiatan.findMany.load();
|
||||
return true;
|
||||
}
|
||||
throw new Error(result.message || "Gagal update kegiatan");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update");
|
||||
return false;
|
||||
} finally {
|
||||
kegiatan.edit.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
kegiatan.edit.id = "";
|
||||
kegiatan.edit.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================= KATEGORI KEGIATAN ========================================= //
|
||||
|
||||
const kategoriKegiatan = proxy({
|
||||
create: {
|
||||
form: { ...defaultKategori },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateKategori.safeParse(kategoriKegiatan.create.form);
|
||||
if (!cek.success) {
|
||||
return toast.error("Nama kategori harus diisi");
|
||||
}
|
||||
|
||||
try {
|
||||
kategoriKegiatan.create.loading = true;
|
||||
const res = await ApiFetch.api.desa["kategorikegiatan"]["create"].post(
|
||||
kategoriKegiatan.create.form
|
||||
);
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kategoriKegiatan.findMany.load();
|
||||
toast.success("Kategori kegiatan berhasil dibuat");
|
||||
return true;
|
||||
}
|
||||
toast.error(res.data?.message || "Gagal membuat kategori");
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Terjadi kesalahan");
|
||||
return false;
|
||||
} finally {
|
||||
kategoriKegiatan.create.loading = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
kategoriKegiatan.create.form = { ...defaultKategori };
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: [] as any[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
kategoriKegiatan.findMany.loading = true;
|
||||
kategoriKegiatan.findMany.page = page;
|
||||
kategoriKegiatan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.desa["kategorikegiatan"]["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kategoriKegiatan.findMany.data = res.data.data ?? [];
|
||||
kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
kategoriKegiatan.findMany.data = [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch kategori kegiatan:", err);
|
||||
} finally {
|
||||
kategoriKegiatan.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findUnique: {
|
||||
data: null as any | null,
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/desa/kategorikegiatan/${id}`);
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
kategoriKegiatan.findUnique.data = result.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return;
|
||||
try {
|
||||
kategoriKegiatan.delete.loading = true;
|
||||
const res = await fetch(`/api/desa/kategorikegiatan/del/${id}`, { method: "DELETE" });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
kategoriKegiatan.findMany.load();
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
kategoriKegiatan.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...defaultKategori },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
if (!id) return;
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`/api/desa/kategorikegiatan/${id}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
this.id = result.data.id;
|
||||
this.form = { nama: result.data.nama };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = templateKategori.safeParse(kategoriKegiatan.update.form);
|
||||
if (!cek.success) return toast.error("Nama kategori harus diisi");
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await fetch(`/api/desa/kategorikegiatan/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update kategori");
|
||||
kategoriKegiatan.findMany.load();
|
||||
return true;
|
||||
}
|
||||
toast.error(result.message);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
reset() {
|
||||
this.id = "";
|
||||
this.form = { ...defaultKategori };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================= GLOBAL STATE ========================================= //
|
||||
|
||||
const stateDashboardKegiatan = proxy({
|
||||
kegiatan,
|
||||
kategoriKegiatan,
|
||||
});
|
||||
|
||||
export default stateDashboardKegiatan;
|
||||
153
src/app/admin/(dashboard)/_state/desa/profile/lambangDesa.ts
Normal file
153
src/app/admin/(dashboard)/_state/desa/profile/lambangDesa.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const lambangDesaForm = z.object({
|
||||
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||
});
|
||||
|
||||
const lambangDesaDefaultForm = {
|
||||
judul: "",
|
||||
deskripsi: "",
|
||||
};
|
||||
|
||||
type LambangDesaForm = Prisma.LambangDesaGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
judul: true;
|
||||
deskripsi: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
const lambangDesa = proxy({
|
||||
findUnique: {
|
||||
data: null as LambangDesaForm | null,
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/lambang/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.data = result.data;
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengambil data lambang desa"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
console.error("Load lambang desa error:", msg);
|
||||
toast.error("Terjadi kesalahan saat mengambil data lambang desa");
|
||||
return null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...lambangDesaDefaultForm },
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
isReadOnly: false,
|
||||
|
||||
initialize(lambangDesaData: LambangDesaForm) {
|
||||
this.id = lambangDesaData.id;
|
||||
this.isReadOnly = false;
|
||||
this.form = {
|
||||
judul: lambangDesaData.judul || "",
|
||||
deskripsi: lambangDesaData.deskripsi || "",
|
||||
};
|
||||
},
|
||||
|
||||
updateField(field: keyof typeof lambangDesaDefaultForm, value: string) {
|
||||
this.form[field] = value;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const validation = lambangDesaForm.safeParse(this.form);
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues
|
||||
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||
.join(", ");
|
||||
toast.error(`Form tidak valid: ${errors}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/lambang/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update lambang desa");
|
||||
await lambangDesa.findUnique.load(this.id);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update lambang desa");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
this.error = errorMessage;
|
||||
console.error("Update lambang desa error:", errorMessage);
|
||||
toast.error("Terjadi kesalahan saat update lambang desa");
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.id = "";
|
||||
this.form = { ...lambangDesaDefaultForm };
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
this.isReadOnly = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default lambangDesa;
|
||||
241
src/app/admin/(dashboard)/_state/desa/profile/mantanPerbekel.ts
Normal file
241
src/app/admin/(dashboard)/_state/desa/profile/mantanPerbekel.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
|
||||
const mantanPerbekelForm = z.object({
|
||||
nama: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
daerah: z.string().min(3, "Daerah minimal 3 karakter"),
|
||||
periode: z.string().min(3, "Periode minimal 3 karakter"),
|
||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||
});
|
||||
|
||||
const mantanPerbekelDefaultForm = {
|
||||
nama: "",
|
||||
daerah: "",
|
||||
periode: "",
|
||||
imageId: "",
|
||||
};
|
||||
|
||||
const mantanPerbekel = proxy({
|
||||
create: {
|
||||
form: { ...mantanPerbekelDefaultForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
mantanPerbekel.create.loading = true;
|
||||
const res = await ApiFetch.api.desa.mantanperbekel["create"].post(
|
||||
mantanPerbekel.create.form
|
||||
);
|
||||
if (res.status === 200) {
|
||||
mantanPerbekel.findMany.load();
|
||||
return toast.success("Foto berhasil disimpan!");
|
||||
}
|
||||
return toast.error("Gagal menyimpan foto");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
} finally {
|
||||
mantanPerbekel.create.loading = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
mantanPerbekel.create.form = { ...mantanPerbekelDefaultForm };
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.PerbekelDariMasaKeMasaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
mantanPerbekel.findMany.loading = true;
|
||||
mantanPerbekel.findMany.page = page;
|
||||
mantanPerbekel.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.desa.mantanperbekel["findMany"].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
mantanPerbekel.findMany.data = res.data.data ?? [];
|
||||
mantanPerbekel.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
mantanPerbekel.findMany.data = [];
|
||||
mantanPerbekel.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch mantan perbekel paginated:", err);
|
||||
mantanPerbekel.findMany.data = [];
|
||||
mantanPerbekel.findMany.totalPages = 1;
|
||||
} finally {
|
||||
mantanPerbekel.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.PerbekelDariMasaKeMasaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/desa/mantanperbekel/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
mantanPerbekel.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch mantan perbekel:", res.statusText);
|
||||
mantanPerbekel.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching mantan perbekel:", error);
|
||||
mantanPerbekel.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
try {
|
||||
mantanPerbekel.delete.loading = true;
|
||||
const response = await fetch(`/api/desa/mantanperbekel/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
toast.success(result.message || "Mantan perbekel berhasil dihapus");
|
||||
await mantanPerbekel.findMany.load();
|
||||
} else {
|
||||
toast.error(result.message || "Gagal menghapus mantan perbekel");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus mantan perbekel");
|
||||
} finally {
|
||||
mantanPerbekel.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...mantanPerbekelDefaultForm },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/api/desa/mantanperbekel/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
nama: data.nama,
|
||||
daerah: data.daerah,
|
||||
periode: data.periode,
|
||||
imageId: data.imageId || "",
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading foto:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.update.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
mantanPerbekel.update.loading = true;
|
||||
const response = await fetch(`/api/desa/mantanperbekel/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nama: this.form.nama,
|
||||
daerah: this.form.daerah,
|
||||
periode: this.form.periode,
|
||||
imageId: this.form.imageId,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success(result.message || "Mantan perbekel berhasil diupdate");
|
||||
await mantanPerbekel.findMany.load();
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal mengupdate mantan perbekel");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating mantan perbekel:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Gagal mengupdate mantan perbekel"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
mantanPerbekel.update.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
mantanPerbekel.update.id = "";
|
||||
mantanPerbekel.update.form = { ...mantanPerbekelDefaultForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default mantanPerbekel;
|
||||
183
src/app/admin/(dashboard)/_state/desa/profile/maskotDesa.ts
Normal file
183
src/app/admin/(dashboard)/_state/desa/profile/maskotDesa.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const maskotForm = z.object({
|
||||
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||
images: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().min(1, "Label wajib"),
|
||||
imageId: z.string().min(1, "Image ID wajib"),
|
||||
})
|
||||
)
|
||||
.min(1, "Minimal 1 gambar harus diisi"),
|
||||
});
|
||||
|
||||
const maskotDefaultForm = {
|
||||
judul: "",
|
||||
deskripsi: "",
|
||||
images: [] as { label: string; imageId: string }[],
|
||||
};
|
||||
|
||||
type FormData = typeof maskotDefaultForm;
|
||||
|
||||
type MaskotDesaForm = Prisma.MaskotDesaGetPayload<{
|
||||
include: {
|
||||
images: {
|
||||
include: {
|
||||
image: {
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
path: true;
|
||||
link: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
const maskotDesa = proxy({
|
||||
findUnique: {
|
||||
data: null as MaskotDesaForm | null,
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/maskot/${id}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
this.data = result.data;
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal mengambil data profile");
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
console.error("Load profile error:", msg);
|
||||
toast.error("Terjadi kesalahan saat mengambil data profile");
|
||||
return null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...maskotDefaultForm },
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
isReadOnly: false,
|
||||
|
||||
initialize(profileData: MaskotDesaForm) {
|
||||
this.id = profileData.id;
|
||||
this.isReadOnly = false;
|
||||
this.form = {
|
||||
judul: profileData.judul || "",
|
||||
deskripsi: profileData.deskripsi || "",
|
||||
images: (profileData.images || []).map((img) => ({
|
||||
label: img.label,
|
||||
imageId: img?.image?.id || "",
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
updateField<K extends keyof FormData>(field: K, value: FormData[K]) {
|
||||
this.form[field] = value;
|
||||
},
|
||||
|
||||
addImage() {
|
||||
this.form.images.push({ label: "", imageId: "" });
|
||||
},
|
||||
|
||||
removeImage(index: number) {
|
||||
this.form.images.splice(index, 1);
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const validation = maskotForm.safeParse(this.form);
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues
|
||||
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||
.join(", ");
|
||||
toast.error(`Form tidak valid: ${errors}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/maskot/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
toast.success("Berhasil update profile");
|
||||
await maskotDesa.findUnique.load(this.id);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update profile");
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
toast.error("Terjadi kesalahan saat update profile");
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.id = "";
|
||||
this.form = { ...maskotDefaultForm };
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
this.isReadOnly = false;
|
||||
},
|
||||
},
|
||||
|
||||
async loadForEdit(id: string) {
|
||||
const data = await this.findUnique.load(id);
|
||||
if (data) {
|
||||
this.update.initialize(data);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.findUnique.reset();
|
||||
this.update.reset();
|
||||
},
|
||||
});
|
||||
|
||||
export default maskotDesa;
|
||||
185
src/app/admin/(dashboard)/_state/desa/profile/profilPerbekel.ts
Normal file
185
src/app/admin/(dashboard)/_state/desa/profile/profilPerbekel.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const profilPerbekelForm = z.object({
|
||||
biodata: z.string().min(3, "Biodata minimal 3 karakter"),
|
||||
pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"),
|
||||
pengalamanOrganisasi: z
|
||||
.string()
|
||||
.min(3, "Pengalaman Organisasi minimal 3 karakter"),
|
||||
programUnggulan: z.string().min(3, "Program Unggulan minimal 3 karakter"),
|
||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||
});
|
||||
|
||||
const profilPerbekelDefaultForm = {
|
||||
biodata: "",
|
||||
pengalaman: "",
|
||||
pengalamanOrganisasi: "",
|
||||
programUnggulan: "",
|
||||
imageId: "",
|
||||
};
|
||||
|
||||
type ProfilPerbekelForm = Prisma.ProfilPerbekelGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
biodata: true;
|
||||
pengalaman: true;
|
||||
pengalamanOrganisasi: true;
|
||||
programUnggulan: true;
|
||||
imageId: true;
|
||||
image?: {
|
||||
select: {
|
||||
link: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
const profilPerbekel = proxy({
|
||||
findUnique: {
|
||||
data: null as ProfilPerbekelForm | null,
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/profileperbekel/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.data = result.data;
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengambil data profil perbekel"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
toast.error("Terjadi kesalahan saat mengambil data profil perbekel");
|
||||
return null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...profilPerbekelDefaultForm },
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
isReadOnly: false,
|
||||
|
||||
initialize(profilData: ProfilPerbekelForm) {
|
||||
this.id = profilData.id;
|
||||
this.isReadOnly = false;
|
||||
this.form = {
|
||||
biodata: profilData.biodata || "",
|
||||
pengalaman: profilData.pengalaman || "",
|
||||
pengalamanOrganisasi: profilData.pengalamanOrganisasi || "",
|
||||
programUnggulan: profilData.programUnggulan || "",
|
||||
imageId: profilData.imageId || "",
|
||||
};
|
||||
},
|
||||
|
||||
updateField(field: keyof typeof profilPerbekelDefaultForm, value: string) {
|
||||
this.form[field] = value;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const validation = profilPerbekelForm.safeParse(this.form);
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues
|
||||
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||
.join(", ");
|
||||
toast.error(`Form tidak valid: ${errors}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/desa/profile/profileperbekel/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update profil perbekel");
|
||||
await profilPerbekel.findUnique.load(this.id);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update profil perbekel");
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
toast.error("Terjadi kesalahan saat update profil perbekel");
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.id = "";
|
||||
this.form = { ...profilPerbekelDefaultForm };
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
this.isReadOnly = false;
|
||||
},
|
||||
},
|
||||
|
||||
async loadForEdit(id: string) {
|
||||
const profileData = await this.findUnique.load(id);
|
||||
if (profileData) {
|
||||
this.edit.initialize(profileData);
|
||||
}
|
||||
return profileData;
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.findUnique.reset();
|
||||
this.edit.reset();
|
||||
},
|
||||
});
|
||||
|
||||
export default profilPerbekel;
|
||||
153
src/app/admin/(dashboard)/_state/desa/profile/sejarahDesa.ts
Normal file
153
src/app/admin/(dashboard)/_state/desa/profile/sejarahDesa.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const sejarahDesaForm = z.object({
|
||||
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||
});
|
||||
|
||||
const sejarahDesaDefaultForm = {
|
||||
judul: "",
|
||||
deskripsi: "",
|
||||
};
|
||||
|
||||
type SejarahDesaForm = Prisma.SejarahDesaGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
judul: true;
|
||||
deskripsi: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
const sejarahDesa = proxy({
|
||||
findUnique: {
|
||||
data: null as SejarahDesaForm | null,
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/sejarah/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.data = result.data;
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengambil data sejarah desa"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
console.error("Load sejarah desa error:", msg);
|
||||
toast.error("Terjadi kesalahan saat mengambil data sejarah desa");
|
||||
return null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...sejarahDesaDefaultForm },
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
isReadOnly: false,
|
||||
|
||||
initialize(sejarahData: SejarahDesaForm) {
|
||||
this.id = sejarahData.id;
|
||||
this.isReadOnly = false;
|
||||
this.form = {
|
||||
judul: sejarahData.judul || "",
|
||||
deskripsi: sejarahData.deskripsi || "",
|
||||
};
|
||||
},
|
||||
|
||||
updateField(field: keyof typeof sejarahDesaDefaultForm, value: string) {
|
||||
this.form[field] = value;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const validation = sejarahDesaForm.safeParse(this.form);
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues
|
||||
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||
.join(", ");
|
||||
toast.error(`Form tidak valid: ${errors}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/sejarah/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update profile");
|
||||
await sejarahDesa.findUnique.load(this.id);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update profile");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
this.error = errorMessage;
|
||||
console.error("Update profile error:", errorMessage);
|
||||
toast.error("Terjadi kesalahan saat update profile");
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.id = "";
|
||||
this.form = { ...sejarahDesaDefaultForm };
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
this.isReadOnly = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default sejarahDesa;
|
||||
153
src/app/admin/(dashboard)/_state/desa/profile/visiMisiDesa.ts
Normal file
153
src/app/admin/(dashboard)/_state/desa/profile/visiMisiDesa.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const visiMisiDesaForm = z.object({
|
||||
visi: z.string().min(3, "Visi minimal 3 karakter"),
|
||||
misi: z.string().min(3, "Misi minimal 3 karakter"),
|
||||
});
|
||||
|
||||
const visiMisiDesaDefaultForm = {
|
||||
visi: "",
|
||||
misi: "",
|
||||
};
|
||||
|
||||
type VisiMisiDesaForm = Prisma.VisiMisiDesaGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
visi: true;
|
||||
misi: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
const visiMisiDesa = proxy({
|
||||
findUnique: {
|
||||
data: null as VisiMisiDesaForm | null,
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/visi-misi/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.data = result.data;
|
||||
return result.data;
|
||||
} else {
|
||||
throw new Error(
|
||||
result.message || "Gagal mengambil data visi misi desa"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
this.error = msg;
|
||||
console.error("Load visi misi desa error:", msg);
|
||||
toast.error("Terjadi kesalahan saat mengambil data visi misi desa");
|
||||
return null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...visiMisiDesaDefaultForm },
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
isReadOnly: false,
|
||||
|
||||
initialize(visiMisiData: VisiMisiDesaForm) {
|
||||
this.id = visiMisiData.id;
|
||||
this.isReadOnly = false;
|
||||
this.form = {
|
||||
visi: visiMisiData.visi || "",
|
||||
misi: visiMisiData.misi || "",
|
||||
};
|
||||
},
|
||||
|
||||
updateField(field: keyof typeof visiMisiDesaDefaultForm, value: string) {
|
||||
this.form[field] = value;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const validation = visiMisiDesaForm.safeParse(this.form);
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues
|
||||
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||
.join(", ");
|
||||
toast.error(`Form tidak valid: ${errors}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/desa/profile/visi-misi/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update visi misi desa");
|
||||
await visiMisiDesa.findUnique.load(this.id);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update visi misi desa");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message;
|
||||
this.error = errorMessage;
|
||||
console.error("Update visi misi desa error:", errorMessage);
|
||||
toast.error("Terjadi kesalahan saat update visi misi desa");
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.id = "";
|
||||
this.form = { ...visiMisiDesaDefaultForm };
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
this.isReadOnly = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default visiMisiDesa;
|
||||
218
src/app/admin/(dashboard)/_state/kesehatan/balita/balita.ts
Normal file
218
src/app/admin/(dashboard)/_state/kesehatan/balita/balita.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { JenisKelaminBalita, Prisma, StatusStunting } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
nama: z.string().min(1, { message: "Nama wajib diisi" }),
|
||||
nik: z.string().optional(),
|
||||
tanggalLahir: z.string().min(1, { message: "Tanggal lahir wajib diisi" }),
|
||||
jenisKelamin: z.enum(["L", "P"]),
|
||||
beratBadanKg: z.number().optional(),
|
||||
tinggiBadanCm: z.number().optional(),
|
||||
namaOrtu: z.string().optional(),
|
||||
alamat: z.string().optional(),
|
||||
noHpOrtu: z.string().optional(),
|
||||
posyanduId: z.string().optional(),
|
||||
imunisasiLengkap: z.boolean(),
|
||||
giziBaik: z.boolean(),
|
||||
pemeriksaanRutin: z.boolean(),
|
||||
statusStunting: z.enum(["NORMAL", "ALERT", "STUNTING"]),
|
||||
catatan: z.string().optional(),
|
||||
});
|
||||
|
||||
const defaultForm = {
|
||||
nama: "",
|
||||
nik: "",
|
||||
tanggalLahir: "",
|
||||
jenisKelamin: "L" as JenisKelaminBalita,
|
||||
beratBadanKg: undefined as number | undefined,
|
||||
tinggiBadanCm: undefined as number | undefined,
|
||||
namaOrtu: "",
|
||||
alamat: "",
|
||||
noHpOrtu: "",
|
||||
posyanduId: "",
|
||||
imunisasiLengkap: false,
|
||||
giziBaik: true,
|
||||
pemeriksaanRutin: true,
|
||||
statusStunting: "NORMAL" as StatusStunting,
|
||||
catatan: "",
|
||||
};
|
||||
|
||||
const balitaState = proxy({
|
||||
create: {
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async submit() {
|
||||
const cek = formSchema.safeParse(balitaState.create.form);
|
||||
if (!cek.success) {
|
||||
const err = cek.error.issues.map((v) => v.message).join(", ");
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
balitaState.create.loading = true;
|
||||
const res = await fetch("/api/kesehatan/balita/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(cek.data),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success("Balita berhasil ditambahkan");
|
||||
balitaState.findMany.load();
|
||||
return true;
|
||||
}
|
||||
toast.error(result.message || "Gagal menambahkan data");
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Terjadi kesalahan");
|
||||
return false;
|
||||
} finally {
|
||||
balitaState.create.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
balitaState.create.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.BalitaGetPayload<{
|
||||
include: { posyandu: { select: { id: true; name: true } } };
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
statusStuntingFilter: "",
|
||||
async load(page = 1, limit = 10, search = "", statusStuntingFilter = "") {
|
||||
balitaState.findMany.loading = true;
|
||||
balitaState.findMany.page = page;
|
||||
balitaState.findMany.search = search;
|
||||
balitaState.findMany.statusStuntingFilter = statusStuntingFilter;
|
||||
try {
|
||||
const query = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||
if (search) query.set("search", search);
|
||||
if (statusStuntingFilter) query.set("statusStunting", statusStuntingFilter);
|
||||
const res = await fetch(`/api/kesehatan/balita/find-many?${query}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
balitaState.findMany.data = result.data ?? [];
|
||||
balitaState.findMany.totalPages = result.totalPages ?? 1;
|
||||
balitaState.findMany.total = result.total ?? 0;
|
||||
} else {
|
||||
balitaState.findMany.data = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("balitaFindMany error:", e);
|
||||
balitaState.findMany.data = [];
|
||||
} finally {
|
||||
balitaState.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/kesehatan/balita/${id}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
const d = result.data;
|
||||
balitaState.edit.id = d.id;
|
||||
balitaState.edit.form = {
|
||||
nama: d.nama,
|
||||
nik: d.nik ?? "",
|
||||
tanggalLahir: d.tanggalLahir ? d.tanggalLahir.slice(0, 10) : "",
|
||||
jenisKelamin: d.jenisKelamin,
|
||||
beratBadanKg: d.beratBadanKg ?? undefined,
|
||||
tinggiBadanCm: d.tinggiBadanCm ?? undefined,
|
||||
namaOrtu: d.namaOrtu ?? "",
|
||||
alamat: d.alamat ?? "",
|
||||
noHpOrtu: d.noHpOrtu ?? "",
|
||||
posyanduId: d.posyanduId ?? "",
|
||||
imunisasiLengkap: d.imunisasiLengkap,
|
||||
giziBaik: d.giziBaik,
|
||||
pemeriksaanRutin: d.pemeriksaanRutin,
|
||||
statusStunting: d.statusStunting,
|
||||
catatan: d.catatan ?? "",
|
||||
};
|
||||
return d;
|
||||
}
|
||||
toast.error("Gagal memuat data");
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Gagal memuat data");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = formSchema.safeParse(balitaState.edit.form);
|
||||
if (!cek.success) {
|
||||
const err = cek.error.issues.map((v) => v.message).join(", ");
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
balitaState.edit.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/balita/${balitaState.edit.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(cek.data),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success("Data balita berhasil diperbarui");
|
||||
balitaState.findMany.load();
|
||||
return true;
|
||||
}
|
||||
toast.error(result.message || "Gagal memperbarui data");
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Terjadi kesalahan");
|
||||
return false;
|
||||
} finally {
|
||||
balitaState.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
balitaState.edit.id = "";
|
||||
balitaState.edit.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
try {
|
||||
balitaState.delete.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/balita/del/${id}`, { method: "DELETE" });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success(result.message || "Data berhasil dihapus");
|
||||
await balitaState.findMany.load();
|
||||
} else {
|
||||
toast.error(result.message || "Gagal menghapus data");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Terjadi kesalahan saat menghapus");
|
||||
} finally {
|
||||
balitaState.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default balitaState;
|
||||
203
src/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil.ts
Normal file
203
src/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { IbuHamilStatus, Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
nama: z.string().min(1, { message: "Nama wajib diisi" }),
|
||||
nik: z.string().optional(),
|
||||
usiaKehamilan: z.number().min(0),
|
||||
hpht: z.string().optional(),
|
||||
taksiranLahir: z.string().optional(),
|
||||
alamat: z.string().optional(),
|
||||
noHp: z.string().optional(),
|
||||
catatan: z.string().optional(),
|
||||
posyanduId: z.string().optional(),
|
||||
status: z.enum(["AKTIF", "MELAHIRKAN", "KEGUGURAN", "NONAKTIF"]),
|
||||
});
|
||||
|
||||
const defaultForm = {
|
||||
nama: "",
|
||||
nik: "",
|
||||
usiaKehamilan: 0,
|
||||
hpht: "",
|
||||
taksiranLahir: "",
|
||||
alamat: "",
|
||||
noHp: "",
|
||||
catatan: "",
|
||||
posyanduId: "",
|
||||
status: "AKTIF" as IbuHamilStatus,
|
||||
};
|
||||
|
||||
const ibuHamilState = proxy({
|
||||
create: {
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async submit() {
|
||||
const cek = formSchema.safeParse(ibuHamilState.create.form);
|
||||
if (!cek.success) {
|
||||
const err = cek.error.issues.map((v) => v.message).join(", ");
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ibuHamilState.create.loading = true;
|
||||
const res = await fetch("/api/kesehatan/ibuhamil/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(cek.data),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success("Ibu hamil berhasil ditambahkan");
|
||||
ibuHamilState.findMany.load();
|
||||
return true;
|
||||
}
|
||||
toast.error(result.message || "Gagal menambahkan data");
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Terjadi kesalahan");
|
||||
return false;
|
||||
} finally {
|
||||
ibuHamilState.create.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
ibuHamilState.create.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.IbuHamilGetPayload<{
|
||||
include: { posyandu: { select: { id: true; name: true } } };
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
statusFilter: "",
|
||||
async load(page = 1, limit = 10, search = "", statusFilter = "") {
|
||||
ibuHamilState.findMany.loading = true;
|
||||
ibuHamilState.findMany.page = page;
|
||||
ibuHamilState.findMany.search = search;
|
||||
ibuHamilState.findMany.statusFilter = statusFilter;
|
||||
try {
|
||||
const query = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||
if (search) query.set("search", search);
|
||||
if (statusFilter) query.set("status", statusFilter);
|
||||
const res = await fetch(`/api/kesehatan/ibuhamil/find-many?${query}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
ibuHamilState.findMany.data = result.data ?? [];
|
||||
ibuHamilState.findMany.totalPages = result.totalPages ?? 1;
|
||||
ibuHamilState.findMany.total = result.total ?? 0;
|
||||
} else {
|
||||
ibuHamilState.findMany.data = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("ibuHamilFindMany error:", e);
|
||||
ibuHamilState.findMany.data = [];
|
||||
} finally {
|
||||
ibuHamilState.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/kesehatan/ibuhamil/${id}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
const d = result.data;
|
||||
ibuHamilState.edit.id = d.id;
|
||||
ibuHamilState.edit.form = {
|
||||
nama: d.nama,
|
||||
nik: d.nik ?? "",
|
||||
usiaKehamilan: d.usiaKehamilan,
|
||||
hpht: d.hpht ? d.hpht.slice(0, 10) : "",
|
||||
taksiranLahir: d.taksiranLahir ? d.taksiranLahir.slice(0, 10) : "",
|
||||
alamat: d.alamat ?? "",
|
||||
noHp: d.noHp ?? "",
|
||||
catatan: d.catatan ?? "",
|
||||
posyanduId: d.posyanduId ?? "",
|
||||
status: d.status,
|
||||
};
|
||||
return d;
|
||||
}
|
||||
toast.error("Gagal memuat data");
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Gagal memuat data");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = formSchema.safeParse(ibuHamilState.edit.form);
|
||||
if (!cek.success) {
|
||||
const err = cek.error.issues.map((v) => v.message).join(", ");
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ibuHamilState.edit.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/ibuhamil/${ibuHamilState.edit.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(cek.data),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success("Ibu hamil berhasil diperbarui");
|
||||
ibuHamilState.findMany.load();
|
||||
return true;
|
||||
}
|
||||
toast.error(result.message || "Gagal memperbarui data");
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Terjadi kesalahan");
|
||||
return false;
|
||||
} finally {
|
||||
ibuHamilState.edit.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
ibuHamilState.edit.id = "";
|
||||
ibuHamilState.edit.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
try {
|
||||
ibuHamilState.delete.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/ibuhamil/del/${id}`, { method: "DELETE" });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
toast.success(result.message || "Data berhasil dihapus");
|
||||
await ibuHamilState.findMany.load();
|
||||
} else {
|
||||
toast.error(result.message || "Gagal menghapus data");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Terjadi kesalahan saat menghapus");
|
||||
} finally {
|
||||
ibuHamilState.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default ibuHamilState;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
type StatsData = {
|
||||
ibuHamilAktif: number;
|
||||
balitaTerdaftar: number;
|
||||
alertStunting: number;
|
||||
imunisasiLengkapPct: number;
|
||||
pemeriksaanRutinPct: number;
|
||||
giziBaikPct: number;
|
||||
targetStuntingPct: number;
|
||||
};
|
||||
|
||||
const intPct = z
|
||||
.number({ invalid_type_error: "Harus berupa angka" })
|
||||
.int({ message: "Harus bilangan bulat" })
|
||||
.min(0, { message: "Minimal 0" })
|
||||
.max(100, { message: "Maksimal 100" });
|
||||
|
||||
const ringkasanKesehatanState = proxy({
|
||||
findStats: {
|
||||
data: null as StatsData | null,
|
||||
loading: false,
|
||||
async load() {
|
||||
try {
|
||||
ringkasanKesehatanState.findStats.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/ringkasankesehatan/stats`);
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
ringkasanKesehatanState.findStats.data = result?.data ?? null;
|
||||
if (result?.data) {
|
||||
ringkasanKesehatanState.update.form.targetStuntingPct =
|
||||
result.data.targetStuntingPct;
|
||||
}
|
||||
} else {
|
||||
ringkasanKesehatanState.findStats.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching ringkasan stats:", error);
|
||||
ringkasanKesehatanState.findStats.data = null;
|
||||
} finally {
|
||||
ringkasanKesehatanState.findStats.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
update: {
|
||||
form: { targetStuntingPct: 0 },
|
||||
loading: false,
|
||||
async submitTarget() {
|
||||
const pct = ringkasanKesehatanState.update.form.targetStuntingPct;
|
||||
const cek = intPct.safeParse(pct);
|
||||
if (!cek.success) {
|
||||
toast.error("Target stunting harus 0-100");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ringkasanKesehatanState.update.loading = true;
|
||||
const response = await fetch(`/api/kesehatan/ringkasankesehatan/update`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ targetStuntingPct: pct }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success("Target stunting berhasil disimpan");
|
||||
await ringkasanKesehatanState.findStats.load();
|
||||
return true;
|
||||
}
|
||||
toast.error(result.message || "Gagal menyimpan");
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error saving target stunting:", error);
|
||||
toast.error("Gagal menyimpan target stunting");
|
||||
return false;
|
||||
} finally {
|
||||
ringkasanKesehatanState.update.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default ringkasanKesehatanState;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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:
|
||||
|
||||
110
src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx
Normal file
110
src/app/admin/(dashboard)/desa/event-budaya/[id]/edit/page.tsx
Normal 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;
|
||||
95
src/app/admin/(dashboard)/desa/event-budaya/create/page.tsx
Normal file
95
src/app/admin/(dashboard)/desa/event-budaya/create/page.tsx
Normal 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;
|
||||
167
src/app/admin/(dashboard)/desa/event-budaya/page.tsx
Normal file
167
src/app/admin/(dashboard)/desa/event-budaya/page.tsx
Normal 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;
|
||||
106
src/app/admin/(dashboard)/desa/kegiatan-desa/_com/layoutTabs.tsx
Normal file
106
src/app/admin/(dashboard)/desa/kegiatan-desa/_com/layoutTabs.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { IconCalendarEvent, IconCategory } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabsKegiatanDesa({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Kegiatan",
|
||||
value: "list_kegiatan",
|
||||
href: "/admin/desa/kegiatan-desa/list-kegiatan-desa",
|
||||
icon: <IconCalendarEvent size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Kategori Kegiatan",
|
||||
value: "kategori_kegiatan",
|
||||
href: "/admin/desa/kegiatan-desa/kategori-kegiatan-desa",
|
||||
icon: <IconCategory 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" }}>Kegiatan Desa</Title>
|
||||
<Tabs
|
||||
color={colors["blue-button"]}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<ScrollArea type="auto" offsetScrollbars>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
gap: "0.5rem",
|
||||
paddingInline: "0.5rem",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsTab
|
||||
key={i}
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
</ScrollArea>
|
||||
|
||||
{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)",
|
||||
}}
|
||||
>
|
||||
<>{children}</>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsKegiatanDesa;
|
||||
@@ -0,0 +1,137 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditKategoriKegiatan() {
|
||||
const editState = useProxy(stateDashboardKegiatan.kategoriKegiatan);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({ nama: '' });
|
||||
const [originalData, setOriginalData] = useState({ nama: '' });
|
||||
|
||||
const isFormValid = () => formData.nama?.trim() !== '';
|
||||
|
||||
useEffect(() => {
|
||||
const loadKategori = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
await stateDashboardKegiatan.kategoriKegiatan.update.load(id);
|
||||
const nama = stateDashboardKegiatan.kategoriKegiatan.update.form.nama || '';
|
||||
setFormData({ nama });
|
||||
setOriginalData({ nama });
|
||||
} catch (error) {
|
||||
console.error('Error loading kategori kegiatan:', error);
|
||||
toast.error('Gagal memuat data kategori kegiatan');
|
||||
}
|
||||
};
|
||||
|
||||
loadKategori();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormData({ nama: originalData.nama });
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.nama?.trim()) {
|
||||
toast.error('Nama kategori kegiatan wajib diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
editState.update.form = {
|
||||
...editState.update.form,
|
||||
nama: formData.nama,
|
||||
};
|
||||
|
||||
const success = await editState.update.update();
|
||||
if (success) {
|
||||
router.push('/admin/desa/kegiatan-desa/kategori-kegiatan-desa');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating kategori kegiatan:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui kategori kegiatan');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Kategori Kegiatan
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Kategori Kegiatan"
|
||||
placeholder="Masukkan nama kategori kegiatan"
|
||||
value={formData.nama}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, nama: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" size="md" onClick={handleResetForm}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background:
|
||||
!isFormValid() || isSubmitting
|
||||
? 'linear-gradient(135deg, #cccccc, #eeeeee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditKategoriKegiatan;
|
||||
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function CreateKategoriKegiatan() {
|
||||
const createState = useProxy(stateDashboardKegiatan.kategoriKegiatan);
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const isFormValid = () => createState.create.form.nama?.trim() !== '';
|
||||
|
||||
const resetForm = () => {
|
||||
createState.create.resetForm();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!createState.create.form.nama?.trim()) {
|
||||
toast.error('Nama kategori kegiatan wajib diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const success = await createState.create.create();
|
||||
if (success) {
|
||||
resetForm();
|
||||
router.push('/admin/desa/kegiatan-desa/kategori-kegiatan-desa');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating kategori kegiatan:', error);
|
||||
toast.error('Gagal menambahkan kategori kegiatan');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Kategori Kegiatan
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Kategori Kegiatan"
|
||||
placeholder="Masukkan nama kategori kegiatan"
|
||||
value={createState.create.form.nama || ''}
|
||||
onChange={(e) => (createState.create.form.nama = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" size="md" onClick={resetForm}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background:
|
||||
!isFormValid() || isSubmitting
|
||||
? 'linear-gradient(135deg, #cccccc, #eeeeee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateKategoriKegiatan;
|
||||
@@ -0,0 +1,239 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
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 HeaderSearch from '../../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import stateDashboardKegiatan from '../../../_state/desa/kegiatanDesa';
|
||||
|
||||
function KategoriKegiatanDesa() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Kategori Kegiatan"
|
||||
placeholder="Cari nama kategori kegiatan..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListKategoriKegiatan search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
const listDataState = useProxy(stateDashboardKegiatan.kategoriKegiatan);
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, loading, load, page, totalPages } = listDataState.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
listDataState.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={{ base: 'sm', md: 'lg' }}>
|
||||
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
|
||||
<Title order={4} lh={1.2}>
|
||||
Daftar Kategori Kegiatan
|
||||
</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push('/admin/desa/kegiatan-desa/kategori-kegiatan-desa/create')
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover layout="fixed" withColumnBorders={false} miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="60%">
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||
</TableTh>
|
||||
<TableTh w="20%">
|
||||
<Text fz="sm" fw={600} lh={1.4} ta="center">Edit</Text>
|
||||
</TableTh>
|
||||
<TableTh w="20%">
|
||||
<Text fz="sm" fw={600} lh={1.4} ta="center">Hapus</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="sm" fw={500} lh={1.45} truncate="end">
|
||||
{item.nama}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/kegiatan-desa/kategori-kegiatan-desa/${item.id}`
|
||||
)
|
||||
}
|
||||
size="compact-sm"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
size="compact-sm"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Center py={24}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kategori kegiatan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="xs" mt="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder radius="md" p="sm" bg="white">
|
||||
<Box flex={1} ml="md">
|
||||
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45} truncate>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Box>
|
||||
<Group mt="sm" justify="flex-end" gap="xs">
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="compact-xs"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/kegiatan-desa/kategori-kegiatan-desa/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="compact-xs"
|
||||
disabled={listDataState.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py={32}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kategori kegiatan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center mt={{ base: 'lg', md: 'xl' }}>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleDelete}
|
||||
text="Apakah anda yakin ingin menghapus kategori kegiatan ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KategoriKegiatanDesa;
|
||||
28
src/app/admin/(dashboard)/desa/kegiatan-desa/layout.tsx
Normal file
28
src/app/admin/(dashboard)/desa/kegiatan-desa/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import React from 'react';
|
||||
import LayoutTabsKegiatanDesa from './_com/layoutTabs';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const isDetailPage = segments.length >= 5;
|
||||
|
||||
if (isDetailPage) {
|
||||
return (
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutTabsKegiatanDesa>
|
||||
{children}
|
||||
</LayoutTabsKegiatanDesa>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,335 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
const isHtmlEmpty = (html: string) => html.replace(/<[^>]*>/g, '').trim() === '';
|
||||
|
||||
interface KegiatanData {
|
||||
id: string;
|
||||
judul: string;
|
||||
deskripsiSingkat: string;
|
||||
deskripsiLengkap: string;
|
||||
tanggal: string;
|
||||
lokasi: string;
|
||||
partisipan: number;
|
||||
kategoriKegiatanId: string | null;
|
||||
imageId: string | null;
|
||||
image?: { link: string } | null;
|
||||
}
|
||||
|
||||
function EditKegiatanDesa() {
|
||||
const kegiatanState = useProxy(stateDashboardKegiatan);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const emptyForm = {
|
||||
judul: '',
|
||||
deskripsiSingkat: '',
|
||||
deskripsiLengkap: '',
|
||||
tanggal: '',
|
||||
lokasi: '',
|
||||
partisipan: 0,
|
||||
kategoriKegiatanId: '',
|
||||
imageId: '',
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState(emptyForm);
|
||||
const [originalData, setOriginalData] = useState({ ...emptyForm, imageUrl: '' });
|
||||
|
||||
const isFormValid = () =>
|
||||
formData.judul.trim() !== '' &&
|
||||
formData.deskripsiSingkat.trim() !== '' &&
|
||||
!isHtmlEmpty(formData.deskripsiLengkap) &&
|
||||
formData.tanggal !== '' &&
|
||||
formData.lokasi.trim() !== '' &&
|
||||
formData.kategoriKegiatanId !== '';
|
||||
|
||||
useEffect(() => {
|
||||
kegiatanState.kategoriKegiatan.findMany.load();
|
||||
|
||||
const load = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await stateDashboardKegiatan.kegiatan.edit.load(id) as KegiatanData | null;
|
||||
if (data) {
|
||||
const tanggal = data.tanggal
|
||||
? new Date(data.tanggal).toISOString().split('T')[0]
|
||||
: '';
|
||||
|
||||
const form = {
|
||||
judul: data.judul || '',
|
||||
deskripsiSingkat: data.deskripsiSingkat || '',
|
||||
deskripsiLengkap: data.deskripsiLengkap || '',
|
||||
tanggal,
|
||||
lokasi: data.lokasi || '',
|
||||
partisipan: data.partisipan || 0,
|
||||
kategoriKegiatanId: data.kategoriKegiatanId || '',
|
||||
imageId: data.imageId || '',
|
||||
};
|
||||
|
||||
setFormData(form);
|
||||
setOriginalData({ ...form, imageUrl: data.image?.link || '' });
|
||||
if (data.image?.link) setPreviewImage(data.image.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading kegiatan:', error);
|
||||
toast.error('Gagal memuat data kegiatan');
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleChange = (field: string, value: string | number) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.judul.trim()) return toast.error('Judul wajib diisi');
|
||||
if (!formData.deskripsiSingkat.trim()) return toast.error('Deskripsi singkat wajib diisi');
|
||||
if (isHtmlEmpty(formData.deskripsiLengkap)) return toast.error('Deskripsi lengkap wajib diisi');
|
||||
if (!formData.tanggal) return toast.error('Tanggal wajib diisi');
|
||||
if (!formData.lokasi.trim()) return toast.error('Lokasi wajib diisi');
|
||||
if (!formData.kategoriKegiatanId) return toast.error('Kategori wajib dipilih');
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
kegiatanState.kegiatan.edit.form = {
|
||||
...kegiatanState.kegiatan.edit.form,
|
||||
...formData,
|
||||
};
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||
kegiatanState.kegiatan.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
const success = await kegiatanState.kegiatan.edit.update();
|
||||
if (success) {
|
||||
router.push('/admin/desa/kegiatan-desa/list-kegiatan-desa');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating kegiatan:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui kegiatan');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFormData({
|
||||
judul: originalData.judul,
|
||||
deskripsiSingkat: originalData.deskripsiSingkat,
|
||||
deskripsiLengkap: originalData.deskripsiLengkap,
|
||||
tanggal: originalData.tanggal,
|
||||
lokasi: originalData.lokasi,
|
||||
partisipan: originalData.partisipan,
|
||||
kategoriKegiatanId: originalData.kategoriKegiatanId,
|
||||
imageId: originalData.imageId,
|
||||
});
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setFile(null);
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">Edit Kegiatan Desa</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul"
|
||||
placeholder="Masukkan judul kegiatan"
|
||||
value={formData.judul}
|
||||
onChange={(e) => handleChange('judul', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Kategori Kegiatan"
|
||||
placeholder="Pilih kategori"
|
||||
data={kegiatanState.kategoriKegiatan.findMany.data.map((item) => ({
|
||||
label: item.nama,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={formData.kategoriKegiatanId || null}
|
||||
onChange={(val) => handleChange('kategoriKegiatanId', val || '')}
|
||||
searchable
|
||||
clearable
|
||||
nothingFoundMessage="Tidak ditemukan"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
type="date"
|
||||
value={formData.tanggal}
|
||||
onChange={(e) => handleChange('tanggal', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi kegiatan"
|
||||
value={formData.lokasi}
|
||||
onChange={(e) => handleChange('lokasi', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah Partisipan"
|
||||
placeholder="Masukkan jumlah partisipan"
|
||||
value={formData.partisipan}
|
||||
onChange={(val) => handleChange('partisipan', Number(val) || 0)}
|
||||
min={0}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Deskripsi Singkat"
|
||||
placeholder="Masukkan deskripsi singkat kegiatan"
|
||||
value={formData.deskripsiSingkat}
|
||||
onChange={(e) => handleChange('deskripsiSingkat', e.target.value)}
|
||||
minRows={3}
|
||||
autosize
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb={6}>Deskripsi Lengkap</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsiLengkap}
|
||||
onChange={(html) => setFormData((prev) => ({ ...prev, deskripsiLengkap: html }))}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>Gambar (Opsional)</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const f = files[0];
|
||||
if (f) { setFile(f); setPreviewImage(URL.createObjectURL(f)); }
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={160}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
objectFit: 'contain',
|
||||
border: `1px solid ${colors['blue-button']}`,
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="red"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
pos="absolute"
|
||||
top={5}
|
||||
right={5}
|
||||
onClick={() => { setPreviewImage(null); setFile(null); }}
|
||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<IconX size={14} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" size="md" onClick={handleReset}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background:
|
||||
!isFormValid() || isSubmitting
|
||||
? 'linear-gradient(135deg, #cccccc, #eeeeee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditKegiatanDesa;
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
interface KegiatanDetail {
|
||||
id: string;
|
||||
judul: string;
|
||||
deskripsiSingkat: string;
|
||||
deskripsiLengkap: string;
|
||||
tanggal: string;
|
||||
lokasi: string;
|
||||
partisipan: number;
|
||||
kategoriKegiatan?: { nama: string } | null;
|
||||
image?: { link: string } | null;
|
||||
}
|
||||
|
||||
function DetailKegiatanDesa() {
|
||||
const kegiatanState = useProxy(stateDashboardKegiatan);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
kegiatanState.kegiatan.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
kegiatanState.kegiatan.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push('/admin/desa/kegiatan-desa/list-kegiatan-desa');
|
||||
}
|
||||
};
|
||||
|
||||
if (!kegiatanState.kegiatan.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const data = kegiatanState.kegiatan.findUnique.data as unknown as KegiatanDetail;
|
||||
|
||||
const formatTanggal = (val: string) => {
|
||||
if (!val) return '-';
|
||||
return new Date(val).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '70%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Kegiatan Desa
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Kategori</Text>
|
||||
<Text fz="md" c="dimmed">{data.kategoriKegiatan?.nama || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul</Text>
|
||||
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Tanggal</Text>
|
||||
<Text fz="md" c="dimmed">{formatTanggal(data.tanggal)}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Lokasi</Text>
|
||||
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Partisipan</Text>
|
||||
<Text fz="md" c="dimmed">{data.partisipan ?? 0} orang</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
|
||||
<Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{data.deskripsiSingkat || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi Lengkap</Text>
|
||||
<Paper bg="white" p="md" radius="md" mt="xs">
|
||||
<Text
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{data.image?.link && (
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.judul || 'Gambar Kegiatan'}
|
||||
w={{ base: '100%', md: 400 }}
|
||||
h={300}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
mt="xs"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => { setSelectedId(data.id); setModalHapus(true); }}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
leftSection={<IconTrash size={20} />}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/desa/kegiatan-desa/list-kegiatan-desa/${data.id}/edit`
|
||||
)
|
||||
}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
leftSection={<IconEdit size={20} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah Anda yakin ingin menghapus kegiatan desa ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailKegiatanDesa;
|
||||
@@ -0,0 +1,258 @@
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import stateDashboardKegiatan from '@/app/admin/(dashboard)/_state/desa/kegiatanDesa';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
const isHtmlEmpty = (html: string) => html.replace(/<[^>]*>/g, '').trim() === '';
|
||||
|
||||
export default function CreateKegiatanDesa() {
|
||||
const kegiatanState = useProxy(stateDashboardKegiatan);
|
||||
const router = useRouter();
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useShallowEffect(() => {
|
||||
kegiatanState.kategoriKegiatan.findMany.load();
|
||||
}, []);
|
||||
|
||||
const isFormValid = () => {
|
||||
const f = kegiatanState.kegiatan.create.form;
|
||||
return (
|
||||
f.judul.trim() !== '' &&
|
||||
f.deskripsiSingkat.trim() !== '' &&
|
||||
!isHtmlEmpty(f.deskripsiLengkap) &&
|
||||
f.tanggal !== '' &&
|
||||
f.lokasi.trim() !== '' &&
|
||||
f.kategoriKegiatanId !== ''
|
||||
);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
kegiatanState.kegiatan.create.resetForm();
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const f = kegiatanState.kegiatan.create.form;
|
||||
if (!f.judul.trim()) return toast.error('Judul wajib diisi');
|
||||
if (!f.deskripsiSingkat.trim()) return toast.error('Deskripsi singkat wajib diisi');
|
||||
if (isHtmlEmpty(f.deskripsiLengkap)) return toast.error('Deskripsi lengkap wajib diisi');
|
||||
if (!f.tanggal) return toast.error('Tanggal wajib diisi');
|
||||
if (!f.lokasi.trim()) return toast.error('Lokasi wajib diisi');
|
||||
if (!f.kategoriKegiatanId) return toast.error('Kategori wajib dipilih');
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
if (!uploaded?.id) return toast.error('Gagal mengunggah gambar');
|
||||
kegiatanState.kegiatan.create.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
const success = await kegiatanState.kegiatan.create.create();
|
||||
if (success) {
|
||||
resetForm();
|
||||
router.push('/admin/desa/kegiatan-desa/list-kegiatan-desa');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating kegiatan:', error);
|
||||
toast.error('Terjadi kesalahan saat membuat kegiatan');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">Tambah Kegiatan Desa</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul"
|
||||
placeholder="Masukkan judul kegiatan"
|
||||
value={kegiatanState.kegiatan.create.form.judul}
|
||||
onChange={(e) => (kegiatanState.kegiatan.create.form.judul = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Kategori Kegiatan"
|
||||
placeholder="Pilih kategori"
|
||||
data={kegiatanState.kategoriKegiatan.findMany.data.map((item) => ({
|
||||
label: item.nama,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={kegiatanState.kegiatan.create.form.kategoriKegiatanId || null}
|
||||
onChange={(val) => {
|
||||
kegiatanState.kegiatan.create.form.kategoriKegiatanId = val || '';
|
||||
}}
|
||||
searchable
|
||||
clearable
|
||||
nothingFoundMessage="Tidak ditemukan"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
type="date"
|
||||
value={kegiatanState.kegiatan.create.form.tanggal}
|
||||
onChange={(e) => (kegiatanState.kegiatan.create.form.tanggal = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi kegiatan"
|
||||
value={kegiatanState.kegiatan.create.form.lokasi}
|
||||
onChange={(e) => (kegiatanState.kegiatan.create.form.lokasi = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Jumlah Partisipan"
|
||||
placeholder="Masukkan jumlah partisipan"
|
||||
value={kegiatanState.kegiatan.create.form.partisipan}
|
||||
onChange={(val) => (kegiatanState.kegiatan.create.form.partisipan = Number(val) || 0)}
|
||||
min={0}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Deskripsi Singkat"
|
||||
placeholder="Masukkan deskripsi singkat kegiatan"
|
||||
value={kegiatanState.kegiatan.create.form.deskripsiSingkat}
|
||||
onChange={(e) => (kegiatanState.kegiatan.create.form.deskripsiSingkat = e.target.value)}
|
||||
minRows={3}
|
||||
autosize
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb={6}>Deskripsi Lengkap</Text>
|
||||
<CreateEditor
|
||||
value={kegiatanState.kegiatan.create.form.deskripsiLengkap}
|
||||
onChange={(html) => { kegiatanState.kegiatan.create.form.deskripsiLengkap = html; }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>Gambar (Opsional)</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const f = files[0];
|
||||
if (f) { setFile(f); setPreviewImage(URL.createObjectURL(f)); }
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={160}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
</Group>
|
||||
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||
Seret gambar atau klik untuk memilih (maks 5MB)
|
||||
</Text>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
radius="md"
|
||||
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="red"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
pos="absolute"
|
||||
top={5}
|
||||
right={5}
|
||||
onClick={() => { setPreviewImage(null); setFile(null); }}
|
||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<IconX size={14} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" size="md" onClick={resetForm}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
style={{
|
||||
background:
|
||||
!isFormValid() || isSubmitting
|
||||
? 'linear-gradient(135deg, #cccccc, #eeeeee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconPlus, IconDeviceImacCog, 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 stateDashboardKegiatan from '../../../_state/desa/kegiatanDesa';
|
||||
|
||||
function KegiatanDesa() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Kegiatan Desa"
|
||||
placeholder="Cari judul atau lokasi kegiatan..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListKegiatanDesa search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListKegiatanDesa({ search }: { search: string }) {
|
||||
const kegiatanState = useProxy(stateDashboardKegiatan);
|
||||
const router = useRouter();
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = kegiatanState.kegiatan.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="md">
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
return (
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Kegiatan Desa</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/desa/kegiatan-desa/list-kegiatan-desa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover layout="fixed" withColumnBorders={false} miw={0}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="35%">Judul</TableTh>
|
||||
<TableTh w="25%">Kategori</TableTh>
|
||||
<TableTh w="20%">Lokasi</TableTh>
|
||||
<TableTh w="20%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fz="md" fw={600} lh={1.45} truncate="end">
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" lh={1.45}>
|
||||
{item.kategoriKegiatan?.nama || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm" c="dimmed" lh={1.45} truncate="end">
|
||||
{item.lokasi || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/kegiatan-desa/list-kegiatan-desa/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
px="sm"
|
||||
h={36}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml="xs">Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kegiatan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Cards */}
|
||||
<Stack hiddenFrom="md" gap="sm" mt="sm">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<Paper key={item.id} withBorder p="md" radius="md">
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz="sm" fw={600} lh={1.4} c="dimmed">Judul</Text>
|
||||
<Text fz="sm" fw={500} lh={1.45}>{item.judul}</Text>
|
||||
|
||||
<Text fz="sm" fw={600} lh={1.4} c="dimmed" mt="xs">Kategori</Text>
|
||||
<Text fz="sm" lh={1.45} fw={500}>{item.kategoriKegiatan?.nama || '-'}</Text>
|
||||
|
||||
<Text fz="sm" fw={600} lh={1.4} c="dimmed" mt="xs">Lokasi</Text>
|
||||
<Text fz="sm" lh={1.45} fw={500}>{item.lokasi || '-'}</Text>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
onClick={() =>
|
||||
router.push(`/admin/desa/kegiatan-desa/list-kegiatan-desa/${item.id}`)
|
||||
}
|
||||
fz="sm"
|
||||
h={36}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
<Text ml="xs">Detail</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data kegiatan yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, debouncedSearch);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default KegiatanDesa;
|
||||
161
src/app/admin/(dashboard)/kesehatan/posyandu/_com/layoutTabs.tsx
Normal file
161
src/app/admin/(dashboard)/kesehatan/posyandu/_com/layoutTabs.tsx
Normal 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;
|
||||
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Group,
|
||||
Loader,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
export default function BalitaCreatePage() {
|
||||
const router = useRouter();
|
||||
const state = useProxy(balitaState);
|
||||
const form = state.create.form;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const ok = await balitaState.create.submit();
|
||||
if (ok) {
|
||||
balitaState.create.reset();
|
||||
router.push('/admin/kesehatan/posyandu/balita');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md" gap="sm">
|
||||
<Button variant="subtle" onClick={() => router.back()} radius="md">
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
<Title order={3} c="black">Tambah Balita</Title>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
|
||||
<Stack gap="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
required
|
||||
placeholder="Nama balita"
|
||||
value={form.nama}
|
||||
onChange={(e) => { balitaState.create.form.nama = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="NIK"
|
||||
placeholder="Nomor Induk Kependudukan"
|
||||
value={form.nik}
|
||||
onChange={(e) => { balitaState.create.form.nik = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal Lahir"
|
||||
required
|
||||
type="date"
|
||||
value={form.tanggalLahir}
|
||||
onChange={(e) => { balitaState.create.form.tanggalLahir = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
required
|
||||
data={[
|
||||
{ value: 'L', label: 'Laki-laki' },
|
||||
{ value: 'P', label: 'Perempuan' },
|
||||
]}
|
||||
value={form.jenisKelamin}
|
||||
onChange={(v) => { if (v) balitaState.create.form.jenisKelamin = v as typeof form.jenisKelamin; }}
|
||||
radius="md"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Berat Badan (kg)"
|
||||
placeholder="0.0"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
value={form.beratBadanKg ?? ''}
|
||||
onChange={(v) => { balitaState.create.form.beratBadanKg = v === '' ? undefined : Number(v); }}
|
||||
radius="md"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tinggi Badan (cm)"
|
||||
placeholder="0.0"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
value={form.tinggiBadanCm ?? ''}
|
||||
onChange={(v) => { balitaState.create.form.tinggiBadanCm = v === '' ? undefined : Number(v); }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Nama Orang Tua"
|
||||
placeholder="Nama ayah/ibu"
|
||||
value={form.namaOrtu}
|
||||
onChange={(e) => { balitaState.create.form.namaOrtu = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="No. HP Orang Tua"
|
||||
placeholder="08xx-xxxx-xxxx"
|
||||
value={form.noHpOrtu}
|
||||
onChange={(e) => { balitaState.create.form.noHpOrtu = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<Select
|
||||
label="Status Stunting"
|
||||
required
|
||||
data={[
|
||||
{ value: 'NORMAL', label: 'Normal' },
|
||||
{ value: 'ALERT', label: 'Alert (Berisiko)' },
|
||||
{ value: 'STUNTING', label: 'Stunting' },
|
||||
]}
|
||||
value={form.statusStunting}
|
||||
onChange={(v) => { if (v) balitaState.create.form.statusStunting = v as typeof form.statusStunting; }}
|
||||
radius="md"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Alamat lengkap"
|
||||
value={form.alamat}
|
||||
onChange={(e) => { balitaState.create.form.alamat = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
|
||||
<Group gap="xl">
|
||||
<Checkbox
|
||||
label="Imunisasi Lengkap"
|
||||
checked={form.imunisasiLengkap}
|
||||
onChange={(e) => { balitaState.create.form.imunisasiLengkap = e.currentTarget.checked; }}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Gizi Baik"
|
||||
checked={form.giziBaik}
|
||||
onChange={(e) => { balitaState.create.form.giziBaik = e.currentTarget.checked; }}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Pemeriksaan Rutin"
|
||||
checked={form.pemeriksaanRutin}
|
||||
onChange={(e) => { balitaState.create.form.pemeriksaanRutin = e.currentTarget.checked; }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Textarea
|
||||
label="Catatan"
|
||||
placeholder="Catatan tambahan"
|
||||
value={form.catatan}
|
||||
onChange={(e) => { balitaState.create.form.catatan = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
disabled={state.create.loading}
|
||||
style={{
|
||||
background: state.create.loading
|
||||
? 'linear-gradient(135deg, #ccc, #eee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{state.create.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
'use client';
|
||||
|
||||
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Group,
|
||||
Loader,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
export default function BalitaEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const state = useProxy(balitaState);
|
||||
const form = state.edit.form;
|
||||
|
||||
useEffect(() => {
|
||||
if (id) balitaState.edit.load(id);
|
||||
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const ok = await balitaState.edit.update();
|
||||
if (ok) router.push('/admin/kesehatan/posyandu/balita');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md" gap="sm">
|
||||
<Button variant="subtle" onClick={() => router.back()} radius="md">
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
<Title order={3} c="black">Edit Balita</Title>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
|
||||
<Stack gap="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
required
|
||||
placeholder="Nama balita"
|
||||
value={form.nama}
|
||||
onChange={(e) => { balitaState.edit.form.nama = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="NIK"
|
||||
placeholder="Nomor Induk Kependudukan"
|
||||
value={form.nik}
|
||||
onChange={(e) => { balitaState.edit.form.nik = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal Lahir"
|
||||
required
|
||||
type="date"
|
||||
value={form.tanggalLahir}
|
||||
onChange={(e) => { balitaState.edit.form.tanggalLahir = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<Select
|
||||
label="Jenis Kelamin"
|
||||
required
|
||||
data={[
|
||||
{ value: 'L', label: 'Laki-laki' },
|
||||
{ value: 'P', label: 'Perempuan' },
|
||||
]}
|
||||
value={form.jenisKelamin}
|
||||
onChange={(v) => { if (v) balitaState.edit.form.jenisKelamin = v as typeof form.jenisKelamin; }}
|
||||
radius="md"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Berat Badan (kg)"
|
||||
placeholder="0.0"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
value={form.beratBadanKg ?? ''}
|
||||
onChange={(v) => { balitaState.edit.form.beratBadanKg = v === '' ? undefined : Number(v); }}
|
||||
radius="md"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tinggi Badan (cm)"
|
||||
placeholder="0.0"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
value={form.tinggiBadanCm ?? ''}
|
||||
onChange={(v) => { balitaState.edit.form.tinggiBadanCm = v === '' ? undefined : Number(v); }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Nama Orang Tua"
|
||||
placeholder="Nama ayah/ibu"
|
||||
value={form.namaOrtu}
|
||||
onChange={(e) => { balitaState.edit.form.namaOrtu = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="No. HP Orang Tua"
|
||||
placeholder="08xx-xxxx-xxxx"
|
||||
value={form.noHpOrtu}
|
||||
onChange={(e) => { balitaState.edit.form.noHpOrtu = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<Select
|
||||
label="Status Stunting"
|
||||
required
|
||||
data={[
|
||||
{ value: 'NORMAL', label: 'Normal' },
|
||||
{ value: 'ALERT', label: 'Alert (Berisiko)' },
|
||||
{ value: 'STUNTING', label: 'Stunting' },
|
||||
]}
|
||||
value={form.statusStunting}
|
||||
onChange={(v) => { if (v) balitaState.edit.form.statusStunting = v as typeof form.statusStunting; }}
|
||||
radius="md"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Alamat lengkap"
|
||||
value={form.alamat}
|
||||
onChange={(e) => { balitaState.edit.form.alamat = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
|
||||
<Group gap="xl">
|
||||
<Checkbox
|
||||
label="Imunisasi Lengkap"
|
||||
checked={form.imunisasiLengkap}
|
||||
onChange={(e) => { balitaState.edit.form.imunisasiLengkap = e.currentTarget.checked; }}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Gizi Baik"
|
||||
checked={form.giziBaik}
|
||||
onChange={(e) => { balitaState.edit.form.giziBaik = e.currentTarget.checked; }}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Pemeriksaan Rutin"
|
||||
checked={form.pemeriksaanRutin}
|
||||
onChange={(e) => { balitaState.edit.form.pemeriksaanRutin = e.currentTarget.checked; }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Textarea
|
||||
label="Catatan"
|
||||
placeholder="Catatan tambahan"
|
||||
value={form.catatan}
|
||||
onChange={(e) => { balitaState.edit.form.catatan = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
disabled={state.edit.loading}
|
||||
style={{
|
||||
background: state.edit.loading
|
||||
? 'linear-gradient(135deg, #ccc, #eee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{state.edit.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
307
src/app/admin/(dashboard)/kesehatan/posyandu/balita/page.tsx
Normal file
307
src/app/admin/(dashboard)/kesehatan/posyandu/balita/page.tsx
Normal 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;
|
||||
@@ -0,0 +1,146 @@
|
||||
'use client';
|
||||
|
||||
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
export default function IbuHamilCreatePage() {
|
||||
const router = useRouter();
|
||||
const state = useProxy(ibuHamilState);
|
||||
const form = state.create.form;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const ok = await ibuHamilState.create.submit();
|
||||
if (ok) {
|
||||
ibuHamilState.create.reset();
|
||||
router.push('/admin/kesehatan/posyandu/ibu-hamil');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md" gap="sm">
|
||||
<Button variant="subtle" onClick={() => router.back()} radius="md">
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
<Title order={3} c="black">Tambah Ibu Hamil</Title>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
|
||||
<Stack gap="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
required
|
||||
placeholder="Nama ibu hamil"
|
||||
value={form.nama}
|
||||
onChange={(e) => { ibuHamilState.create.form.nama = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="NIK"
|
||||
placeholder="Nomor Induk Kependudukan"
|
||||
value={form.nik}
|
||||
onChange={(e) => { ibuHamilState.create.form.nik = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Usia Kehamilan (minggu)"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={String(form.usiaKehamilan)}
|
||||
onChange={(e) => { ibuHamilState.create.form.usiaKehamilan = Number(e.currentTarget.value) || 0; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="No. HP"
|
||||
placeholder="08xx-xxxx-xxxx"
|
||||
value={form.noHp}
|
||||
onChange={(e) => { ibuHamilState.create.form.noHp = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="HPHT (Hari Pertama Haid Terakhir)"
|
||||
type="date"
|
||||
value={form.hpht}
|
||||
onChange={(e) => { ibuHamilState.create.form.hpht = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Taksiran Persalinan"
|
||||
type="date"
|
||||
value={form.taksiranLahir}
|
||||
onChange={(e) => { ibuHamilState.create.form.taksiranLahir = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<Select
|
||||
label="Status"
|
||||
required
|
||||
data={[
|
||||
{ value: 'AKTIF', label: 'Aktif' },
|
||||
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
|
||||
{ value: 'KEGUGURAN', label: 'Keguguran' },
|
||||
{ value: 'NONAKTIF', label: 'Nonaktif' },
|
||||
]}
|
||||
value={form.status}
|
||||
onChange={(v) => { if (v) ibuHamilState.create.form.status = v as typeof form.status; }}
|
||||
radius="md"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Alamat lengkap"
|
||||
value={form.alamat}
|
||||
onChange={(e) => { ibuHamilState.create.form.alamat = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Catatan"
|
||||
placeholder="Catatan tambahan"
|
||||
value={form.catatan}
|
||||
onChange={(e) => { ibuHamilState.create.form.catatan = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
disabled={state.create.loading}
|
||||
style={{
|
||||
background: state.create.loading
|
||||
? 'linear-gradient(135deg, #ccc, #eee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{state.create.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
export default function IbuHamilEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
const state = useProxy(ibuHamilState);
|
||||
const form = state.edit.form;
|
||||
|
||||
useEffect(() => {
|
||||
if (id) ibuHamilState.edit.load(id);
|
||||
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const ok = await ibuHamilState.edit.update();
|
||||
if (ok) router.push('/admin/kesehatan/posyandu/ibu-hamil');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md" gap="sm">
|
||||
<Button variant="subtle" onClick={() => router.back()} radius="md">
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
<Title order={3} c="black">Edit Ibu Hamil</Title>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder w={{ base: '100%', md: '80%' }} p="lg" radius="md" shadow="xl" bg="white">
|
||||
<Stack gap="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
required
|
||||
placeholder="Nama ibu hamil"
|
||||
value={form.nama}
|
||||
onChange={(e) => { ibuHamilState.edit.form.nama = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="NIK"
|
||||
placeholder="Nomor Induk Kependudukan"
|
||||
value={form.nik}
|
||||
onChange={(e) => { ibuHamilState.edit.form.nik = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Usia Kehamilan (minggu)"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={String(form.usiaKehamilan)}
|
||||
onChange={(e) => { ibuHamilState.edit.form.usiaKehamilan = Number(e.currentTarget.value) || 0; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="No. HP"
|
||||
placeholder="08xx-xxxx-xxxx"
|
||||
value={form.noHp}
|
||||
onChange={(e) => { ibuHamilState.edit.form.noHp = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="HPHT"
|
||||
type="date"
|
||||
value={form.hpht}
|
||||
onChange={(e) => { ibuHamilState.edit.form.hpht = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Taksiran Persalinan"
|
||||
type="date"
|
||||
value={form.taksiranLahir}
|
||||
onChange={(e) => { ibuHamilState.edit.form.taksiranLahir = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
<Select
|
||||
label="Status"
|
||||
required
|
||||
data={[
|
||||
{ value: 'AKTIF', label: 'Aktif' },
|
||||
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
|
||||
{ value: 'KEGUGURAN', label: 'Keguguran' },
|
||||
{ value: 'NONAKTIF', label: 'Nonaktif' },
|
||||
]}
|
||||
value={form.status}
|
||||
onChange={(v) => { if (v) ibuHamilState.edit.form.status = v as typeof form.status; }}
|
||||
radius="md"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Alamat lengkap"
|
||||
value={form.alamat}
|
||||
onChange={(e) => { ibuHamilState.edit.form.alamat = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Catatan"
|
||||
placeholder="Catatan tambahan"
|
||||
value={form.catatan}
|
||||
onChange={(e) => { ibuHamilState.edit.form.catatan = e.currentTarget.value; }}
|
||||
radius="md"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button variant="outline" color="gray" radius="md" onClick={() => router.back()}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
disabled={state.edit.loading}
|
||||
style={{
|
||||
background: state.edit.loading
|
||||
? 'linear-gradient(135deg, #ccc, #eee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{state.edit.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
278
src/app/admin/(dashboard)/kesehatan/posyandu/ibu-hamil/page.tsx
Normal file
278
src/app/admin/(dashboard)/kesehatan/posyandu/ibu-hamil/page.tsx
Normal 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;
|
||||
35
src/app/admin/(dashboard)/kesehatan/posyandu/layout.tsx
Normal file
35
src/app/admin/(dashboard)/kesehatan/posyandu/layout.tsx
Normal 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;
|
||||
@@ -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">
|
||||
@@ -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"
|
||||
@@ -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');
|
||||
@@ -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
|
||||
@@ -0,0 +1,220 @@
|
||||
'use client';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
NumberInput,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconMoodBoy,
|
||||
IconHeartbeat,
|
||||
IconPercentage,
|
||||
IconUser,
|
||||
IconAlertTriangle,
|
||||
} from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import ringkasanKesehatanState from '../../../_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan';
|
||||
|
||||
|
||||
type StatCardProps = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
icon: React.ReactNode;
|
||||
color?: string;
|
||||
suffix?: string;
|
||||
};
|
||||
|
||||
function StatCard({ label, value, icon, color = 'blue', suffix }: StatCardProps) {
|
||||
return (
|
||||
<Card withBorder radius="md" p="md">
|
||||
<Group gap="sm" align="flex-start">
|
||||
<Box
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: `var(--mantine-color-${color}-1)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: `var(--mantine-color-${color}-6)`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="xs" c="dimmed" fw={500}>{label}</Text>
|
||||
<Text fz="xl" fw={700}>
|
||||
{value}{suffix && <Text component="span" fz="sm" c="dimmed" fw={400}> {suffix}</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RingkasanKesehatanPage() {
|
||||
const router = useRouter();
|
||||
const state = useProxy(ringkasanKesehatanState);
|
||||
const stats = state.findStats.data;
|
||||
|
||||
const loadStats = useCallback(() => { ringkasanKesehatanState.findStats.load(); }, []);
|
||||
useEffect(() => { loadStats(); }, [loadStats]);
|
||||
|
||||
const isLoading = state.findStats.loading;
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Title order={3} c="black" mb="md">Ringkasan Kesehatan Desa</Title>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
{/* KPI Utama */}
|
||||
<Box>
|
||||
<Text fw={600} mb="sm" c="dark">KPI Utama</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
|
||||
<StatCard
|
||||
label="Ibu Hamil Aktif"
|
||||
value={stats?.ibuHamilAktif ?? 0}
|
||||
icon={<IconUser size={20} />}
|
||||
color="pink"
|
||||
suffix="orang"
|
||||
/>
|
||||
<StatCard
|
||||
label="Balita Terdaftar"
|
||||
value={stats?.balitaTerdaftar ?? 0}
|
||||
icon={<IconMoodBoy size={20} />}
|
||||
color="blue"
|
||||
suffix="anak"
|
||||
/>
|
||||
<StatCard
|
||||
label="Alert Stunting"
|
||||
value={stats?.alertStunting ?? 0}
|
||||
icon={<IconAlertTriangle size={20} />}
|
||||
color="red"
|
||||
suffix="anak"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Statistik % */}
|
||||
<Box>
|
||||
<Text fw={600} mb="sm" c="dark">Statistik Kesehatan Balita</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md">
|
||||
<StatCard
|
||||
label="Imunisasi Lengkap"
|
||||
value={stats?.imunisasiLengkapPct ?? 0}
|
||||
icon={<IconHeartbeat size={20} />}
|
||||
color="teal"
|
||||
suffix="%"
|
||||
/>
|
||||
<StatCard
|
||||
label="Pemeriksaan Rutin"
|
||||
value={stats?.pemeriksaanRutinPct ?? 0}
|
||||
icon={<IconHeartbeat size={20} />}
|
||||
color="green"
|
||||
suffix="%"
|
||||
/>
|
||||
<StatCard
|
||||
label="Gizi Baik"
|
||||
value={stats?.giziBaikPct ?? 0}
|
||||
icon={<IconHeartbeat size={20} />}
|
||||
color="lime"
|
||||
suffix="%"
|
||||
/>
|
||||
<StatCard
|
||||
label="Target Penurunan Stunting"
|
||||
value={stats?.targetStuntingPct ?? 0}
|
||||
icon={<IconPercentage size={20} />}
|
||||
color="orange"
|
||||
suffix="%"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Target Stunting Config */}
|
||||
<Paper withBorder p="md" radius="md" maw={400}>
|
||||
<Text fw={600} mb="sm" c="dark">Atur Target Stunting</Text>
|
||||
<Text fz="xs" c="dimmed" mb="sm">
|
||||
Target penurunan angka stunting adalah nilai kebijakan yang ditentukan
|
||||
oleh admin, bukan turunan dari data.
|
||||
</Text>
|
||||
<Group align="flex-end" gap="sm">
|
||||
<NumberInput
|
||||
label="Target (%)"
|
||||
min={0}
|
||||
max={100}
|
||||
suffix="%"
|
||||
value={state.update.form.targetStuntingPct}
|
||||
onChange={(v) => { state.update.form.targetStuntingPct = Number(v) || 0; }}
|
||||
radius="md"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => state.update.submitTarget()}
|
||||
radius="md"
|
||||
disabled={state.update.loading}
|
||||
style={{
|
||||
background: state.update.loading
|
||||
? 'linear-gradient(135deg, #ccc, #eee)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
marginBottom: 1,
|
||||
}}
|
||||
>
|
||||
{state.update.loading ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Kelola Data */}
|
||||
<Box>
|
||||
<Text fw={600} mb="sm" c="dark">Kelola Data</Text>
|
||||
<Group gap="md">
|
||||
<Button
|
||||
variant="light"
|
||||
color="pink"
|
||||
radius="md"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil')}
|
||||
>
|
||||
Kelola Ibu Hamil
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
radius="md"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push('/admin/kesehatan/posyandu/balita')}
|
||||
>
|
||||
Kelola Balita
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -118,6 +118,16 @@ export const devBar = [
|
||||
id: "Desa_7",
|
||||
name: "Penghargaan",
|
||||
path: "/admin/desa/penghargaan"
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -130,7 +140,7 @@ export const devBar = [
|
||||
{
|
||||
id: "Kesehatan_1",
|
||||
name: "Posyandu",
|
||||
path: "/admin/kesehatan/posyandu"
|
||||
path: "/admin/kesehatan/posyandu/list-posyandu"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_2",
|
||||
@@ -549,6 +559,16 @@ export const navBar = [
|
||||
id: "Desa_7",
|
||||
name: "Penghargaan",
|
||||
path: "/admin/desa/penghargaan"
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -561,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",
|
||||
@@ -995,6 +1015,16 @@ export const role1 = [
|
||||
id: "Desa_7",
|
||||
name: "Penghargaan",
|
||||
path: "/admin/desa/penghargaan"
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -1183,7 +1213,7 @@ export const role1 = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "Kependudukan",
|
||||
name: "Kependudukan",
|
||||
path: "",
|
||||
@@ -1226,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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/create.ts
Normal file
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/create.ts
Normal 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;
|
||||
20
src/app/api/[[...slugs]]/_lib/desa/event-budaya/del.ts
Normal file
20
src/app/api/[[...slugs]]/_lib/desa/event-budaya/del.ts
Normal 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;
|
||||
46
src/app/api/[[...slugs]]/_lib/desa/event-budaya/find-many.ts
Normal file
46
src/app/api/[[...slugs]]/_lib/desa/event-budaya/find-many.ts
Normal 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;
|
||||
@@ -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;
|
||||
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/index.ts
Normal file
29
src/app/api/[[...slugs]]/_lib/desa/event-budaya/index.ts
Normal 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;
|
||||
31
src/app/api/[[...slugs]]/_lib/desa/event-budaya/updt.ts
Normal file
31
src/app/api/[[...slugs]]/_lib/desa/event-budaya/updt.ts
Normal 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;
|
||||
@@ -13,6 +13,9 @@ import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
|
||||
import MantanPerbekel from "./profile/profile-mantan-perbekel";
|
||||
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"] })
|
||||
@@ -30,6 +33,9 @@ const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
|
||||
.use(KategoriPengumuman)
|
||||
.use(AjukanPermohonan)
|
||||
.use(Musik)
|
||||
.use(KegiatanDesa)
|
||||
.use(KategoriKegiatan)
|
||||
.use(EventBudaya)
|
||||
|
||||
|
||||
export default Desa;
|
||||
|
||||
30
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/create.ts
Normal file
30
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/create.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaCreate(context: Context) {
|
||||
const body = context.body as any;
|
||||
|
||||
try {
|
||||
const data = await prisma.kegiatanDesa.create({
|
||||
data: {
|
||||
judul: body.judul,
|
||||
deskripsiSingkat: body.deskripsiSingkat,
|
||||
deskripsiLengkap: body.deskripsiLengkap,
|
||||
tanggal: new Date(body.tanggal),
|
||||
lokasi: body.lokasi,
|
||||
partisipan: Number(body.partisipan) || 0,
|
||||
kategoriKegiatanId: body.kategoriKegiatanId,
|
||||
imageId: body.imageId || null,
|
||||
},
|
||||
include: { kategoriKegiatan: true, image: true },
|
||||
});
|
||||
|
||||
return { success: true, message: "Kegiatan desa berhasil dibuat", data };
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaCreate:", e);
|
||||
return { success: false, message: "Gagal membuat kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaCreate;
|
||||
19
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/del.ts
Normal file
19
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/del.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaDelete(context: Context) {
|
||||
const { id } = context.params as { id: string };
|
||||
|
||||
try {
|
||||
await prisma.kegiatanDesa.update({
|
||||
where: { id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
return { success: true, message: "Kegiatan desa berhasil dihapus" };
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaDelete:", e);
|
||||
return { success: false, message: "Gagal menghapus kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaDelete;
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const kategori = (context.query.kategori as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ judul: { contains: search, mode: 'insensitive' } },
|
||||
{ lokasi: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
if (kategori) {
|
||||
where.kategoriKegiatan = {
|
||||
nama: { contains: kategori, mode: 'insensitive' },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.kegiatanDesa.findMany({
|
||||
where,
|
||||
include: {
|
||||
kategoriKegiatan: true,
|
||||
image: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { tanggal: 'asc' },
|
||||
}),
|
||||
prisma.kegiatanDesa.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil kegiatan desa",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaFindMany:", e);
|
||||
return { success: false, message: "Gagal mengambil data kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaFindMany;
|
||||
@@ -0,0 +1,35 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaFindUnique(context: Context) {
|
||||
const { id } = context.params as { id: string };
|
||||
|
||||
if (!id) {
|
||||
return { success: false, message: "ID is required" };
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await prisma.kegiatanDesa.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
kategoriKegiatan: true,
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return { success: false, message: "Data tidak ditemukan" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil kegiatan desa",
|
||||
data,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaFindUnique:", e);
|
||||
return { success: false, message: "Gagal mengambil data kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaFindUnique;
|
||||
37
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/index.ts
Normal file
37
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import kegiatanDesaFindMany from "./find-many";
|
||||
import kegiatanDesaFindUnique from "./findUnique";
|
||||
import kegiatanDesaCreate from "./create";
|
||||
import kegiatanDesaDelete from "./del";
|
||||
import kegiatanDesaUpdate from "./updt";
|
||||
|
||||
const KegiatanDesa = new Elysia({ prefix: "/kegiatandesa", tags: ["Desa/Kegiatan Desa"] })
|
||||
.get("/find-many", kegiatanDesaFindMany)
|
||||
.get("/:id", kegiatanDesaFindUnique)
|
||||
.post("/create", kegiatanDesaCreate, {
|
||||
body: t.Object({
|
||||
judul: t.String(),
|
||||
deskripsiSingkat: t.String(),
|
||||
deskripsiLengkap: t.String(),
|
||||
tanggal: t.String(),
|
||||
lokasi: t.String(),
|
||||
partisipan: t.Optional(t.Number()),
|
||||
kategoriKegiatanId: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.put("/:id", kegiatanDesaUpdate, {
|
||||
body: t.Object({
|
||||
judul: t.String(),
|
||||
deskripsiSingkat: t.String(),
|
||||
deskripsiLengkap: t.String(),
|
||||
tanggal: t.String(),
|
||||
lokasi: t.String(),
|
||||
partisipan: t.Optional(t.Number()),
|
||||
kategoriKegiatanId: t.String(),
|
||||
imageId: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", kegiatanDesaDelete);
|
||||
|
||||
export default KegiatanDesa;
|
||||
@@ -0,0 +1,26 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
nama: string;
|
||||
}
|
||||
|
||||
export default async function kategoriKegiatanCreate(context: Context) {
|
||||
const body = (await context.body) as FormCreate;
|
||||
|
||||
try {
|
||||
const result = await prisma.kategoriKegiatan.create({
|
||||
data: {
|
||||
nama: body.nama,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil membuat kategori kegiatan",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating kategori kegiatan:", error);
|
||||
throw new Error("Gagal membuat kategori kegiatan: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function kategoriKegiatanDelete(context: Context) {
|
||||
try {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "ID tidak boleh kosong",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// ✅ Cek apakah kategori masih digunakan oleh kegiatan
|
||||
const kegiatanCount = await prisma.kegiatanDesa.count({
|
||||
where: {
|
||||
kategoriKegiatanId: id,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (kegiatanCount > 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: `Kategori tidak dapat dihapus karena masih digunakan oleh ${kegiatanCount} kegiatan`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// ✅ Soft delete (bukan hard delete)
|
||||
await prisma.kategoriKegiatan.update({
|
||||
where: { id },
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Kategori kegiatan berhasil dihapus",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Delete kategori error:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kategoriKegiatanFindMany(context: Context) {
|
||||
// Ambil parameter dari query
|
||||
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;
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// Ambil data dan total count secara paralel
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.kategoriKegiatan.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
prisma.kategoriKegiatan.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil kategori kegiatan dengan pagination",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error di findMany paginated:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data kategori kegiatan",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default kategoriKegiatanFindMany;
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function kategoriKegiatanFindUnique(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const pathSegments = url.pathname.split('/');
|
||||
const id = pathSegments[pathSegments.length - 1];
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof id !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: "ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.kategoriKegiatan.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Data not found",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success get kategori kegiatan",
|
||||
data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Find by ID error:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import kategoriKegiatanCreate from "./create";
|
||||
import kategoriKegiatanDelete from "./del";
|
||||
import kategoriKegiatanFindMany from "./findMany";
|
||||
import kategoriKegiatanFindUnique from "./findUnique";
|
||||
import kategoriKegiatanUpdate from "./updt";
|
||||
|
||||
const KategoriKegiatan = new Elysia({
|
||||
prefix: "/kategorikegiatan",
|
||||
tags: ["Desa / Kegiatan Desa / Kategori Kegiatan"],
|
||||
})
|
||||
|
||||
.post("/create", kategoriKegiatanCreate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
}),
|
||||
})
|
||||
|
||||
.get("/findMany", kategoriKegiatanFindMany)
|
||||
.get("/:id", async (context) => {
|
||||
const response = await kategoriKegiatanFindUnique(
|
||||
new Request(context.request)
|
||||
);
|
||||
return response;
|
||||
})
|
||||
.put("/:id", kategoriKegiatanUpdate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
}),
|
||||
})
|
||||
.delete("/del/:id", kategoriKegiatanDelete);
|
||||
|
||||
export default KategoriKegiatan;
|
||||
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdate = {
|
||||
nama: string;
|
||||
}
|
||||
|
||||
export default async function kategoriKegiatanUpdate(context: Context) {
|
||||
const body = (await context.body) as FormUpdate;
|
||||
const id = context.params.id as string;
|
||||
|
||||
try {
|
||||
const result = await prisma.kategoriKegiatan.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nama: body.nama,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil mengupdate kategori kegiatan",
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori kegiatan:", error);
|
||||
throw new Error("Gagal mengupdate kategori kegiatan: " + (error as Error).message);
|
||||
}
|
||||
}
|
||||
32
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/updt.ts
Normal file
32
src/app/api/[[...slugs]]/_lib/desa/kegiatan-desa/updt.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function kegiatanDesaUpdate(context: Context) {
|
||||
const { id } = context.params as { id: string };
|
||||
const body = context.body as any;
|
||||
|
||||
try {
|
||||
const data = await prisma.kegiatanDesa.update({
|
||||
where: { id },
|
||||
data: {
|
||||
judul: body.judul,
|
||||
deskripsiSingkat: body.deskripsiSingkat,
|
||||
deskripsiLengkap: body.deskripsiLengkap,
|
||||
tanggal: new Date(body.tanggal),
|
||||
lokasi: body.lokasi,
|
||||
partisipan: Number(body.partisipan) || 0,
|
||||
kategoriKegiatanId: body.kategoriKegiatanId,
|
||||
imageId: body.imageId || null,
|
||||
},
|
||||
include: { kategoriKegiatan: true, image: true },
|
||||
});
|
||||
|
||||
return { success: true, message: "Kegiatan desa berhasil diupdate", data };
|
||||
} catch (e) {
|
||||
console.error("Error di kegiatanDesaUpdate:", e);
|
||||
return { success: false, message: "Gagal mengupdate kegiatan desa" };
|
||||
}
|
||||
}
|
||||
|
||||
export default kegiatanDesaUpdate;
|
||||
47
src/app/api/[[...slugs]]/_lib/kesehatan/balita/create.ts
Normal file
47
src/app/api/[[...slugs]]/_lib/kesehatan/balita/create.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { JenisKelaminBalita, StatusStunting } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
nama: string;
|
||||
nik?: string;
|
||||
tanggalLahir: string;
|
||||
jenisKelamin: JenisKelaminBalita;
|
||||
beratBadanKg?: number;
|
||||
tinggiBadanCm?: number;
|
||||
namaOrtu?: string;
|
||||
alamat?: string;
|
||||
noHpOrtu?: string;
|
||||
posyanduId?: string;
|
||||
imunisasiLengkap: boolean;
|
||||
giziBaik: boolean;
|
||||
pemeriksaanRutin: boolean;
|
||||
statusStunting: StatusStunting;
|
||||
catatan?: string;
|
||||
};
|
||||
|
||||
export default async function balitaCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
const data = await prisma.balita.create({
|
||||
data: {
|
||||
nama: body.nama,
|
||||
nik: body.nik,
|
||||
tanggalLahir: new Date(body.tanggalLahir),
|
||||
jenisKelamin: body.jenisKelamin,
|
||||
beratBadanKg: body.beratBadanKg,
|
||||
tinggiBadanCm: body.tinggiBadanCm,
|
||||
namaOrtu: body.namaOrtu,
|
||||
alamat: body.alamat,
|
||||
noHpOrtu: body.noHpOrtu,
|
||||
posyanduId: body.posyanduId || null,
|
||||
imunisasiLengkap: body.imunisasiLengkap,
|
||||
giziBaik: body.giziBaik,
|
||||
pemeriksaanRutin: body.pemeriksaanRutin,
|
||||
statusStunting: body.statusStunting ?? "NORMAL",
|
||||
catatan: body.catatan,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, message: "Balita berhasil ditambahkan", data };
|
||||
}
|
||||
22
src/app/api/[[...slugs]]/_lib/kesehatan/balita/del.ts
Normal file
22
src/app/api/[[...slugs]]/_lib/kesehatan/balita/del.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function balitaDelete(context: Context) {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return { success: false, message: "ID tidak diberikan" };
|
||||
}
|
||||
|
||||
const existing = await prisma.balita.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.balita.update({ where: { id }, data: { isActive: false } });
|
||||
|
||||
return { success: true, message: "Balita berhasil dihapus" };
|
||||
}
|
||||
38
src/app/api/[[...slugs]]/_lib/kesehatan/balita/find-by-id.ts
Normal file
38
src/app/api/[[...slugs]]/_lib/kesehatan/balita/find-by-id.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function balitaFindById(context: Context) {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await prisma.balita.findUnique({
|
||||
where: { id },
|
||||
include: { posyandu: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true, data }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("balitaFindById error:", error);
|
||||
return new Response(JSON.stringify({ success: false, message: "Gagal mengambil data" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
52
src/app/api/[[...slugs]]/_lib/kesehatan/balita/find-many.ts
Normal file
52
src/app/api/[[...slugs]]/_lib/kesehatan/balita/find-many.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function balitaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const statusStunting = (context.query.statusStunting as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ nik: { contains: search, mode: "insensitive" } },
|
||||
{ namaOrtu: { contains: search, mode: "insensitive" } },
|
||||
{ alamat: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
if (statusStunting) {
|
||||
where.statusStunting = statusStunting;
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.balita.findMany({
|
||||
where,
|
||||
include: { posyandu: { select: { id: true, name: true } } },
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.balita.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil data balita",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error balitaFindMany:", e);
|
||||
return { success: false, message: "Gagal mengambil data balita" };
|
||||
}
|
||||
}
|
||||
63
src/app/api/[[...slugs]]/_lib/kesehatan/balita/index.ts
Normal file
63
src/app/api/[[...slugs]]/_lib/kesehatan/balita/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import balitaCreate from "./create";
|
||||
import balitaDelete from "./del";
|
||||
import balitaFindById from "./find-by-id";
|
||||
import balitaFindMany from "./find-many";
|
||||
import balitaUpdate from "./updt";
|
||||
|
||||
const Balita = new Elysia({ prefix: "/balita", tags: ["Kesehatan/Balita"] })
|
||||
.post("/create", balitaCreate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
nik: t.Optional(t.String()),
|
||||
tanggalLahir: t.String(),
|
||||
jenisKelamin: t.Union([t.Literal("L"), t.Literal("P")]),
|
||||
beratBadanKg: t.Optional(t.Number()),
|
||||
tinggiBadanCm: t.Optional(t.Number()),
|
||||
namaOrtu: t.Optional(t.String()),
|
||||
alamat: t.Optional(t.String()),
|
||||
noHpOrtu: t.Optional(t.String()),
|
||||
posyanduId: t.Optional(t.String()),
|
||||
imunisasiLengkap: t.Boolean(),
|
||||
giziBaik: t.Boolean(),
|
||||
pemeriksaanRutin: t.Boolean(),
|
||||
statusStunting: t.Union([
|
||||
t.Literal("NORMAL"),
|
||||
t.Literal("ALERT"),
|
||||
t.Literal("STUNTING"),
|
||||
]),
|
||||
catatan: t.Optional(t.String()),
|
||||
}),
|
||||
})
|
||||
.get("/find-many", balitaFindMany)
|
||||
.delete("/del/:id", balitaDelete)
|
||||
.get("/:id", balitaFindById)
|
||||
.put(
|
||||
"/:id",
|
||||
balitaUpdate,
|
||||
{
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
nik: t.Optional(t.String()),
|
||||
tanggalLahir: t.String(),
|
||||
jenisKelamin: t.Union([t.Literal("L"), t.Literal("P")]),
|
||||
beratBadanKg: t.Optional(t.Number()),
|
||||
tinggiBadanCm: t.Optional(t.Number()),
|
||||
namaOrtu: t.Optional(t.String()),
|
||||
alamat: t.Optional(t.String()),
|
||||
noHpOrtu: t.Optional(t.String()),
|
||||
posyanduId: t.Optional(t.String()),
|
||||
imunisasiLengkap: t.Boolean(),
|
||||
giziBaik: t.Boolean(),
|
||||
pemeriksaanRutin: t.Boolean(),
|
||||
statusStunting: t.Union([
|
||||
t.Literal("NORMAL"),
|
||||
t.Literal("ALERT"),
|
||||
t.Literal("STUNTING"),
|
||||
]),
|
||||
catatan: t.Optional(t.String()),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export default Balita;
|
||||
75
src/app/api/[[...slugs]]/_lib/kesehatan/balita/updt.ts
Normal file
75
src/app/api/[[...slugs]]/_lib/kesehatan/balita/updt.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { JenisKelaminBalita, StatusStunting } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdate = {
|
||||
nama: string;
|
||||
nik?: string;
|
||||
tanggalLahir: string;
|
||||
jenisKelamin: JenisKelaminBalita;
|
||||
beratBadanKg?: number;
|
||||
tinggiBadanCm?: number;
|
||||
namaOrtu?: string;
|
||||
alamat?: string;
|
||||
noHpOrtu?: string;
|
||||
posyanduId?: string;
|
||||
imunisasiLengkap: boolean;
|
||||
giziBaik: boolean;
|
||||
pemeriksaanRutin: boolean;
|
||||
statusStunting: StatusStunting;
|
||||
catatan?: string;
|
||||
};
|
||||
|
||||
export default async function balitaUpdate(context: Context) {
|
||||
const id = context.params?.id as string;
|
||||
const body = context.body as FormUpdate;
|
||||
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await prisma.balita.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await prisma.balita.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nama: body.nama,
|
||||
nik: body.nik,
|
||||
tanggalLahir: new Date(body.tanggalLahir),
|
||||
jenisKelamin: body.jenisKelamin,
|
||||
beratBadanKg: body.beratBadanKg,
|
||||
tinggiBadanCm: body.tinggiBadanCm,
|
||||
namaOrtu: body.namaOrtu,
|
||||
alamat: body.alamat,
|
||||
noHpOrtu: body.noHpOrtu,
|
||||
posyanduId: body.posyanduId || null,
|
||||
imunisasiLengkap: body.imunisasiLengkap,
|
||||
giziBaik: body.giziBaik,
|
||||
pemeriksaanRutin: body.pemeriksaanRutin,
|
||||
statusStunting: body.statusStunting,
|
||||
catatan: body.catatan,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: "Balita berhasil diperbarui", data: updated }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("balitaUpdate error:", error);
|
||||
return new Response(JSON.stringify({ success: false, message: "Gagal memperbarui data" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
37
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/create.ts
Normal file
37
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/create.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { IbuHamilStatus } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = {
|
||||
nama: string;
|
||||
nik?: string;
|
||||
usiaKehamilan: number;
|
||||
hpht?: string;
|
||||
taksiranLahir?: string;
|
||||
alamat?: string;
|
||||
noHp?: string;
|
||||
catatan?: string;
|
||||
posyanduId?: string;
|
||||
status: IbuHamilStatus;
|
||||
};
|
||||
|
||||
export default async function ibuHamilCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
const data = await prisma.ibuHamil.create({
|
||||
data: {
|
||||
nama: body.nama,
|
||||
nik: body.nik,
|
||||
usiaKehamilan: body.usiaKehamilan ?? 0,
|
||||
hpht: body.hpht ? new Date(body.hpht) : undefined,
|
||||
taksiranLahir: body.taksiranLahir ? new Date(body.taksiranLahir) : undefined,
|
||||
alamat: body.alamat,
|
||||
noHp: body.noHp,
|
||||
catatan: body.catatan,
|
||||
posyanduId: body.posyanduId || null,
|
||||
status: body.status ?? "AKTIF",
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, message: "Ibu hamil berhasil ditambahkan", data };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user