Compare commits
22 Commits
tasks/ai-c
...
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 |
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:
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
- [x] Fix `KegiatanCard.tsx` image rendering logic
|
||||
- [x] Bump version in `package.json` to 0.1.48
|
||||
- [x] Verify build successful
|
||||
- [ ] Commit changes
|
||||
- [ ] Create branch and push
|
||||
- [ ] Merge to `stg`
|
||||
- [x] Commit changes
|
||||
- [x] Create branch and push
|
||||
- [x] Merge to `stg`
|
||||
|
||||
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.48",
|
||||
"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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
@@ -7,17 +7,8 @@ export async function seedRingkasanKesehatan() {
|
||||
|
||||
await prisma.ringkasanKesehatanDesa.upsert({
|
||||
where: { id: SINGLETON_ID },
|
||||
update: {
|
||||
ibuHamilAkh: 87,
|
||||
balitaTerdaftar: 342,
|
||||
alertStunting: 12,
|
||||
},
|
||||
create: {
|
||||
id: SINGLETON_ID,
|
||||
ibuHamilAkh: 87,
|
||||
balitaTerdaftar: 342,
|
||||
alertStunting: 12,
|
||||
},
|
||||
update: { targetStuntingPct: 10 },
|
||||
create: { id: SINGLETON_ID, targetStuntingPct: 10 },
|
||||
});
|
||||
|
||||
console.log("✅ Ringkasan Kesehatan Desa seeded");
|
||||
|
||||
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."
|
||||
}
|
||||
]
|
||||
@@ -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,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())
|
||||
@@ -2481,14 +2483,85 @@ model BeasiswaConfig {
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
|
||||
model RingkasanKesehatanDesa {
|
||||
id String @id @default(cuid())
|
||||
ibuHamilAkh Int @default(0)
|
||||
balitaTerdaftar Int @default(0)
|
||||
alertStunting Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
// ========================================= IBU HAMIL ========================================= //
|
||||
enum IbuHamilStatus {
|
||||
AKTIF
|
||||
MELAHIRKAN
|
||||
KEGUGURAN
|
||||
NONAKTIF
|
||||
}
|
||||
|
||||
model IbuHamil {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
nik String?
|
||||
usiaKehamilan Int @default(0)
|
||||
hpht DateTime?
|
||||
taksiranLahir DateTime?
|
||||
alamat String?
|
||||
noHp String?
|
||||
catatan String?
|
||||
posyanduId String?
|
||||
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
||||
status IbuHamilStatus @default(AKTIF)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= BALITA ========================================= //
|
||||
enum JenisKelaminBalita {
|
||||
L
|
||||
P
|
||||
}
|
||||
|
||||
enum StatusStunting {
|
||||
NORMAL
|
||||
ALERT
|
||||
STUNTING
|
||||
}
|
||||
|
||||
model Balita {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
nik String?
|
||||
tanggalLahir DateTime
|
||||
jenisKelamin JenisKelaminBalita
|
||||
beratBadanKg Float?
|
||||
tinggiBadanCm Float?
|
||||
namaOrtu String?
|
||||
alamat String?
|
||||
noHpOrtu String?
|
||||
posyanduId String?
|
||||
posyandu Posyandu? @relation(fields: [posyanduId], references: [id], onDelete: SetNull)
|
||||
imunisasiLengkap Boolean @default(false)
|
||||
giziBaik Boolean @default(true)
|
||||
pemeriksaanRutin Boolean @default(true)
|
||||
statusStunting StatusStunting @default(NORMAL)
|
||||
catatan String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= RINGKASAN KESEHATAN DESA ========================================= //
|
||||
model RingkasanKesehatanDesa {
|
||||
id String @id @default(cuid())
|
||||
targetStuntingPct Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= EVENT BUDAYA ========================================= //
|
||||
model EventBudaya {
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
tanggal DateTime
|
||||
lokasi String
|
||||
deskripsi String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { seedBerita } from "./_seeder_list/desa/berita/seed_berita";
|
||||
import { seedKegiatanDesa } from "./_seeder_list/desa/seed_kegiatan_desa";
|
||||
import { seedEventBudaya } from "./_seeder_list/desa/event-budaya/seed_event_budaya";
|
||||
import { seedFoto } from "./_seeder_list/desa/gallery/foto/seed_foto";
|
||||
import { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video";
|
||||
import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan";
|
||||
@@ -48,6 +49,8 @@ import { seedPuskesmas } from "./_seeder_list/kesehatan/puskesmas/seed_puskesmas
|
||||
import { seedGrafikKepuasan } from "./_seeder_list/kesehatan/seed_grafik_kepuasan";
|
||||
import { seedKelahiranKematian } from "./_seeder_list/kesehatan/seed_kelahiran_kematian";
|
||||
import { seedRingkasanKesehatan } from "./_seeder_list/kesehatan/seed_ringkasan_kesehatan";
|
||||
import { seedIbuHamil } from "./_seeder_list/kesehatan/seed_ibu_hamil";
|
||||
import { seedBalita } from "./_seeder_list/kesehatan/seed_balita";
|
||||
import { seedDesaAntiKorupsi } from "./_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi";
|
||||
import { seedPrestasiDesa } from "./_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa";
|
||||
import { seedMediaSosial } from "./_seeder_list/landing-page/profil_landing_page/seed_media_sosial";
|
||||
@@ -244,6 +247,10 @@ import seedAssets from "./seed_assets";
|
||||
// // ==================== SUBMENU POSYANDU =========================
|
||||
await seedPosyandu();
|
||||
|
||||
// // ==================== SUBMENU IBU HAMIL + BALITA =========================
|
||||
await seedIbuHamil();
|
||||
await seedBalita();
|
||||
|
||||
// // ==================== SUBMENU PUSKESMAS =========================
|
||||
await seedPuskesmas();
|
||||
|
||||
@@ -386,6 +393,7 @@ import seedAssets from "./seed_assets";
|
||||
// ===== SOSIAL DASHBOARD =====
|
||||
await seedRingkasanKesehatan();
|
||||
await seedKegiatanDesa();
|
||||
await seedEventBudaya();
|
||||
|
||||
// ===== DESA =====
|
||||
await seedMusikDesa();
|
||||
|
||||
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;
|
||||
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;
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -123,6 +123,11 @@ export const devBar = [
|
||||
id: "Desa_8",
|
||||
name: "Kegiatan Desa",
|
||||
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_9",
|
||||
name: "Event Budaya",
|
||||
path: "/admin/desa/event-budaya"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -135,7 +140,7 @@ export const devBar = [
|
||||
{
|
||||
id: "Kesehatan_1",
|
||||
name: "Posyandu",
|
||||
path: "/admin/kesehatan/posyandu"
|
||||
path: "/admin/kesehatan/posyandu/list-posyandu"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_2",
|
||||
@@ -559,6 +564,11 @@ export const navBar = [
|
||||
id: "Desa_8",
|
||||
name: "Kegiatan Desa",
|
||||
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_9",
|
||||
name: "Event Budaya",
|
||||
path: "/admin/desa/event-budaya"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -571,7 +581,7 @@ export const navBar = [
|
||||
{
|
||||
id: "Kesehatan_1",
|
||||
name: "Posyandu",
|
||||
path: "/admin/kesehatan/posyandu"
|
||||
path: "/admin/kesehatan/posyandu/list-posyandu/list_posyandu"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_2",
|
||||
@@ -1010,6 +1020,11 @@ export const role1 = [
|
||||
id: "Desa_8",
|
||||
name: "Kegiatan Desa",
|
||||
path: "/admin/desa/kegiatan-desa/list-kegiatan-desa"
|
||||
},
|
||||
{
|
||||
id: "Desa_9",
|
||||
name: "Event Budaya",
|
||||
path: "/admin/desa/event-budaya"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -1198,7 +1213,7 @@ export const role1 = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
id: "Kependudukan",
|
||||
name: "Kependudukan",
|
||||
path: "",
|
||||
@@ -1241,7 +1256,7 @@ export const role2 = [
|
||||
{
|
||||
id: "Kesehatan_1",
|
||||
name: "Posyandu",
|
||||
path: "/admin/kesehatan/posyandu"
|
||||
path: "/admin/kesehatan/posyandu/list-posyandu/list_posyandu"
|
||||
},
|
||||
{
|
||||
id: "Kesehatan_2",
|
||||
|
||||
@@ -37,7 +37,7 @@ import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { isDark } = useDarkMode();
|
||||
const tokens = themeTokens(isDark);
|
||||
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [opened, { toggle, close }] = useDisclosure();
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -114,7 +114,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
case 2:
|
||||
return '/admin/landing-page/profil/program-inovasi';
|
||||
case 3:
|
||||
return '/admin/kesehatan/posyandu';
|
||||
return '/admin/kesehatan/posyandu/list-posyandu';
|
||||
case 4:
|
||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
default:
|
||||
|
||||
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;
|
||||
@@ -15,6 +15,7 @@ import AjukanPermohonan from "./layanan/ajukan_permohonan";
|
||||
import Musik from "./musik";
|
||||
import KegiatanDesa from "./kegiatan-desa";
|
||||
import KategoriKegiatan from "./kegiatan-desa/kategori-kegiatan";
|
||||
import EventBudaya from "./event-budaya";
|
||||
|
||||
|
||||
const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
|
||||
@@ -34,6 +35,7 @@ const Desa = new Elysia({ prefix: "/desa", tags: ["Desa"] })
|
||||
.use(Musik)
|
||||
.use(KegiatanDesa)
|
||||
.use(KategoriKegiatan)
|
||||
.use(EventBudaya)
|
||||
|
||||
|
||||
export default Desa;
|
||||
|
||||
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 };
|
||||
}
|
||||
22
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/del.ts
Normal file
22
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/del.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function ibuHamilDelete(context: Context) {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return { success: false, message: "ID tidak diberikan" };
|
||||
}
|
||||
|
||||
const existing = await prisma.ibuHamil.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.ibuHamil.update({ where: { id }, data: { isActive: false } });
|
||||
|
||||
return { success: true, message: "Ibu hamil berhasil dihapus" };
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function ibuHamilFindById(context: Context) {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await prisma.ibuHamil.findUnique({
|
||||
where: { id },
|
||||
include: { posyandu: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true, data }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("ibuHamilFindById error:", error);
|
||||
return new Response(JSON.stringify({ success: false, message: "Gagal mengambil data" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function ibuHamilFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const status = (context.query.status as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ nik: { contains: search, mode: "insensitive" } },
|
||||
{ alamat: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.ibuHamil.findMany({
|
||||
where,
|
||||
include: { posyandu: { select: { id: true, name: true } } },
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.ibuHamil.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil ambil data ibu hamil",
|
||||
data,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error ibuHamilFindMany:", e);
|
||||
return { success: false, message: "Gagal mengambil data ibu hamil" };
|
||||
}
|
||||
}
|
||||
55
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/index.ts
Normal file
55
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import ibuHamilCreate from "./create";
|
||||
import ibuHamilDelete from "./del";
|
||||
import ibuHamilFindById from "./find-by-id";
|
||||
import ibuHamilFindMany from "./find-many";
|
||||
import ibuHamilUpdate from "./updt";
|
||||
|
||||
const IbuHamil = new Elysia({ prefix: "/ibuhamil", tags: ["Kesehatan/IbuHamil"] })
|
||||
.post("/create", ibuHamilCreate, {
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
nik: t.Optional(t.String()),
|
||||
usiaKehamilan: t.Number({ minimum: 0 }),
|
||||
hpht: t.Optional(t.String()),
|
||||
taksiranLahir: t.Optional(t.String()),
|
||||
alamat: t.Optional(t.String()),
|
||||
noHp: t.Optional(t.String()),
|
||||
catatan: t.Optional(t.String()),
|
||||
posyanduId: t.Optional(t.String()),
|
||||
status: t.Union([
|
||||
t.Literal("AKTIF"),
|
||||
t.Literal("MELAHIRKAN"),
|
||||
t.Literal("KEGUGURAN"),
|
||||
t.Literal("NONAKTIF"),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
.get("/find-many", ibuHamilFindMany)
|
||||
.delete("/del/:id", ibuHamilDelete)
|
||||
.get("/:id", ibuHamilFindById)
|
||||
.put(
|
||||
"/:id",
|
||||
ibuHamilUpdate,
|
||||
{
|
||||
body: t.Object({
|
||||
nama: t.String(),
|
||||
nik: t.Optional(t.String()),
|
||||
usiaKehamilan: t.Number({ minimum: 0 }),
|
||||
hpht: t.Optional(t.String()),
|
||||
taksiranLahir: t.Optional(t.String()),
|
||||
alamat: t.Optional(t.String()),
|
||||
noHp: t.Optional(t.String()),
|
||||
catatan: t.Optional(t.String()),
|
||||
posyanduId: t.Optional(t.String()),
|
||||
status: t.Union([
|
||||
t.Literal("AKTIF"),
|
||||
t.Literal("MELAHIRKAN"),
|
||||
t.Literal("KEGUGURAN"),
|
||||
t.Literal("NONAKTIF"),
|
||||
]),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export default IbuHamil;
|
||||
65
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/updt.ts
Normal file
65
src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/updt.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { IbuHamilStatus } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormUpdate = {
|
||||
nama: string;
|
||||
nik?: string;
|
||||
usiaKehamilan: number;
|
||||
hpht?: string;
|
||||
taksiranLahir?: string;
|
||||
alamat?: string;
|
||||
noHp?: string;
|
||||
catatan?: string;
|
||||
posyanduId?: string;
|
||||
status: IbuHamilStatus;
|
||||
};
|
||||
|
||||
export default async function ibuHamilUpdate(context: Context) {
|
||||
const id = context.params?.id as string;
|
||||
const body = context.body as FormUpdate;
|
||||
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ success: false, message: "ID tidak boleh kosong" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await prisma.ibuHamil.findUnique({ where: { id } });
|
||||
if (!existing) {
|
||||
return new Response(JSON.stringify({ success: false, message: "Data tidak ditemukan" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await prisma.ibuHamil.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nama: body.nama,
|
||||
nik: body.nik,
|
||||
usiaKehamilan: body.usiaKehamilan ?? 0,
|
||||
hpht: body.hpht ? new Date(body.hpht) : null,
|
||||
taksiranLahir: body.taksiranLahir ? new Date(body.taksiranLahir) : null,
|
||||
alamat: body.alamat,
|
||||
noHp: body.noHp,
|
||||
catatan: body.catatan,
|
||||
posyanduId: body.posyanduId || null,
|
||||
status: body.status,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: "Ibu hamil berhasil diperbarui", data: updated }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("ibuHamilUpdate error:", error);
|
||||
return new Response(JSON.stringify({ success: false, message: "Gagal memperbarui data" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter
|
||||
import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
|
||||
import TarifLayanan from "./data_kesehatan_warga/fasilitas_kesehatan/tarif-layanan";
|
||||
import RingkasanKesehatan from "./ringkasan-kesehatan";
|
||||
import IbuHamil from "./ibu-hamil";
|
||||
import Balita from "./balita";
|
||||
|
||||
|
||||
const Kesehatan = new Elysia({
|
||||
@@ -51,4 +53,6 @@ const Kesehatan = new Elysia({
|
||||
.use(TarifLayanan)
|
||||
.use(PendaftaranJadwalKegiatan)
|
||||
.use(RingkasanKesehatan)
|
||||
.use(IbuHamil)
|
||||
.use(Balita)
|
||||
export default Kesehatan;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import ringkasanKesehatanFindUnique from "./findUnique";
|
||||
import ringkasanKesehatanUpdate from "./updt";
|
||||
import ringkasanKesehatanStats from "./stats";
|
||||
|
||||
const RingkasanKesehatan = new Elysia({ prefix: "/ringkasankesehatan", tags: ["Kesehatan/Ringkasan"] })
|
||||
.get("/find", ringkasanKesehatanFindUnique)
|
||||
.get("/stats", ringkasanKesehatanStats)
|
||||
.put("/update", ringkasanKesehatanUpdate, {
|
||||
body: t.Object({
|
||||
ibuHamilAkh: t.Number(),
|
||||
balitaTerdaftar: t.Number(),
|
||||
alertStunting: t.Number(),
|
||||
targetStuntingPct: t.Number({ minimum: 0, maximum: 100 }),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
type StatsResult = {
|
||||
ibuHamilAktif: number;
|
||||
balitaTerdaftar: number;
|
||||
alertStunting: number;
|
||||
imunisasiLengkapPct: number;
|
||||
pemeriksaanRutinPct: number;
|
||||
giziBaikPct: number;
|
||||
targetStuntingPct: number;
|
||||
};
|
||||
|
||||
export default async function ringkasanKesehatanStats(): Promise<{ success: boolean; data?: StatsResult; message?: string }> {
|
||||
try {
|
||||
const [
|
||||
ibuHamilAktif,
|
||||
balitaTotal,
|
||||
alertStunting,
|
||||
imunisasiLengkap,
|
||||
pemeriksaanRutin,
|
||||
giziBaik,
|
||||
config,
|
||||
] = await Promise.all([
|
||||
prisma.ibuHamil.count({ where: { status: "AKTIF", isActive: true } }),
|
||||
prisma.balita.count({ where: { isActive: true } }),
|
||||
prisma.balita.count({ where: { isActive: true, statusStunting: { in: ["ALERT", "STUNTING"] } } }),
|
||||
prisma.balita.count({ where: { isActive: true, imunisasiLengkap: true } }),
|
||||
prisma.balita.count({ where: { isActive: true, pemeriksaanRutin: true } }),
|
||||
prisma.balita.count({ where: { isActive: true, giziBaik: true } }),
|
||||
prisma.ringkasanKesehatanDesa.findFirst({ where: { isActive: true }, orderBy: { createdAt: "desc" } }),
|
||||
]);
|
||||
|
||||
const pct = (n: number, total: number) =>
|
||||
total === 0 ? 0 : Math.round((n / total) * 100);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
ibuHamilAktif,
|
||||
balitaTerdaftar: balitaTotal,
|
||||
alertStunting,
|
||||
imunisasiLengkapPct: pct(imunisasiLengkap, balitaTotal),
|
||||
pemeriksaanRutinPct: pct(pemeriksaanRutin, balitaTotal),
|
||||
giziBaikPct: pct(giziBaik, balitaTotal),
|
||||
targetStuntingPct: config?.targetStuntingPct ?? 0,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("ringkasanKesehatanStats error:", e);
|
||||
return { success: false, message: "Gagal menghitung statistik kesehatan" };
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,28 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function ringkasanKesehatanUpdate(context: Context) {
|
||||
const body = context.body as any;
|
||||
const body = context.body as { targetStuntingPct: number };
|
||||
|
||||
try {
|
||||
const existing = await prisma.ringkasanKesehatanDesa.findFirst({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
const data = existing
|
||||
? await prisma.ringkasanKesehatanDesa.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
ibuHamilAkh: Number(body.ibuHamilAkh),
|
||||
balitaTerdaftar: Number(body.balitaTerdaftar),
|
||||
alertStunting: Number(body.alertStunting),
|
||||
},
|
||||
data: { targetStuntingPct: Number(body.targetStuntingPct) },
|
||||
})
|
||||
: await prisma.ringkasanKesehatanDesa.create({
|
||||
data: {
|
||||
ibuHamilAkh: Number(body.ibuHamilAkh),
|
||||
balitaTerdaftar: Number(body.balitaTerdaftar),
|
||||
alertStunting: Number(body.alertStunting),
|
||||
},
|
||||
data: { targetStuntingPct: Number(body.targetStuntingPct) },
|
||||
});
|
||||
|
||||
return { success: true, message: "Ringkasan kesehatan berhasil disimpan", data };
|
||||
return { success: true, message: "Target stunting berhasil disimpan", data };
|
||||
} catch (e) {
|
||||
console.error("Error di ringkasanKesehatanUpdate:", e);
|
||||
return { success: false, message: "Gagal menyimpan ringkasan kesehatan" };
|
||||
return { success: false, message: "Gagal menyimpan target stunting" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Elysia from "elysia";
|
||||
import BeasiswaPendaftar from "./beasiswa-pendaftar";
|
||||
import KeunggulanProgram from "./keunggulan-program";
|
||||
import BeasiswaConfig from "./beasiswa-config";
|
||||
import RingkasanBeasiswa from "./ringkasan";
|
||||
|
||||
const Beasiswa = new Elysia({
|
||||
prefix: "/beasiswa",
|
||||
@@ -10,5 +11,6 @@ const Beasiswa = new Elysia({
|
||||
.use(BeasiswaPendaftar)
|
||||
.use(KeunggulanProgram)
|
||||
.use(BeasiswaConfig)
|
||||
.use(RingkasanBeasiswa)
|
||||
|
||||
export default Beasiswa
|
||||
@@ -0,0 +1,9 @@
|
||||
import Elysia from "elysia";
|
||||
import beasiswaRingkasanStats from "./stats";
|
||||
|
||||
const RingkasanBeasiswa = new Elysia({
|
||||
prefix: "/ringkasan",
|
||||
tags: ["Pendidikan/Beasiswa Desa"],
|
||||
}).get("/stats", beasiswaRingkasanStats);
|
||||
|
||||
export default RingkasanBeasiswa;
|
||||
@@ -0,0 +1,35 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
type StatsResult = {
|
||||
jumlahPenerima: number;
|
||||
danaTersalurkan: string;
|
||||
tahunAjaran: string;
|
||||
};
|
||||
|
||||
export default async function beasiswaRingkasanStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: StatsResult;
|
||||
message?: string;
|
||||
}> {
|
||||
try {
|
||||
const [jumlahPenerima, config] = await Promise.all([
|
||||
prisma.beasiswaPendaftar.count({ where: { isActive: true } }),
|
||||
prisma.beasiswaConfig.findFirst({
|
||||
where: { isActive: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
jumlahPenerima,
|
||||
danaTersalurkan: (config?.danaTersalurkan ?? BigInt(0)).toString(),
|
||||
tahunAjaran: config?.tahunAjaran ?? "-",
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("beasiswaRingkasanStats error:", e);
|
||||
return { success: false, message: "Gagal menghitung statistik beasiswa" };
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import PendidikanNonFormal from "./pendidikan-non-formal";
|
||||
import DataPendidikan from "./data-pendidikan";
|
||||
import Beasiswa from "./beasiswa-desa";
|
||||
import PerpustakaanDigital from "./perpustakaan-digital";
|
||||
import RingkasanPendidikan from "./ringkasan";
|
||||
|
||||
const Pendidikan = new Elysia({
|
||||
prefix: "/pendidikan",
|
||||
@@ -19,5 +20,6 @@ const Pendidikan = new Elysia({
|
||||
.use(DataPendidikan)
|
||||
.use(Beasiswa)
|
||||
.use(PerpustakaanDigital)
|
||||
.use(RingkasanPendidikan)
|
||||
|
||||
export default Pendidikan;
|
||||
@@ -0,0 +1,9 @@
|
||||
import Elysia from "elysia";
|
||||
import pendidikanRingkasanStats from "./stats";
|
||||
|
||||
const RingkasanPendidikan = new Elysia({
|
||||
prefix: "/ringkasan",
|
||||
tags: ["Pendidikan/Ringkasan"],
|
||||
}).get("/stats", pendidikanRingkasanStats);
|
||||
|
||||
export default RingkasanPendidikan;
|
||||
46
src/app/api/[[...slugs]]/_lib/pendidikan/ringkasan/stats.ts
Normal file
46
src/app/api/[[...slugs]]/_lib/pendidikan/ringkasan/stats.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
type PerJenjang = { nama: string; jumlahSiswa: number };
|
||||
|
||||
type StatsResult = {
|
||||
perJenjang: PerJenjang[];
|
||||
jumlahLembaga: number;
|
||||
jumlahPengajar: number;
|
||||
};
|
||||
|
||||
export default async function pendidikanRingkasanStats(): Promise<{
|
||||
success: boolean;
|
||||
data?: StatsResult;
|
||||
message?: string;
|
||||
}> {
|
||||
try {
|
||||
const [jenjangList, jumlahLembaga, jumlahPengajar] = await Promise.all([
|
||||
prisma.jenjangPendidikan.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
lembagas: {
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
_count: { select: { siswa: { where: { isActive: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.lembaga.count({ where: { isActive: true } }),
|
||||
prisma.pengajar.count({ where: { isActive: true } }),
|
||||
]);
|
||||
|
||||
const perJenjang = jenjangList.map((j) => ({
|
||||
nama: j.nama,
|
||||
jumlahSiswa: j.lembagas.reduce((acc, l) => acc + l._count.siswa, 0),
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { perJenjang, jumlahLembaga, jumlahPengajar },
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("pendidikanRingkasanStats error:", e);
|
||||
return { success: false, message: "Gagal menghitung statistik pendidikan" };
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export default function WaitingRoom() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
|
||||
// ⏱️ Countdown timer
|
||||
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
|
||||
const [hasTimedOut, setHasTimedOut] = useState(false);
|
||||
@@ -128,7 +128,7 @@ export default function WaitingRoom() {
|
||||
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||
break;
|
||||
case "3":
|
||||
redirectPath = '/admin/kesehatan/posyandu';
|
||||
redirectPath = '/admin/kesehatan/posyandu/list-posyandu';
|
||||
break;
|
||||
case "4":
|
||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
@@ -200,9 +200,9 @@ export default function WaitingRoom() {
|
||||
Silakan hubungi Superadmin atau coba login ulang nanti.
|
||||
</Text>
|
||||
<Group gap="sm" w="100%">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Kembali ke Login
|
||||
@@ -243,11 +243,11 @@ export default function WaitingRoom() {
|
||||
<Title order={2} c={colors['blue-button']} ta="center">
|
||||
⏳ Menunggu Persetujuan
|
||||
</Title>
|
||||
|
||||
|
||||
<Text ta="center" c="dimmed">
|
||||
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||
</Text>
|
||||
|
||||
|
||||
<Text ta="center" size="sm" fw={500}>
|
||||
Nomor: {user?.nomor || '...'}
|
||||
</Text>
|
||||
@@ -260,8 +260,8 @@ export default function WaitingRoom() {
|
||||
{formatTime(timeLeft)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={progressValue}
|
||||
<Progress
|
||||
value={progressValue}
|
||||
color={timeLeft < 60 ? 'red' : colors['blue-button']}
|
||||
size="sm"
|
||||
animated
|
||||
@@ -269,15 +269,15 @@ export default function WaitingRoom() {
|
||||
</Stack>
|
||||
|
||||
<Loader size="sm" color={colors['blue-button']} />
|
||||
|
||||
|
||||
<Text ta="center" size="xs" c="dimmed">
|
||||
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||
</Text>
|
||||
|
||||
{/* 🚪 Tombol Keluar */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={handleLogout}
|
||||
c="dimmed"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user