feat(kesehatan): posyandu banjar relation, redesign halaman publik, fix tips keamanan image
- Tambah model Banjar + relasi ke Posyandu (migration + seeder) - Update API posyandu (create/update/find) untuk support banjarId - Tambah endpoint banjar di kesehatan API - Redesign halaman publik posyandu dengan tabs: ringkasan, data posyandu, balita, ibu hamil - Update halaman admin posyandu list/create/edit/detail untuk banjar - Fix image ketukar pada seed tips keamanan - Hapus seeder core yang sudah tidak dipakai Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ bun eslint . --fix
|
||||
- Architecture, request flow, domain modules, key files: @.claude/ARCHITECTURE.md
|
||||
- Database conventions, auth flow, file handling: @.claude/DATABASE.md
|
||||
- Env vars, Docker, CI/CD, releasing: @.claude/DEPLOYMENT.md
|
||||
- UI/UX design system, tokens, komponen, pola halaman: @.claude/DESIGN.md
|
||||
- AI collaboration contract, rules, and guidelines: @AI-CONTRACT.md
|
||||
|
||||
### Workflow for Code Changes
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Summary: Posyandu Banjar + Halaman Publik + Fix Tips Keamanan
|
||||
|
||||
**Tanggal:** 2026-05-28
|
||||
**Branch:** tasks/kesehatan/posyandu-banjar-publik-fix-tips-keamanan/20260528
|
||||
|
||||
---
|
||||
|
||||
## Apa yang Berubah
|
||||
|
||||
### 1. Fitur Banjar pada Posyandu
|
||||
|
||||
**Problem:** Posyandu tidak memiliki relasi ke wilayah banjar, sehingga tidak bisa dikelompokkan per banjar.
|
||||
|
||||
**Perubahan:**
|
||||
- `prisma/schema.prisma` — Tambah model `Banjar` baru dan field `banjarId` (optional FK) pada model `Posyandu`
|
||||
- `prisma/migrations/20260528100000_add_banjar_to_posyandu/migration.sql` — Migration: CREATE TABLE `Banjar`, ALTER TABLE `Posyandu` ADD COLUMN `banjarId`
|
||||
- `prisma/_seeder_list/kesehatan/seed_banjar.ts` — Seeder baru untuk data banjar
|
||||
- `prisma/data/kesehatan/banjar/banjar.json` — Data seed 16 banjar Desa Darmasaba
|
||||
- `prisma/seed.ts` — Tambah `seedBanjar()` sebelum `seedTipsKeamanan()`
|
||||
|
||||
**API changes:**
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/create.ts` — Terima `banjarId` optional saat create
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/updt.ts` — Terima `banjarId` optional saat update
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/find-by-id.ts` — Include relasi `banjar`
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/find-many.ts` — Include relasi `banjar`
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/index.ts` — Tambah endpoint banjar
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/index.ts` — Daftarkan endpoint banjar baru
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/banjar/` — Module API baru untuk list banjar
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/balita/find-many.ts` — Support filter per posyandu
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/ibu-hamil/find-many.ts` — Support filter per posyandu
|
||||
|
||||
**Admin CMS changes:**
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/list-posyandu/page.tsx` — Tampilkan info banjar di tabel
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/list-posyandu/create/page.tsx` — Tambah Select banjar
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/list-posyandu/[id]/edit/page.tsx` — Tambah Select banjar
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/list-posyandu/[id]/page.tsx` — Tampilkan nama banjar di detail
|
||||
- `src/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu.ts` — Include `banjar` di state
|
||||
|
||||
---
|
||||
|
||||
### 2. Redesign Halaman Publik Posyandu
|
||||
|
||||
**Problem:** Halaman publik `/darmasaba/kesehatan/posyandu` hanya menampilkan daftar posyandu saja, belum menampilkan data balita, ibu hamil, dan ringkasan statistik.
|
||||
|
||||
**Perubahan:**
|
||||
- `src/app/darmasaba/(pages)/kesehatan/posyandu/page.tsx` — Redesign total dengan sistem Tab:
|
||||
- **Tab Ringkasan** — Statistik kesehatan (total posyandu, balita, ibu hamil, angka stunting)
|
||||
- **Tab Data Posyandu** — List posyandu dengan filter search dan info banjar
|
||||
- **Tab Data Balita** — Tabel data balita dengan filter search + filter status stunting
|
||||
- **Tab Ibu Hamil** — Tabel data ibu hamil dengan filter search + filter status
|
||||
- `src/app/darmasaba/(pages)/kesehatan/posyandu/[id]/page.tsx` — Halaman detail posyandu dengan tampilan tab balita & ibu hamil per posyandu
|
||||
- `src/app/admin/(dashboard)/_state/kesehatan/balita/balita.ts` — Tambah state `findMany` untuk halaman publik
|
||||
- `src/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil.ts` — Tambah state `findMany` untuk halaman publik
|
||||
- `src/app/admin/(dashboard)/_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan.ts` — Refactor state ringkasan
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/balita/page.tsx` — Update admin balita page
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/ibu-hamil/page.tsx` — Update admin ibu hamil page
|
||||
- `src/app/admin/(dashboard)/kesehatan/posyandu/ringkasan-kesehatan/page.tsx` — Update admin ringkasan
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/stats.ts` — Perbaiki kalkulasi statistik
|
||||
- `src/app/api/[[...slugs]]/_lib/kesehatan/ringkasan-kesehatan/index.ts` — Update endpoint
|
||||
|
||||
---
|
||||
|
||||
### 3. Fix Image Tips Keamanan (Ketuker)
|
||||
|
||||
**Problem:** Gambar pada data Tips Keamanan tertukar antara "Keamanan Rumah" dan "Keamanan Lingkungan Tanggungjawab Bersama".
|
||||
|
||||
**Root cause:** Nilai `imageName` di `tips-keamanan.json` salah assign — nama file gambar di-swap antara 2 record.
|
||||
|
||||
**Perubahan:**
|
||||
- `prisma/data/keamanan/tips-keamanan/tips-keamanan.json` — Tukar nilai `imageName`:
|
||||
- "Keamanan Rumah" → `vwZsaxcoFWDlxG1PW7FC0-mobile.webp` (sebelumnya `dSe0xyvNLkP2t2f6iq-Hk-mobile.webp`)
|
||||
- "Keamanan Lingkungan..." → `dSe0xyvNLkP2t2f6iq-Hk-mobile.webp` (sebelumnya `vwZsaxcoFWDlxG1PW7FC0-mobile.webp`)
|
||||
|
||||
**Catatan:** `imageId` akan null di lokal karena MinIO lokal tidak punya file tersebut. Di STG, MinIO sudah punya kedua file — seed akan resolve dengan benar setelah deploy.
|
||||
|
||||
---
|
||||
|
||||
### 4. Cleanup Seed Core
|
||||
|
||||
- `prisma/_seeder_list/core/seed_app_menu.ts` — Dihapus (sudah tidak dipakai)
|
||||
- `prisma/_seeder_list/core/seed_core.ts` — Dihapus (sudah tidak dipakai)
|
||||
- `prisma/seed.ts` — Hapus import + call ke seed core, tambah `seedBanjar` dan `seedTipsKeamanan`
|
||||
|
||||
---
|
||||
|
||||
### 5. Update Seed PPID & Data
|
||||
|
||||
- `prisma/data/ppid/struktur-organisasi-ppid/struktur-organisasi-ppid.json` — Update data struktur organisasi PPID
|
||||
- `prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts` — Update seeder posyandu untuk include `banjarId`
|
||||
- `prisma/data/kesehatan/posyandu/posyandu.json` — Update data seed posyandu dengan `banjarId`
|
||||
|
||||
---
|
||||
|
||||
## Catatan Penting
|
||||
|
||||
- **Migration wajib dijalankan** saat deploy: `prisma migrate deploy` sudah otomatis via `docker-entrypoint.sh`
|
||||
- **Seed harus dijalankan ulang** di STG setelah deploy agar data banjar terisi dan imageId tips keamanan terkoreksi
|
||||
- Gambar tips keamanan akan tetap null di lokal (MinIO lokal tidak punya file), tapi akan resolve di STG
|
||||
@@ -1,57 +0,0 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const appMenuJson = loadJsonData("core/app-menu.json");
|
||||
const appMenuChildJson = loadJsonData("core/app-menu-child.json");
|
||||
|
||||
export async function seedAppMenu() {
|
||||
console.log("🔄 Seeding AppMenu...");
|
||||
|
||||
for (const item of appMenuJson) {
|
||||
await prisma.appMenu.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ AppMenu seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 AppMenu seed selesai");
|
||||
}
|
||||
|
||||
export async function seedAppMenuChild() {
|
||||
console.log("🔄 Seeding AppMenuChild...");
|
||||
|
||||
for (const item of appMenuChildJson) {
|
||||
await prisma.appMenuChild.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
appMenuId: item.appMenuId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
appMenuId: item.appMenuId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ AppMenuChild seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 AppMenuChild seed selesai");
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const layananJson = loadJsonData("core/layanan.json");
|
||||
const potensiJson = loadJsonData("core/potensi.json");
|
||||
const landingPageLayananJson = loadJsonData("core/landingpage-layanan.json");
|
||||
|
||||
export async function seedLayananCore() {
|
||||
console.log("🔄 Seeding Layanan...");
|
||||
|
||||
for (const item of layananJson) {
|
||||
await prisma.layanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Layanan seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Layanan seed selesai");
|
||||
}
|
||||
|
||||
export async function seedPotensiCore() {
|
||||
console.log("🔄 Seeding Potensi...");
|
||||
|
||||
for (const item of potensiJson) {
|
||||
await prisma.potensi.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Potensi seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Potensi seed selesai");
|
||||
}
|
||||
|
||||
export async function seedLandingPageLayanan() {
|
||||
console.log("🔄 Seeding LandingPage_Layanan...");
|
||||
|
||||
for (const item of landingPageLayananJson) {
|
||||
await prisma.landingPage_Layanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
deksripsi: item.deksripsi,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
deksripsi: item.deksripsi,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ LandingPage_Layanan seeded: ${item.id}`);
|
||||
}
|
||||
|
||||
console.log("🎉 LandingPage_Layanan seed selesai");
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export async function seedPosyandu() {
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
banjarId: p.banjarId || null,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
@@ -40,6 +41,7 @@ export async function seedPosyandu() {
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
banjarId: p.banjarId || null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
19
prisma/_seeder_list/kesehatan/seed_banjar.ts
Normal file
19
prisma/_seeder_list/kesehatan/seed_banjar.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const banjarJson = loadJsonData("kesehatan/banjar/banjar.json");
|
||||
|
||||
export async function seedBanjar() {
|
||||
console.log("🔄 Seeding Banjar...");
|
||||
|
||||
for (const b of banjarJson) {
|
||||
await prisma.banjar.upsert({
|
||||
where: { id: b.id },
|
||||
update: { name: b.name },
|
||||
create: { id: b.id, name: b.name },
|
||||
});
|
||||
console.log(`✅ Banjar seeded: ${b.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Banjar seed selesai");
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
"id": "cmkp70zau0002vnu9o1jtpi1i",
|
||||
"judul": "Keamanan Rumah",
|
||||
"deskripsi": "<p><ul><li><p>Pastikan pintu dan jendela selalu terkunci saat meninggalkan rumah</p></li><li><p>Pasang lampu penerangan di halaman dan area sekitar rumah untuk mencegah tindak kejahatan.</p></li><li><p>Jangan mudah memberikan akses masuk ke orang yang tidak dikenal.</p></li></ul></p>",
|
||||
"imageName": "dSe0xyvNLkP2t2f6iq-Hk-mobile.webp"
|
||||
"imageName": "vwZsaxcoFWDlxG1PW7FC0-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmkp71pzo0005vnu9p3n9646d",
|
||||
"judul": "Keamanan Lingkungan Tanggungjawab Bersama",
|
||||
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan sosialisasi dan pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai. Warga diajak berperan aktif dalam menjaga keamanan lingkungan serta mendukung penyediaan lampu penerangan jalan untuk mencegah tindak kriminal dan kecelakaan. Bhabinkamtibmas dan Babinsa turut memberikan materi keamanan dan ketertiban kepada warga, menekankan pentingnya partisipasi masyarakat dalam menjaga keamanan desa.</p>",
|
||||
"imageName": "vwZsaxcoFWDlxG1PW7FC0-mobile.webp"
|
||||
"imageName": "dSe0xyvNLkP2t2f6iq-Hk-mobile.webp"
|
||||
}
|
||||
]
|
||||
10
prisma/data/kesehatan/banjar/banjar.json
Normal file
10
prisma/data/kesehatan/banjar/banjar.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{ "id": "banjar_pudak_amara_001", "name": "Banjar Pudak Amara" },
|
||||
{ "id": "banjar_mawar_001", "name": "Banjar Mawar" },
|
||||
{ "id": "banjar_melati_001", "name": "Banjar Melati" },
|
||||
{ "id": "banjar_dahlia_001", "name": "Banjar Dahlia" },
|
||||
{ "id": "banjar_anggrek_001", "name": "Banjar Anggrek" },
|
||||
{ "id": "banjar_kamboja_001", "name": "Banjar Kamboja" },
|
||||
{ "id": "banjar_melur_001", "name": "Banjar Melur" },
|
||||
{ "id": "banjar_kenanga_001", "name": "Banjar Kenanga" }
|
||||
]
|
||||
@@ -5,55 +5,63 @@
|
||||
"nomor": "(0361) 8463263",
|
||||
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
||||
"jadwalPelayanan": "Senin, 10 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"imageName": "TDQReg1lQ73s39crXW0ra-mobile.webp"
|
||||
"imageName": "TDQReg1lQ73s39crXW0ra-mobile.webp",
|
||||
"banjarId": "banjar_pudak_amara_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_mawar_001",
|
||||
"name": "Posyandu Mawar",
|
||||
"nomor": "(0361) 8463264",
|
||||
"deskripsi": "<p>Posyandu Mawar melayani kesehatan ibu dan anak di wilayah Banjar Mawar, Desa Darmasaba, dengan fokus pada pemantauan tumbuh kembang balita dan kesehatan ibu hamil.</p>",
|
||||
"jadwalPelayanan": "Senin, 15 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Senin, 15 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_mawar_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_melati_001",
|
||||
"name": "Posyandu Melati",
|
||||
"nomor": "(0361) 8463265",
|
||||
"deskripsi": "<p>Posyandu Melati berperan aktif dalam pelayanan kesehatan dasar masyarakat di Banjar Melati, meliputi imunisasi, penimbangan balita, dan konsultasi gizi.</p>",
|
||||
"jadwalPelayanan": "Selasa, 16 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Selasa, 16 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_melati_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_dahlia_001",
|
||||
"name": "Posyandu Dahlia",
|
||||
"nomor": "(0361) 8463266",
|
||||
"deskripsi": "<p>Posyandu Dahlia aktif melayani masyarakat Banjar Dahlia dengan program unggulan pemantauan stunting dan pemberian makanan tambahan bagi balita berisiko.</p>",
|
||||
"jadwalPelayanan": "Rabu, 17 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Rabu, 17 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_dahlia_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_anggrek_001",
|
||||
"name": "Posyandu Anggrek",
|
||||
"nomor": "(0361) 8463267",
|
||||
"deskripsi": "<p>Posyandu Anggrek melayani ibu hamil, ibu menyusui, dan balita di wilayah Banjar Anggrek dengan dukungan tenaga kesehatan dari Puskesmas Abiansemal 3.</p>",
|
||||
"jadwalPelayanan": "Kamis, 18 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Kamis, 18 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_anggrek_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_kamboja_001",
|
||||
"name": "Posyandu Kamboja",
|
||||
"nomor": "(0361) 8463268",
|
||||
"deskripsi": "<p>Posyandu Kamboja hadir untuk mendukung kesehatan masyarakat Banjar Kamboja melalui layanan pemeriksaan rutin, imunisasi lengkap, dan edukasi gizi keluarga.</p>",
|
||||
"jadwalPelayanan": "Jumat, 19 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Jumat, 19 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_kamboja_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_melur_001",
|
||||
"name": "Posyandu Melur",
|
||||
"nomor": "(0361) 8463269",
|
||||
"deskripsi": "<p>Posyandu Melur aktif memberikan layanan kesehatan preventif bagi ibu dan anak di Banjar Melur, termasuk deteksi dini stunting dan pemantauan gizi balita.</p>",
|
||||
"jadwalPelayanan": "Sabtu, 20 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Sabtu, 20 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_melur_001"
|
||||
},
|
||||
{
|
||||
"id": "posyandu_kenanga_001",
|
||||
"name": "Posyandu Kenanga",
|
||||
"nomor": "(0361) 8463270",
|
||||
"deskripsi": "<p>Posyandu Kenanga melayani masyarakat Banjar Kenanga dengan program kesehatan ibu dan anak, pemberian vitamin A, dan konseling laktasi bagi ibu menyusui.</p>",
|
||||
"jadwalPelayanan": "Senin, 23 Feb 2026, 08:00 - 11:00 WITA"
|
||||
"jadwalPelayanan": "Senin, 23 Feb 2026, 08:00 - 11:00 WITA",
|
||||
"banjarId": "banjar_kenanga_001"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"id": "struktur-org-ppid-001",
|
||||
"posisiOrganisasiId": "posisi-001",
|
||||
"pegawaiId": "pegawai-001",
|
||||
"posisiOrganisasiId": "kepala_desa",
|
||||
"pegawaiId": "cmgewz4gt000704ib91i3f169",
|
||||
"hubunganOrganisasiId": "hubungan-001"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Banjar" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Banjar_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Posyandu" ADD COLUMN "banjarId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Posyandu" ADD CONSTRAINT "Posyandu_banjarId_fkey" FOREIGN KEY ("banjarId") REFERENCES "Banjar"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1150,6 +1150,16 @@ model DoctorSign {
|
||||
ArtikelKesehatan ArtikelKesehatan[]
|
||||
}
|
||||
|
||||
// ========================================= BANJAR ========================================= //
|
||||
model Banjar {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
posyandus Posyandu[]
|
||||
}
|
||||
|
||||
// ========================================= POSYANDU ========================================= //
|
||||
model Posyandu {
|
||||
id String @id @default(cuid())
|
||||
@@ -1159,6 +1169,8 @@ model Posyandu {
|
||||
jadwalPelayanan String
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
banjar Banjar? @relation(fields: [banjarId], references: [id])
|
||||
banjarId String?
|
||||
ibuHamil IbuHamil[]
|
||||
balita Balita[]
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -38,6 +38,7 @@ import { seedKontakDaruratKeamanan } from "./_seeder_list/keamanan/seed_kontak_d
|
||||
import { seedLaporanPublik } from "./_seeder_list/keamanan/seed_laporan_publik";
|
||||
import { seedPencegahanKriminalitas } from "./_seeder_list/keamanan/seed_pencegahan_kriminalitas";
|
||||
import { seedPolsekTerdekat } from "./_seeder_list/keamanan/seed_polsek_terdekat";
|
||||
import { seedTipsKeamanan } from "./_seeder_list/keamanan/seed_tips_keamanan";
|
||||
import { seedArtikelKesehatan } from "./_seeder_list/kesehatan/artikel-kesehatan/seed_artikel_kesehatan";
|
||||
import { seedFasilitasKesehatan } from "./_seeder_list/kesehatan/fasilitas-kesehatan/seed_fasilitas_kesehatan";
|
||||
import { seedInfoWabahPenyakit } from "./_seeder_list/kesehatan/info-wabah-penyakit/seed_info_wabah_penyakit";
|
||||
@@ -49,6 +50,7 @@ import { seedProgramKesehatan } from "./_seeder_list/kesehatan/program-kesehatan
|
||||
import { seedPuskesmas } from "./_seeder_list/kesehatan/puskesmas/seed_puskesmas";
|
||||
import { seedGrafikKepuasan } from "./_seeder_list/kesehatan/seed_grafik_kepuasan";
|
||||
import { seedKelahiranKematian } from "./_seeder_list/kesehatan/seed_kelahiran_kematian";
|
||||
import { seedBanjar } from "./_seeder_list/kesehatan/seed_banjar";
|
||||
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";
|
||||
@@ -84,6 +86,7 @@ import { seedIkmPpid } from "./_seeder_list/ppid/ikm/seed_ikm";
|
||||
import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd";
|
||||
import { seedPegawaiPpid } from "./_seeder_list/ppid/struktur-ppid/seed_struktur_ppid";
|
||||
import { seedVisiMisiPpid } from "./_seeder_list/ppid/visi-misi-ppid/seed_visi_misi_ppid";
|
||||
import { seedStrukturOrganisasiPpid, seedFormulirPermohonanKeberatan, seedIndeksKepuasanMasyarakat, seedGrafikBerdasarkanJenisKelamin, seedGrafikBerdasarkanResponden, seedGrafikBerdasarkanUmur } from "./_seeder_list/ppid/seed_ppid_extra";
|
||||
import roles from "./data/user/roles.json";
|
||||
import users from "./data/user/users.json";
|
||||
import { safeSeedUnique } from "./safeseedUnique";
|
||||
@@ -219,6 +222,12 @@ import seedAssets from "./seed_assets";
|
||||
|
||||
// // =========== SUBMENU INDEKS KEPUASAN MASYARAKAT ===========
|
||||
await seedIkmPpid();
|
||||
await seedStrukturOrganisasiPpid();
|
||||
await seedFormulirPermohonanKeberatan();
|
||||
await seedIndeksKepuasanMasyarakat();
|
||||
await seedGrafikBerdasarkanJenisKelamin();
|
||||
await seedGrafikBerdasarkanResponden();
|
||||
await seedGrafikBerdasarkanUmur();
|
||||
|
||||
// // =========== MENU DESA ===========
|
||||
// // =========== SUBMENU PROFILE ===========
|
||||
@@ -245,6 +254,9 @@ import seedAssets from "./seed_assets";
|
||||
await seedPenghargaan();
|
||||
|
||||
// // ====================== MENU KESEHATAN ========================
|
||||
// // ==================== SUBMENU BANJAR =========================
|
||||
await seedBanjar();
|
||||
|
||||
// // ==================== SUBMENU POSYANDU =========================
|
||||
await seedPosyandu();
|
||||
|
||||
@@ -285,7 +297,7 @@ import seedAssets from "./seed_assets";
|
||||
await seedCctv();
|
||||
|
||||
// // ==================== SUBMENU TIPS KEAMANAN ==================
|
||||
await seedKeamananLingkungan();
|
||||
await seedTipsKeamanan();
|
||||
|
||||
// // ====================== MENU EKONOMI ========================
|
||||
// // ==================== SUBMENU UMKM ==========================
|
||||
|
||||
@@ -82,7 +82,15 @@ const balitaState = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.BalitaGetPayload<{
|
||||
include: { posyandu: { select: { id: true; name: true } } };
|
||||
include: {
|
||||
posyandu: {
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
banjar: { select: { id: true; name: true } };
|
||||
};
|
||||
};
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
|
||||
@@ -72,7 +72,15 @@ const ibuHamilState = proxy({
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.IbuHamilGetPayload<{
|
||||
include: { posyandu: { select: { id: true; name: true } } };
|
||||
include: {
|
||||
posyandu: {
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
banjar: { select: { id: true; name: true } };
|
||||
};
|
||||
};
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
|
||||
@@ -19,6 +19,7 @@ const defaultForm = {
|
||||
deskripsi: "",
|
||||
imageId: "",
|
||||
jadwalPelayanan: "",
|
||||
banjarId: "",
|
||||
};
|
||||
|
||||
const posyandustate = proxy({
|
||||
@@ -57,6 +58,7 @@ const posyandustate = proxy({
|
||||
| Prisma.PosyanduGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
banjar: { select: { id: true; name: true } };
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
@@ -92,10 +94,11 @@ const posyandustate = proxy({
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as
|
||||
data: null as
|
||||
| Prisma.PosyanduGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
banjar: { select: { id: true; name: true } };
|
||||
}
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
@@ -176,6 +179,7 @@ const posyandustate = proxy({
|
||||
deskripsi: data.deskripsi,
|
||||
imageId: data.imageId || "",
|
||||
jadwalPelayanan: data.jadwalPelayanan || "",
|
||||
banjarId: data.banjarId || "",
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
@@ -210,6 +214,7 @@ const posyandustate = proxy({
|
||||
deskripsi: this.form.deskripsi,
|
||||
imageId: this.form.imageId,
|
||||
jadwalPelayanan: this.form.jadwalPelayanan,
|
||||
banjarId: this.form.banjarId || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -18,14 +18,39 @@ const intPct = z
|
||||
.min(0, { message: "Minimal 0" })
|
||||
.max(100, { message: "Maksimal 100" });
|
||||
|
||||
type BanjarOption = { id: string; name: string };
|
||||
|
||||
const ringkasanKesehatanState = proxy({
|
||||
banjarId: "" as string,
|
||||
|
||||
findBanjar: {
|
||||
data: [] as BanjarOption[],
|
||||
loading: false,
|
||||
async load() {
|
||||
try {
|
||||
ringkasanKesehatanState.findBanjar.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/banjar/find-many`);
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
ringkasanKesehatanState.findBanjar.data = result?.data ?? [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching banjar:", error);
|
||||
} finally {
|
||||
ringkasanKesehatanState.findBanjar.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
findStats: {
|
||||
data: null as StatsData | null,
|
||||
loading: false,
|
||||
async load() {
|
||||
try {
|
||||
ringkasanKesehatanState.findStats.loading = true;
|
||||
const res = await fetch(`/api/kesehatan/ringkasankesehatan/stats`);
|
||||
const banjarId = ringkasanKesehatanState.banjarId;
|
||||
const params = banjarId ? `?banjarId=${encodeURIComponent(banjarId)}` : "";
|
||||
const res = await fetch(`/api/kesehatan/ringkasankesehatan/stats${params}`);
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
ringkasanKesehatanState.findStats.data = result?.data ?? null;
|
||||
|
||||
@@ -115,13 +115,14 @@ function ListBalita({ search }: { search: string }) {
|
||||
<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="18%">Nama</TableTh>
|
||||
<TableTh w="6%">JK</TableTh>
|
||||
<TableTh w="11%">Tgl Lahir</TableTh>
|
||||
<TableTh w="13%">Banjar</TableTh>
|
||||
<TableTh w="10%">Imunisasi</TableTh>
|
||||
<TableTh w="8%">Gizi</TableTh>
|
||||
<TableTh w="10%">Pemeriksaan</TableTh>
|
||||
<TableTh w="10%">Stunting</TableTh>
|
||||
<TableTh w="14%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
@@ -136,6 +137,7 @@ function ListBalita({ search }: { search: string }) {
|
||||
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</TableTd>
|
||||
<TableTd>{d.posyandu?.banjar?.name ?? '-'}</TableTd>
|
||||
<TableTd>
|
||||
<Badge color={d.imunisasiLengkap ? 'green' : 'red'} variant="light">
|
||||
{d.imunisasiLengkap ? 'Lengkap' : 'Belum'}
|
||||
@@ -190,7 +192,7 @@ function ListBalita({ search }: { search: string }) {
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={8}>
|
||||
<TableTd colSpan={9}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data balita yang cocok
|
||||
@@ -212,19 +214,22 @@ function ListBalita({ search }: { search: string }) {
|
||||
<Text fz="sm" fw={600} mb={4}>
|
||||
{d.nama}
|
||||
</Text>
|
||||
<Group gap="xs" mb={6}>
|
||||
<Group gap="xs" mb={4}>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.jenisKelamin}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
·
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">·</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.tanggalLahir
|
||||
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Group>
|
||||
{d.posyandu?.banjar?.name && (
|
||||
<Text fz="xs" c="dimmed" mb={4}>
|
||||
Banjar: {d.posyandu.banjar.name}
|
||||
</Text>
|
||||
)}
|
||||
<Group gap="xs" mb={8}>
|
||||
<Badge
|
||||
size="xs"
|
||||
|
||||
@@ -117,12 +117,13 @@ function ListIbuHamil({ search }: { search: string }) {
|
||||
<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>
|
||||
<TableTh w="20%">Nama</TableTh>
|
||||
<TableTh w="15%">NIK</TableTh>
|
||||
<TableTh w="15%">Usia Kehamilan</TableTh>
|
||||
<TableTh w="13%">No. HP</TableTh>
|
||||
<TableTh w="15%">Banjar</TableTh>
|
||||
<TableTh w="10%">Status</TableTh>
|
||||
<TableTh w="12%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
@@ -133,6 +134,7 @@ function ListIbuHamil({ search }: { search: string }) {
|
||||
<TableTd>{d.nik || '-'}</TableTd>
|
||||
<TableTd>{d.usiaKehamilan} minggu</TableTd>
|
||||
<TableTd>{d.noHp || '-'}</TableTd>
|
||||
<TableTd>{d.posyandu?.banjar?.name ?? '-'}</TableTd>
|
||||
<TableTd>
|
||||
<Badge
|
||||
color={STATUS_COLORS[d.status] ?? 'gray'}
|
||||
@@ -172,7 +174,7 @@ function ListIbuHamil({ search }: { search: string }) {
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={6}>
|
||||
<TableTd colSpan={7}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data ibu hamil yang cocok
|
||||
@@ -194,17 +196,20 @@ function ListIbuHamil({ search }: { search: string }) {
|
||||
<Text fz="sm" fw={600} mb={4}>
|
||||
{d.nama}
|
||||
</Text>
|
||||
<Group gap="xs" mb={6}>
|
||||
<Group gap="xs" mb={4}>
|
||||
<Text fz="xs" c="dimmed">
|
||||
NIK: {d.nik || '-'}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
·
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">·</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.usiaKehamilan} minggu
|
||||
</Text>
|
||||
</Group>
|
||||
{d.posyandu?.banjar?.name && (
|
||||
<Text fz="xs" c="dimmed" mb={4}>
|
||||
Banjar: {d.posyandu.banjar.name}
|
||||
</Text>
|
||||
)}
|
||||
<Group gap="xs" mb={8}>
|
||||
<Badge
|
||||
size="xs"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
'use client';
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
|
||||
import ringkasanKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import {
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -26,6 +28,7 @@ import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditPosyandu() {
|
||||
const statePosyandu = useProxy(posyandustate);
|
||||
const stateBanjar = useProxy(ringkasanKesehatanState);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
@@ -57,6 +60,7 @@ function EditPosyandu() {
|
||||
deskripsi: '',
|
||||
imageId: '',
|
||||
jadwalPelayanan: '',
|
||||
banjarId: '',
|
||||
});
|
||||
const [originalData, setOriginalData] = useState({
|
||||
name: "",
|
||||
@@ -64,11 +68,13 @@ function EditPosyandu() {
|
||||
deskripsi: "",
|
||||
imageId: "",
|
||||
jadwalPelayanan: "",
|
||||
banjarId: "",
|
||||
imageUrl: ""
|
||||
});
|
||||
|
||||
// Load data posyandu
|
||||
// Load data posyandu dan banjar
|
||||
useEffect(() => {
|
||||
ringkasanKesehatanState.findBanjar.load();
|
||||
const loadPosyandu = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
@@ -82,6 +88,7 @@ function EditPosyandu() {
|
||||
deskripsi: data.deskripsi || '',
|
||||
imageId: data.imageId || '',
|
||||
jadwalPelayanan: data.jadwalPelayanan || '',
|
||||
banjarId: data.banjarId || '',
|
||||
});
|
||||
setOriginalData({
|
||||
name: data.name || '',
|
||||
@@ -89,6 +96,7 @@ function EditPosyandu() {
|
||||
deskripsi: data.deskripsi || '',
|
||||
imageId: data.imageId || '',
|
||||
jadwalPelayanan: data.jadwalPelayanan || '',
|
||||
banjarId: data.banjarId || '',
|
||||
imageUrl: data.image?.link || '',
|
||||
});
|
||||
if (data?.image?.link) setPreviewImage(data.image.link);
|
||||
@@ -129,7 +137,7 @@ function EditPosyandu() {
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const updatedForm = { ...statePosyandu.edit.form, ...formData };
|
||||
const updatedForm = { ...statePosyandu.edit.form, ...formData, banjarId: formData.banjarId };
|
||||
|
||||
// Upload file jika ada
|
||||
if (file) {
|
||||
@@ -160,6 +168,7 @@ function EditPosyandu() {
|
||||
deskripsi: originalData.deskripsi,
|
||||
imageId: originalData.imageId,
|
||||
jadwalPelayanan: originalData.jadwalPelayanan,
|
||||
banjarId: originalData.banjarId,
|
||||
});
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setFile(null);
|
||||
@@ -282,6 +291,15 @@ function EditPosyandu() {
|
||||
required
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Banjar"
|
||||
placeholder="Pilih banjar"
|
||||
clearable
|
||||
data={stateBanjar.findBanjar.data.map((b) => ({ value: b.id, label: b.name }))}
|
||||
value={formData.banjarId || null}
|
||||
onChange={(val) => setFormData({ ...formData, banjarId: val ?? '' })}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi Posyandu
|
||||
|
||||
@@ -82,6 +82,11 @@ function DetailPosyandu() {
|
||||
</Box>
|
||||
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Banjar</Text>
|
||||
<Text fz="md" c="dimmed">{data.banjar?.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Nomor Posyandu</Text>
|
||||
<Text fz="md" c="dimmed">{data.nomor || '-'}</Text>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
@@ -19,19 +20,23 @@ import {
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import ringkasanKesehatanState from '../../../../_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan';
|
||||
|
||||
|
||||
|
||||
function CreatePosyandu() {
|
||||
const statePosyandu = useProxy(posyandustate);
|
||||
const stateBanjar = useProxy(ringkasanKesehatanState);
|
||||
const router = useRouter();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => { ringkasanKesehatanState.findBanjar.load(); }, []);
|
||||
|
||||
// Helper function to check if HTML content is empty
|
||||
const isHtmlEmpty = (html: string) => {
|
||||
// Remove all HTML tags and check if there's any text content
|
||||
@@ -57,6 +62,7 @@ function CreatePosyandu() {
|
||||
deskripsi: '',
|
||||
imageId: '',
|
||||
jadwalPelayanan: '',
|
||||
banjarId: '',
|
||||
};
|
||||
setFile(null);
|
||||
setPreviewImage(null);
|
||||
@@ -223,6 +229,14 @@ function CreatePosyandu() {
|
||||
onChange={(e) => (statePosyandu.create.form.nomor = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Select
|
||||
label="Banjar"
|
||||
placeholder="Pilih banjar"
|
||||
clearable
|
||||
data={stateBanjar.findBanjar.data.map((b) => ({ value: b.id, label: b.name }))}
|
||||
value={statePosyandu.create.form.banjarId || null}
|
||||
onChange={(val) => { statePosyandu.create.form.banjarId = val ?? ''; }}
|
||||
/>
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi Posyandu
|
||||
|
||||
@@ -96,27 +96,33 @@ function ListPosyandu({ search }: { search: string }) {
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w={220} fz="sm" fw={600} ta="left" lh={1.4}>Nama Posyandu</TableTh>
|
||||
<TableTh w={220} fz="sm" fw={600} ta="left" lh={1.4}>Nomor Posyandu</TableTh>
|
||||
<TableTh w={220} fz="sm" fw={600} ta="left" lh={1.4}>Deskripsi</TableTh>
|
||||
<TableTh w={150} fz="sm" fw={600} ta="left" lh={1.4}>Aksi</TableTh>
|
||||
<TableTh w={200} fz="sm" fw={600} ta="left" lh={1.4}>Nama Posyandu</TableTh>
|
||||
<TableTh w={160} fz="sm" fw={600} ta="left" lh={1.4}>Banjar</TableTh>
|
||||
<TableTh w={160} fz="sm" fw={600} ta="left" lh={1.4}>Nomor Posyandu</TableTh>
|
||||
<TableTh w={200} fz="sm" fw={600} ta="left" lh={1.4}>Deskripsi</TableTh>
|
||||
<TableTh w={120} fz="sm" fw={600} ta="left" lh={1.4}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd w={220}>
|
||||
<TableTd w={200}>
|
||||
<Text fz="md" fw={500} lh={1.5} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd w={220}>
|
||||
<TableTd w={160}>
|
||||
<Text fz="sm" c={item.banjar ? undefined : 'dimmed'} lh={1.5}>
|
||||
{item.banjar?.name || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd w={160}>
|
||||
<Text fz="sm" c="dimmed" lh={1.5}>
|
||||
{item.nomor || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd w={220}>
|
||||
<TableTd w={200}>
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
@@ -125,7 +131,7 @@ function ListPosyandu({ search }: { search: string }) {
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd w={220}>
|
||||
<TableTd w={120}>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
@@ -141,7 +147,7 @@ function ListPosyandu({ search }: { search: string }) {
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<TableTd colSpan={5}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data posyandu yang cocok
|
||||
@@ -169,6 +175,14 @@ function ListPosyandu({ search }: { search: string }) {
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Banjar
|
||||
</Text>
|
||||
<Text fz="sm" fw={500} lh={1.5} c={item.banjar ? undefined : 'dimmed'}>
|
||||
{item.banjar?.name || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} lh={1.4}>
|
||||
Nomor Posyandu
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Loader,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
@@ -73,13 +74,29 @@ export default function RingkasanKesehatanPage() {
|
||||
const stats = state.findStats.data;
|
||||
|
||||
const loadStats = useCallback(() => { ringkasanKesehatanState.findStats.load(); }, []);
|
||||
useEffect(() => { loadStats(); }, [loadStats]);
|
||||
useEffect(() => {
|
||||
ringkasanKesehatanState.findBanjar.load();
|
||||
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>
|
||||
<Group justify="space-between" align="flex-end" mb="md" wrap="wrap" gap="sm">
|
||||
<Title order={3} c="black">Ringkasan Kesehatan Desa</Title>
|
||||
<Select
|
||||
placeholder="Semua Banjar"
|
||||
clearable
|
||||
data={state.findBanjar.data.map((b) => ({ value: b.id, label: b.name }))}
|
||||
value={state.banjarId || null}
|
||||
onChange={(val) => {
|
||||
ringkasanKesehatanState.banjarId = val ?? "";
|
||||
ringkasanKesehatanState.findStats.load();
|
||||
}}
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
|
||||
@@ -28,7 +28,15 @@ export default async function balitaFindMany(context: Context) {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.balita.findMany({
|
||||
where,
|
||||
include: { posyandu: { select: { id: true, name: true } } },
|
||||
include: {
|
||||
posyandu: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
banjar: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
|
||||
19
src/app/api/[[...slugs]]/_lib/kesehatan/banjar/find-many.ts
Normal file
19
src/app/api/[[...slugs]]/_lib/kesehatan/banjar/find-many.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function banjarFindMany(context: Context) {
|
||||
try {
|
||||
const data = await prisma.banjar.findMany({
|
||||
where: { isActive: true },
|
||||
select: { id: true, name: true },
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return { success: true, data };
|
||||
} catch (e) {
|
||||
console.error("Error di banjarFindMany:", e);
|
||||
return { success: false, message: "Gagal mengambil data banjar" };
|
||||
}
|
||||
}
|
||||
|
||||
export default banjarFindMany;
|
||||
9
src/app/api/[[...slugs]]/_lib/kesehatan/banjar/index.ts
Normal file
9
src/app/api/[[...slugs]]/_lib/kesehatan/banjar/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Elysia from "elysia";
|
||||
import banjarFindMany from "./find-many";
|
||||
|
||||
const Banjar = new Elysia({
|
||||
prefix: "/banjar",
|
||||
tags: ["Kesehatan/Banjar"],
|
||||
}).get("/find-many", banjarFindMany);
|
||||
|
||||
export default Banjar;
|
||||
@@ -27,7 +27,15 @@ export default async function ibuHamilFindMany(context: Context) {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.ibuHamil.findMany({
|
||||
where,
|
||||
include: { posyandu: { select: { id: true, name: true } } },
|
||||
include: {
|
||||
posyandu: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
banjar: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
|
||||
@@ -24,6 +24,7 @@ import TarifLayanan from "./data_kesehatan_warga/fasilitas_kesehatan/tarif-layan
|
||||
import RingkasanKesehatan from "./ringkasan-kesehatan";
|
||||
import IbuHamil from "./ibu-hamil";
|
||||
import Balita from "./balita";
|
||||
import Banjar from "./banjar";
|
||||
|
||||
|
||||
const Kesehatan = new Elysia({
|
||||
@@ -55,4 +56,5 @@ const Kesehatan = new Elysia({
|
||||
.use(RingkasanKesehatan)
|
||||
.use(IbuHamil)
|
||||
.use(Balita)
|
||||
.use(Banjar)
|
||||
export default Kesehatan;
|
||||
|
||||
@@ -2,15 +2,14 @@ import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
|
||||
type FormCreate = Prisma.PosyanduGetPayload<{
|
||||
select: {
|
||||
name: true;
|
||||
nomor: true;
|
||||
deskripsi: true;
|
||||
imageId: true;
|
||||
jadwalPelayanan: true;
|
||||
};
|
||||
}>;
|
||||
type FormCreate = {
|
||||
name: string;
|
||||
nomor: string;
|
||||
deskripsi: string;
|
||||
imageId: string;
|
||||
jadwalPelayanan: string;
|
||||
banjarId?: string;
|
||||
};
|
||||
export default async function posyanduCreate(context: Context) {
|
||||
const body = context.body as FormCreate;
|
||||
|
||||
@@ -21,6 +20,7 @@ export default async function posyanduCreate(context: Context) {
|
||||
deskripsi: body.deskripsi,
|
||||
imageId: body.imageId,
|
||||
jadwalPelayanan: body.jadwalPelayanan,
|
||||
banjarId: body.banjarId || null,
|
||||
}
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -23,7 +23,8 @@ export default async function findPosyanduById(request: Request) {
|
||||
const data = await prisma.posyandu.findUnique({
|
||||
where: {id},
|
||||
include: {
|
||||
image: true
|
||||
image: true,
|
||||
banjar: { select: { id: true, name: true } },
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ async function posyanduFindMany(context: Context) {
|
||||
where,
|
||||
include: {
|
||||
image: true,
|
||||
banjar: { select: { id: true, name: true } },
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
|
||||
@@ -16,6 +16,7 @@ const Posyandu = new Elysia({
|
||||
deskripsi: t.String(),
|
||||
imageId: t.String(),
|
||||
jadwalPelayanan: t.String(),
|
||||
banjarId: t.Optional(t.String()),
|
||||
})
|
||||
})
|
||||
.get("/find-many", posyanduFindMany)
|
||||
@@ -37,6 +38,7 @@ const Posyandu = new Elysia({
|
||||
deskripsi: t.String(),
|
||||
imageId: t.String(),
|
||||
jadwalPelayanan: t.String(),
|
||||
banjarId: t.Optional(t.String()),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Context } from "elysia";
|
||||
import minio, { MINIO_BUCKET } from "@/lib/minio";
|
||||
|
||||
type FormUpdate = Prisma.PosyanduGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
nomor: true;
|
||||
deskripsi: true;
|
||||
imageId: true;
|
||||
jadwalPelayanan: true;
|
||||
}
|
||||
}>
|
||||
type FormUpdate = {
|
||||
name: string;
|
||||
nomor: string;
|
||||
deskripsi: string;
|
||||
imageId: string;
|
||||
jadwalPelayanan: string;
|
||||
banjarId?: string;
|
||||
};
|
||||
|
||||
export default async function posyanduUpdate(context: Context) {
|
||||
try {
|
||||
const id = context.params?.id as string;
|
||||
const body = (await context.body) as Omit<FormUpdate, "id">;
|
||||
const body = (await context.body) as FormUpdate;
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -25,6 +22,7 @@ export default async function posyanduUpdate(context: Context) {
|
||||
deskripsi,
|
||||
imageId,
|
||||
jadwalPelayanan,
|
||||
banjarId,
|
||||
} = body;
|
||||
|
||||
if(!id) {
|
||||
@@ -80,6 +78,7 @@ export default async function posyanduUpdate(context: Context) {
|
||||
deskripsi,
|
||||
imageId,
|
||||
jadwalPelayanan,
|
||||
banjarId: banjarId || null,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import ringkasanKesehatanStats from "./stats";
|
||||
|
||||
const RingkasanKesehatan = new Elysia({ prefix: "/ringkasankesehatan", tags: ["Kesehatan/Ringkasan"] })
|
||||
.get("/find", ringkasanKesehatanFindUnique)
|
||||
.get("/stats", ringkasanKesehatanStats)
|
||||
.get("/stats", (context) => {
|
||||
const banjarId = (context.query.banjarId as string) || undefined;
|
||||
return ringkasanKesehatanStats(banjarId);
|
||||
})
|
||||
.put("/update", ringkasanKesehatanUpdate, {
|
||||
body: t.Object({
|
||||
targetStuntingPct: t.Number({ minimum: 0, maximum: 100 }),
|
||||
|
||||
@@ -10,8 +10,12 @@ type StatsResult = {
|
||||
targetStuntingPct: number;
|
||||
};
|
||||
|
||||
export default async function ringkasanKesehatanStats(): Promise<{ success: boolean; data?: StatsResult; message?: string }> {
|
||||
export default async function ringkasanKesehatanStats(
|
||||
banjarId?: string
|
||||
): Promise<{ success: boolean; data?: StatsResult; message?: string }> {
|
||||
try {
|
||||
const posyanduFilter = banjarId ? { posyandu: { banjarId } } : {};
|
||||
|
||||
const [
|
||||
ibuHamilAktif,
|
||||
balitaTotal,
|
||||
@@ -21,12 +25,12 @@ export default async function ringkasanKesehatanStats(): Promise<{ success: bool
|
||||
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.ibuHamil.count({ where: { status: "AKTIF", isActive: true, ...posyanduFilter } }),
|
||||
prisma.balita.count({ where: { isActive: true, ...posyanduFilter } }),
|
||||
prisma.balita.count({ where: { isActive: true, statusStunting: { in: ["ALERT", "STUNTING"] }, ...posyanduFilter } }),
|
||||
prisma.balita.count({ where: { isActive: true, imunisasiLengkap: true, ...posyanduFilter } }),
|
||||
prisma.balita.count({ where: { isActive: true, pemeriksaanRutin: true, ...posyanduFilter } }),
|
||||
prisma.balita.count({ where: { isActive: true, giziBaik: true, ...posyanduFilter } }),
|
||||
prisma.ringkasanKesehatanDesa.findFirst({ where: { isActive: true }, orderBy: { createdAt: "desc" } }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,19 +2,14 @@
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Box, Button, Flex, Group, Image,
|
||||
Paper, Skeleton, Stack, Text, Title,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconCalendar, IconInfoCircle, IconPhone } from '@tabler/icons-react';
|
||||
import {
|
||||
IconArrowBack, IconCalendar, IconInfoCircle,
|
||||
IconMapPin, IconPhone,
|
||||
} from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import posyanduState from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
|
||||
@@ -30,8 +25,8 @@ export default function DetailPosyanduUser() {
|
||||
|
||||
if (!statePosyandu.findUnique.data) {
|
||||
return (
|
||||
<Stack py="xl" px={{ base: 'md', md: 100 }}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
<Stack py="xl" px={{ base: 'md', md: 100 }} bg={colors.Bg} mih="100vh">
|
||||
<Skeleton height={500} radius="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -39,97 +34,104 @@ export default function DetailPosyanduUser() {
|
||||
const data = statePosyandu.findUnique.data;
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl">
|
||||
{/* Tombol Kembali */}
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} gap="xl" mih="100vh">
|
||||
<Group>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
|
||||
mb="sm"
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Konten utama */}
|
||||
<Paper
|
||||
withBorder
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
bg={colors['white-trans-1']}
|
||||
bg="white"
|
||||
maw={800}
|
||||
mx="auto"
|
||||
w="100%"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Header — Dijadikan Title */}
|
||||
<Title
|
||||
ta="center"
|
||||
order={1}
|
||||
c={colors['blue-button']}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Title ta="center" order={2} c={colors['blue-button']}>
|
||||
{data.name || 'Posyandu Desa'}
|
||||
</Title>
|
||||
|
||||
{/* Gambar */}
|
||||
{data.image?.link ? (
|
||||
<Center>
|
||||
<Image
|
||||
src={data.image?.link ?? "/no-image.jpg"}
|
||||
alt={`Gambar ${data.name}`}
|
||||
w="100%"
|
||||
h={300}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Center>
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={`Gambar ${data.name}`}
|
||||
w="100%"
|
||||
h={300}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Center>
|
||||
<Text
|
||||
fz={{ base: 'xs', md: 'sm' }}
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
>
|
||||
Tidak ada gambar
|
||||
</Text>
|
||||
</Center>
|
||||
<Box
|
||||
h={200}
|
||||
style={{
|
||||
background: '#f4f5f6',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Info utama */}
|
||||
<Stack gap="sm" mt="md">
|
||||
<Stack gap="sm" mt="sm">
|
||||
{data.banjar && (
|
||||
<Flex align="center" gap="xs">
|
||||
<IconMapPin size={18} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="black">
|
||||
Banjar {data.banjar.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex align="center" gap="xs">
|
||||
<IconPhone size={18} stroke={1.5} />
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
c="black"
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
>
|
||||
<IconPhone size={18} stroke={1.5} color="gray" />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} c="black">
|
||||
{data.nomor || 'Nomor tidak tersedia'}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="xs">
|
||||
<IconCalendar size={18} stroke={1.5} />
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconCalendar
|
||||
size={18}
|
||||
stroke={1.5}
|
||||
color="gray"
|
||||
style={{ marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
c="black"
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
lh={1.6}
|
||||
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '–' }}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="xs">
|
||||
<IconInfoCircle size={18} stroke={1.5} />
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconInfoCircle
|
||||
size={18}
|
||||
stroke={1.5}
|
||||
color="gray"
|
||||
style={{ marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
c="black"
|
||||
lh={{ base: 1.5, md: 1.6 }}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
lh={1.6}
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '–' }}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
@@ -137,4 +139,4 @@ export default function DetailPosyanduUser() {
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,213 +1,651 @@
|
||||
'use client'
|
||||
|
||||
import balitaState from "@/app/admin/(dashboard)/_state/kesehatan/balita/balita";
|
||||
import ibuHamilState from "@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil";
|
||||
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
|
||||
import ringkasanState from "@/app/admin/(dashboard)/_state/kesehatan/ringkasan-kesehatan/ringkasanKesehatan";
|
||||
import colors from "@/con/colors";
|
||||
import { Badge, Box, Button, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import {
|
||||
Badge, Box, Button, Center, Flex, Group, Image,
|
||||
Pagination, Paper, Progress, ScrollArea, Select,
|
||||
SimpleGrid, Skeleton, Stack, Tabs, Text, TextInput,
|
||||
ThemeIcon, Title,
|
||||
} from "@mantine/core";
|
||||
import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
|
||||
import {
|
||||
IconActivity, IconCalendar, IconChartBar, IconHeart,
|
||||
IconMapPin, IconPhone, IconSearch, IconShieldCheck,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import BackButton from "../../desa/layanan/_com/BackButto";
|
||||
import { useTransitionRouter } from "next-view-transitions";
|
||||
|
||||
const stuntingBadgeColor = (s: string) =>
|
||||
s === "STUNTING" ? "red" : s === "ALERT" ? "yellow" : "green";
|
||||
|
||||
const ibuHamilBadgeColor = (s: string) => {
|
||||
if (s === "AKTIF") return "green";
|
||||
if (s === "MELAHIRKAN") return "blue";
|
||||
if (s === "KEGUGURAN") return "gray";
|
||||
return "red";
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const state = useProxy(posyandustate);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
const [activeTab, setActiveTab] = useState<string | null>("ringkasan");
|
||||
|
||||
const stPosyandu = useProxy(posyandustate);
|
||||
const stBalita = useProxy(balitaState);
|
||||
const stIbuHamil = useProxy(ibuHamilState);
|
||||
const stRingkasan = useProxy(ringkasanState);
|
||||
|
||||
const [searchPosyandu, setSearchPosyandu] = useState("");
|
||||
const [debouncedSearchPosyandu] = useDebouncedValue(searchPosyandu, 800);
|
||||
|
||||
const [searchBalita, setSearchBalita] = useState("");
|
||||
const [debouncedSearchBalita] = useDebouncedValue(searchBalita, 800);
|
||||
const [filterStunting, setFilterStunting] = useState<string | null>(null);
|
||||
|
||||
const [searchIbuHamil, setSearchIbuHamil] = useState("");
|
||||
const [debouncedSearchIbuHamil] = useDebouncedValue(searchIbuHamil, 800);
|
||||
const [filterStatusIbuHamil, setFilterStatusIbuHamil] = useState<string | null>(null);
|
||||
|
||||
const router = useTransitionRouter();
|
||||
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
// Initial load: ringkasan
|
||||
useShallowEffect(() => {
|
||||
ringkasanState.findStats.load();
|
||||
}, []);
|
||||
|
||||
// Load data sesuai tab aktif
|
||||
useShallowEffect(() => {
|
||||
if (activeTab === "ringkasan") {
|
||||
ringkasanState.findStats.load();
|
||||
} else if (activeTab === "posyandu") {
|
||||
posyandustate.findMany.load(1, 6, debouncedSearchPosyandu);
|
||||
} else if (activeTab === "balita") {
|
||||
balitaState.findMany.load(1, 10, debouncedSearchBalita, filterStunting ?? "");
|
||||
} else if (activeTab === "ibu-hamil") {
|
||||
ibuHamilState.findMany.load(1, 10, debouncedSearchIbuHamil, filterStatusIbuHamil ?? "");
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 6, debouncedSearch);
|
||||
}, [page, debouncedSearch]);
|
||||
if (activeTab === "posyandu") {
|
||||
posyandustate.findMany.load(1, 6, debouncedSearchPosyandu);
|
||||
}
|
||||
}, [debouncedSearchPosyandu]);
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Box py="xl" px={{ base: "md", md: 100 }}>
|
||||
<Skeleton h={500} radius="lg" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
useShallowEffect(() => {
|
||||
if (activeTab === "balita") {
|
||||
balitaState.findMany.load(1, 10, debouncedSearchBalita, filterStunting ?? "");
|
||||
}
|
||||
}, [debouncedSearchBalita, filterStunting]);
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<BackButton />
|
||||
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Title
|
||||
order={1}
|
||||
ta="left"
|
||||
c={colors["blue-button"]}
|
||||
>
|
||||
Posyandu Desa Darmasaba
|
||||
</Title>
|
||||
<TextInput
|
||||
placeholder="Cari posyandu berdasarkan nama..."
|
||||
aria-label="Pencarian Posyandu"
|
||||
radius="xl"
|
||||
size="md"
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "100%", md: "35%" }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Title c={"dimmed"} order={3} ta={"center"}>Belum ada posyandu yang terdaftar</Title>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
useShallowEffect(() => {
|
||||
if (activeTab === "ibu-hamil") {
|
||||
ibuHamilState.findMany.load(1, 10, debouncedSearchIbuHamil, filterStatusIbuHamil ?? "");
|
||||
}
|
||||
}, [debouncedSearchIbuHamil, filterStatusIbuHamil]);
|
||||
|
||||
const stats = stRingkasan.findStats.data;
|
||||
|
||||
return (
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl" mih="100vh">
|
||||
{/* Header */}
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<BackButton />
|
||||
<Flex mt="md" justify="space-between" align="center" wrap="wrap" gap="md">
|
||||
<Title
|
||||
order={1}
|
||||
ta="left"
|
||||
c={colors["blue-button"]}
|
||||
>
|
||||
Posyandu Desa Darmasaba
|
||||
<Stack gap={4} mt="md">
|
||||
<Title order={1} c={colors["blue-button"]}>
|
||||
Kesehatan Posyandu
|
||||
</Title>
|
||||
<TextInput
|
||||
placeholder="Cari posyandu berdasarkan nama..."
|
||||
aria-label="Pencarian Posyandu"
|
||||
radius="xl"
|
||||
size="md"
|
||||
leftSection={<IconSearch size={20} />}
|
||||
w={{ base: "100%", md: "35%" }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</Flex>
|
||||
<Text fz={{ base: "sm", md: "md" }} c="dimmed">
|
||||
Informasi posyandu, data balita, ibu hamil, dan ringkasan kesehatan Desa Darmasaba
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Tabs */}
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap="xl">
|
||||
<SimpleGrid
|
||||
pb="lg"
|
||||
cols={{ base: 1, sm: 2, md: 3 }}
|
||||
spacing="lg"
|
||||
<Tabs
|
||||
variant="pills"
|
||||
color={colors["blue-button"]}
|
||||
radius="lg"
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
>
|
||||
|
||||
<Box mb="md"
|
||||
style={{
|
||||
overflowX: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
WebkitOverflowScrolling: 'touch', // ✅ smooth scroll di iOS
|
||||
msOverflowStyle: 'none', // ✅ sembunyikan scrollbar di IE/Edge
|
||||
scrollbarWidth: 'none', // ✅ sembunyikan scrollbar di Firefox
|
||||
}}
|
||||
>
|
||||
{data?.map((v, k) => (
|
||||
<Paper
|
||||
key={k}
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="md"
|
||||
withBorder
|
||||
bg={colors["white-trans-1"]}
|
||||
style={{
|
||||
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = "translateY(-4px)";
|
||||
(e.currentTarget as HTMLElement).style.boxShadow =
|
||||
"0 8px 24px rgba(0,0,0,0.12)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = "translateY(0)";
|
||||
(e.currentTarget as HTMLElement).style.boxShadow =
|
||||
"0 4px 12px rgba(0,0,0,0.08)";
|
||||
}}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Title order={3} c={colors["blue-button"]} fw="bold" lineClamp={1}>
|
||||
{v.name}
|
||||
</Title>
|
||||
<Badge color="blue" variant="light" size="sm" radius="sm">
|
||||
Aktif
|
||||
</Badge>
|
||||
</Group>
|
||||
<Tabs.List
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
padding: "0.25rem",
|
||||
display: 'inline-flex', // ✅ lebih tepat dari 'flex' untuk nowrap
|
||||
flexWrap: 'nowrap',
|
||||
gap: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<Tabs.Tab value="ringkasan" leftSection={<IconChartBar size={16} />}>
|
||||
Ringkasan
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="posyandu" leftSection={<IconMapPin size={16} />}>
|
||||
Data Posyandu
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="balita" leftSection={<IconUsers size={16} />}>
|
||||
Balita
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="ibu-hamil" leftSection={<IconHeart size={16} />}>
|
||||
Ibu Hamil
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Box>
|
||||
|
||||
{/* ===== RINGKASAN ===== */}
|
||||
<Tabs.Panel value="ringkasan" pt="md">
|
||||
{stRingkasan.findStats.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} h={120} radius="lg" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : !stats ? (
|
||||
<Paper p="xl" radius="lg" withBorder bg="white" ta="center">
|
||||
<Text c="dimmed">Data ringkasan belum tersedia</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
{/* KPI utama */}
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
|
||||
{[
|
||||
{
|
||||
label: "Ibu Hamil Aktif",
|
||||
value: stats.ibuHamilAktif,
|
||||
color: colors["blue-button"],
|
||||
icon: <IconHeart size={28} />,
|
||||
},
|
||||
{
|
||||
label: "Balita Terdaftar",
|
||||
value: stats.balitaTerdaftar,
|
||||
color: "#22c55e",
|
||||
icon: <IconUsers size={28} />,
|
||||
},
|
||||
{
|
||||
label: "Alert Stunting",
|
||||
value: stats.alertStunting,
|
||||
color: "#ef4444",
|
||||
icon: <IconActivity size={28} />,
|
||||
},
|
||||
].map((s, i) => (
|
||||
<Paper key={i} p="xl" radius="lg" withBorder bg="white" shadow="sm">
|
||||
<Group gap="md">
|
||||
<ThemeIcon size={52} radius="lg" color={s.color} variant="light">
|
||||
{s.icon}
|
||||
</ThemeIcon>
|
||||
<Stack gap={4}>
|
||||
<Text fz="2rem" fw={700} c={s.color} lh={1}>
|
||||
{s.value}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">{s.label}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Persentase */}
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
|
||||
{[
|
||||
{
|
||||
label: "Imunisasi Lengkap",
|
||||
pct: stats.imunisasiLengkapPct,
|
||||
color: colors["blue-button"],
|
||||
icon: <IconShieldCheck size={20} />,
|
||||
},
|
||||
{
|
||||
label: "Gizi Baik",
|
||||
pct: stats.giziBaikPct,
|
||||
color: "#22c55e",
|
||||
icon: <IconHeart size={20} />,
|
||||
},
|
||||
{
|
||||
label: "Pemeriksaan Rutin",
|
||||
pct: stats.pemeriksaanRutinPct,
|
||||
color: "#f59e0b",
|
||||
icon: <IconActivity size={20} />,
|
||||
},
|
||||
].map((s, i) => (
|
||||
<Paper key={i} p="lg" radius="lg" withBorder bg="white" shadow="sm">
|
||||
<Stack gap="sm">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size={32} radius="md" color={s.color} variant="light">
|
||||
{s.icon}
|
||||
</ThemeIcon>
|
||||
<Text fz="sm" fw={600}>{s.label}</Text>
|
||||
</Group>
|
||||
<Group gap="xs" align="center">
|
||||
<Progress
|
||||
value={s.pct}
|
||||
color={s.color}
|
||||
radius="xl"
|
||||
size="md"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Text fz="sm" fw={700} c={s.color} w={40} ta="right">
|
||||
{s.pct}%
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ===== DATA POSYANDU ===== */}
|
||||
<Tabs.Panel value="posyandu" pt="md">
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
placeholder="Cari nama posyandu..."
|
||||
radius="xl"
|
||||
leftSection={<IconSearch size={18} />}
|
||||
value={searchPosyandu}
|
||||
onChange={(e) => setSearchPosyandu(e.currentTarget.value)}
|
||||
w={{ base: "100%", md: "40%" }}
|
||||
/>
|
||||
|
||||
{stPosyandu.findMany.loading ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} h={300} radius="lg" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : !stPosyandu.findMany.data?.length ? (
|
||||
<Paper p="xl" radius="lg" withBorder bg="white" ta="center">
|
||||
<Text c="dimmed">Tidak ada posyandu ditemukan</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
<>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
|
||||
{stPosyandu.findMany.data.map((v, k) => (
|
||||
<Paper
|
||||
key={k}
|
||||
p="xl"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
bg="white"
|
||||
style={{ transition: "transform 0.2s, box-shadow 0.2s" }}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = "translateY(-4px)";
|
||||
(e.currentTarget as HTMLElement).style.boxShadow =
|
||||
"0 8px 24px rgba(0,0,0,0.12)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = "translateY(0)";
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = "";
|
||||
}}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<Title
|
||||
order={4}
|
||||
c={colors["blue-button"]}
|
||||
lineClamp={1}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{v.name}
|
||||
</Title>
|
||||
<Badge
|
||||
color="green"
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
Aktif
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Image
|
||||
src={v.image?.link ?? "/no-image.jpg"}
|
||||
alt={`Gambar ${v.name}`}
|
||||
radius="md"
|
||||
h={160}
|
||||
fit="cover"
|
||||
/>
|
||||
|
||||
{v.banjar && (
|
||||
<Flex align="center" gap="xs">
|
||||
<IconMapPin size={16} stroke={1.5} color={colors["blue-button"]} />
|
||||
<Text fz="sm" c="dimmed">Banjar {v.banjar.name}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex align="center" gap="xs">
|
||||
<IconPhone size={16} stroke={1.5} color="gray" />
|
||||
<Text fz="sm" c="dimmed">{v.nomor || "–"}</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconCalendar
|
||||
size={16}
|
||||
stroke={1.5}
|
||||
color="gray"
|
||||
style={{ marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<Text
|
||||
fz="sm"
|
||||
c="dimmed"
|
||||
lh={1.5}
|
||||
lineClamp={2}
|
||||
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
|
||||
style={{ wordBreak: "break-word" }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
radius="lg"
|
||||
variant="outline"
|
||||
color={colors["blue-button"]}
|
||||
fullWidth
|
||||
mt="xs"
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)
|
||||
}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Image
|
||||
src={v.image?.link ?? "/no-image.jpg"}
|
||||
alt={`Gambar ${v.name}`}
|
||||
radius="md"
|
||||
w="100%"
|
||||
h={180}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
<Pagination
|
||||
value={stPosyandu.findMany.page}
|
||||
onChange={(p) =>
|
||||
posyandustate.findMany.load(p, 6, searchPosyandu)
|
||||
}
|
||||
total={stPosyandu.findMany.totalPages}
|
||||
radius="lg"
|
||||
color={colors["blue-button"]}
|
||||
/>
|
||||
</Center>
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconPhone size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
c="black"
|
||||
lh={{ base: 1.4, md: 1.5 }}
|
||||
>
|
||||
{v.nomor || "Tidak tersedia"}
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconCalendar size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
c="black"
|
||||
lh={{ base: 1.4, md: 1.5 }}
|
||||
>
|
||||
<strong>Jadwal:</strong>{" "}
|
||||
<span
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }}
|
||||
/>
|
||||
</Text>
|
||||
</Flex>
|
||||
{/* ===== BALITA ===== */}
|
||||
<Tabs.Panel value="balita" pt="md">
|
||||
<Stack gap="md">
|
||||
<Group wrap="wrap">
|
||||
<TextInput
|
||||
placeholder="Cari nama, orang tua..."
|
||||
radius="xl"
|
||||
leftSection={<IconSearch size={18} />}
|
||||
value={searchBalita}
|
||||
onChange={(e) => setSearchBalita(e.currentTarget.value)}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Semua status stunting"
|
||||
clearable
|
||||
radius="xl"
|
||||
data={[
|
||||
{ value: "NORMAL", label: "Normal" },
|
||||
{ value: "ALERT", label: "Alert" },
|
||||
{ value: "STUNTING", label: "Stunting" },
|
||||
]}
|
||||
value={filterStunting}
|
||||
onChange={setFilterStunting}
|
||||
w={{ base: "100%", sm: 200 }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Flex align="flex-start" gap="xs">
|
||||
<IconInfoCircle size={18} stroke={1.5} style={{ marginTop: 3 }} />
|
||||
<Text
|
||||
fz={{ base: "sm", md: "md" }}
|
||||
lh={{ base: 1.4, md: 1.5 }}
|
||||
c="black"
|
||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
lineClamp={3}
|
||||
truncate="end"
|
||||
/>
|
||||
</Flex>
|
||||
<Button radius="lg" size="md" variant="outline" onClick={() => router.push(`/darmasaba/kesehatan/posyandu/${v.id}`)}>
|
||||
Detail
|
||||
</Button>
|
||||
{stBalita.findMany.loading ? (
|
||||
<Stack gap="sm">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} h={100} radius="lg" />
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : !stBalita.findMany.data?.length ? (
|
||||
<Paper p="xl" radius="lg" withBorder bg="white" ta="center">
|
||||
<Text c="dimmed">Tidak ada data balita ditemukan</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
<>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Total: <strong>{stBalita.findMany.total}</strong> balita terdaftar
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
{stBalita.findMany.data.map((v, i) => (
|
||||
<Paper key={i} p="md" radius="lg" withBorder bg="white" shadow="xs">
|
||||
<Stack gap="xs">
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={600} fz="sm" lineClamp={1}>{v.nama}</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{v.jenisKelamin === "L" ? "Laki-laki" : "Perempuan"}
|
||||
{v.tanggalLahir
|
||||
? ` · ${new Date(v.tanggalLahir).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}`
|
||||
: ""}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge
|
||||
color={stuntingBadgeColor(v.statusStunting)}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{v.statusStunting}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
radius="lg"
|
||||
size="md"
|
||||
withControls
|
||||
mt="md"
|
||||
/>
|
||||
</Center>
|
||||
{v.posyandu && (
|
||||
<Flex align="center" gap={4}>
|
||||
<IconMapPin size={13} color="gray" />
|
||||
<Text fz="xs" c="dimmed">
|
||||
{v.posyandu.name}
|
||||
{v.posyandu.banjar
|
||||
? ` · Banjar ${v.posyandu.banjar.name}`
|
||||
: ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Stack gap="sm">
|
||||
<Flex align="center" gap="xs">
|
||||
<IconInfoCircle size={22} color={colors["blue-button"]} />
|
||||
<Title order={2} c={colors["blue-button"]}>
|
||||
Layanan Utama Posyandu
|
||||
</Title>
|
||||
</Flex>
|
||||
<List spacing="xs" fz={{ base: "sm", md: "md" }} lh={{ base: 1.4, md: 1.5 }} c="black">
|
||||
<ListItem>Penimbangan bayi dan balita</ListItem>
|
||||
<ListItem>Pemantauan status gizi</ListItem>
|
||||
<ListItem>Imunisasi dasar lengkap</ListItem>
|
||||
<ListItem>Konseling kesehatan</ListItem>
|
||||
<ListItem>Pemantauan kesehatan ibu hamil</ListItem>
|
||||
</List>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Group gap="xs">
|
||||
<Badge
|
||||
color={v.imunisasiLengkap ? "blue" : "gray"}
|
||||
variant="dot"
|
||||
size="xs"
|
||||
>
|
||||
{v.imunisasiLengkap
|
||||
? "Imunisasi Lengkap"
|
||||
: "Imunisasi Belum Lengkap"}
|
||||
</Badge>
|
||||
<Badge
|
||||
color={v.giziBaik ? "green" : "orange"}
|
||||
variant="dot"
|
||||
size="xs"
|
||||
>
|
||||
{v.giziBaik ? "Gizi Baik" : "Gizi Kurang"}
|
||||
</Badge>
|
||||
<Badge
|
||||
color={v.pemeriksaanRutin ? "teal" : "gray"}
|
||||
variant="dot"
|
||||
size="xs"
|
||||
>
|
||||
{v.pemeriksaanRutin ? "Periksa Rutin" : "Tidak Rutin"}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={stBalita.findMany.page}
|
||||
onChange={(p) =>
|
||||
balitaState.findMany.load(
|
||||
p,
|
||||
10,
|
||||
searchBalita,
|
||||
filterStunting ?? ""
|
||||
)
|
||||
}
|
||||
total={stBalita.findMany.totalPages}
|
||||
radius="lg"
|
||||
color={colors["blue-button"]}
|
||||
/>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ===== IBU HAMIL ===== */}
|
||||
<Tabs.Panel value="ibu-hamil" pt="md">
|
||||
<Stack gap="md">
|
||||
<Group wrap="wrap">
|
||||
<TextInput
|
||||
placeholder="Cari nama..."
|
||||
radius="xl"
|
||||
leftSection={<IconSearch size={18} />}
|
||||
value={searchIbuHamil}
|
||||
onChange={(e) => setSearchIbuHamil(e.currentTarget.value)}
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Semua status"
|
||||
clearable
|
||||
radius="xl"
|
||||
data={[
|
||||
{ value: "AKTIF", label: "Aktif" },
|
||||
{ value: "MELAHIRKAN", label: "Melahirkan" },
|
||||
{ value: "KEGUGURAN", label: "Keguguran" },
|
||||
{ value: "NONAKTIF", label: "Nonaktif" },
|
||||
]}
|
||||
value={filterStatusIbuHamil}
|
||||
onChange={setFilterStatusIbuHamil}
|
||||
w={{ base: "100%", sm: 200 }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{stIbuHamil.findMany.loading ? (
|
||||
<Stack gap="sm">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} h={100} radius="lg" />
|
||||
))}
|
||||
</Stack>
|
||||
) : !stIbuHamil.findMany.data?.length ? (
|
||||
<Paper p="xl" radius="lg" withBorder bg="white" ta="center">
|
||||
<Text c="dimmed">Tidak ada data ibu hamil ditemukan</Text>
|
||||
</Paper>
|
||||
) : (
|
||||
<>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Total: <strong>{stIbuHamil.findMany.total}</strong> data ibu hamil
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
{stIbuHamil.findMany.data.map((v, i) => (
|
||||
<Paper key={i} p="md" radius="lg" withBorder bg="white" shadow="xs">
|
||||
<Stack gap="xs">
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={600} fz="sm" lineClamp={1}>{v.nama}</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
Usia kehamilan: {v.usiaKehamilan} minggu
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge
|
||||
color={ibuHamilBadgeColor(v.status)}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{v.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{v.posyandu && (
|
||||
<Flex align="center" gap={4}>
|
||||
<IconMapPin size={13} color="gray" />
|
||||
<Text fz="xs" c="dimmed">
|
||||
{v.posyandu.name}
|
||||
{v.posyandu.banjar
|
||||
? ` · Banjar ${v.posyandu.banjar.name}`
|
||||
: ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{v.taksiranLahir && (
|
||||
<Flex align="center" gap={4}>
|
||||
<IconCalendar size={13} color="gray" />
|
||||
<Text fz="xs" c="dimmed">
|
||||
Taksiran lahir:{" "}
|
||||
{new Date(v.taksiranLahir).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={stIbuHamil.findMany.page}
|
||||
onChange={(p) =>
|
||||
ibuHamilState.findMany.load(
|
||||
p,
|
||||
10,
|
||||
searchIbuHamil,
|
||||
filterStatusIbuHamil ?? ""
|
||||
)
|
||||
}
|
||||
total={stIbuHamil.findMany.totalPages}
|
||||
radius="lg"
|
||||
color={colors["blue-button"]}
|
||||
/>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user