diff --git a/.gitignore b/.gitignore
index fa5f03cd..ebd64b35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,7 +41,15 @@ next-env.d.ts
# uploads
/uploads
+# download
+/download
+
# cache
/cache
.github/
+
+.env.*
+
+*.tar.gz
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..9e5a22b3
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "WillLuke.nextjs.addTypesOnSave": true,
+ "WillLuke.nextjs.hasPrompted": true
+}
diff --git a/bun.lockb b/bun.lockb
index 624c5ecf..e571b0b1 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/foldergambar/desa/name.png b/foldergambar/desa/name.png
new file mode 100644
index 00000000..8f31f48e
Binary files /dev/null and b/foldergambar/desa/name.png differ
diff --git a/foldergambar/desa/ppid/profile-ppid/name.png b/foldergambar/desa/ppid/profile-ppid/name.png
new file mode 100644
index 00000000..97ffac9e
Binary files /dev/null and b/foldergambar/desa/ppid/profile-ppid/name.png differ
diff --git a/next.config.ts b/next.config.ts
index ac27ff93..29d246d6 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,6 +1,11 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
+ experimental: {},
+ allowedDevOrigins: [
+ "http://192.168.1.82:3000", // buat akses dari HP/device lain
+ "http://localhost:3000", // akses lokal
+ ],
async headers() {
return [
{
diff --git a/package.json b/package.json
index d6976f97..472efe96 100644
--- a/package.json
+++ b/package.json
@@ -1,60 +1,109 @@
{
"name": "desa-darmasaba",
- "version": "0.1.0",
+ "version": "0.1.5",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
- "build": "next build",
- "start": "next start",
- "lint": "next lint",
- "prisma:seed": "bun run prisma/seed.ts"
+ "dev": "bun --bun next dev",
+ "build": "bun --bun next build",
+ "start": "bun --bun next start"
},
"prisma": {
"seed": "bun run prisma/seed.ts"
},
"dependencies": {
+ "@cubejs-client/core": "^0.31.0",
+ "@elysiajs/cookie": "^0.8.0",
"@elysiajs/cors": "^1.2.0",
- "@elysiajs/eden": "^1.2.0",
+ "@elysiajs/eden": "^1.3.2",
+ "@elysiajs/jwt": "^1.3.2",
+ "@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
"@mantine/carousel": "^7.16.2",
- "@mantine/core": "^7.16.2",
- "@mantine/dropzone": "^7.17.0",
- "@mantine/hooks": "^7.16.2",
+ "@mantine/charts": "^7.17.1",
+ "@mantine/core": "^7.17.4",
+ "@mantine/dates": "^8.1.0",
+ "@mantine/dropzone": "^8.1.1",
+ "@mantine/form": "^8.1.0",
+ "@mantine/hooks": "^7.17.4",
+ "@mantine/tiptap": "^7.17.4",
"@paljs/types": "^8.1.0",
"@prisma/client": "^6.3.1",
"@tabler/icons-react": "^3.30.0",
+ "@tiptap/extension-highlight": "^2.11.7",
+ "@tiptap/extension-link": "^2.11.7",
+ "@tiptap/extension-subscript": "^2.11.7",
+ "@tiptap/extension-superscript": "^2.11.7",
+ "@tiptap/extension-text-align": "^2.11.7",
+ "@tiptap/extension-underline": "^2.11.7",
+ "@tiptap/pm": "^2.11.7",
+ "@tiptap/react": "^2.11.7",
+ "@tiptap/starter-kit": "^2.11.7",
+ "@types/adm-zip": "^0.5.7",
"@types/bun": "^1.2.2",
- "@types/lodash": "^4.17.15",
+ "@types/leaflet": "^1.9.20",
+ "@types/lodash": "^4.17.16",
+ "@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
+ "adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
+ "bcryptjs": "^3.0.2",
"bun": "^1.2.2",
- "elysia": "^1.2.12",
+ "chart.js": "^4.4.8",
+ "classnames": "^2.5.1",
+ "colors": "^1.4.0",
+ "dayjs": "^1.11.13",
+ "dotenv": "^17.2.3",
+ "elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
- "framer-motion": "^12.4.1",
+ "extract-zip": "^2.0.1",
+ "form-data": "^4.0.2",
+ "framer-motion": "^12.23.5",
"get-port": "^7.1.0",
+ "iron-session": "^8.0.4",
+ "jose": "^6.1.0",
+ "jotai": "^2.12.3",
+ "jsonwebtoken": "^9.0.2",
+ "leaflet": "^1.9.4",
+ "list": "^2.0.19",
"lodash": "^4.17.21",
"motion": "^12.4.1",
- "nanoid": "^5.1.0",
- "next": "15.1.6",
+ "nanoid": "^5.1.5",
+ "next": "^15.5.2",
"next-view-transitions": "^0.3.4",
+ "node-fetch": "^3.3.2",
+ "nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
+ "primeicons": "^7.0.0",
+ "primereact": "^10.9.6",
"prisma": "^6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-international-phone": "^4.6.0",
+ "react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
+ "react-toastify": "^11.0.5",
+ "react-transition-group": "^4.4.5",
+ "react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
+ "recharts": "^2.15.3",
+ "sharp": "^0.34.3",
"swr": "^2.3.2",
- "valtio": "^2.1.3"
+ "uuid": "^11.1.0",
+ "valtio": "^2.1.3",
+ "zlib": "^1.0.5",
+ "zod": "^3.24.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+ "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.6",
+ "parcel": "^2.6.2",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
diff --git a/prisma/data/category-pengumuman.json b/prisma/data/category-pengumuman.json
new file mode 100644
index 00000000..5bacc742
--- /dev/null
+++ b/prisma/data/category-pengumuman.json
@@ -0,0 +1,8 @@
+[
+ { "name": "Sosial & Kesehatan" },
+ { "name": "Ekonomi & UMKM" },
+ { "name": "Pendidikan & Kepemudaan" },
+ { "name": "Lingkungan & Bencana" },
+ { "name": "Adat & Budaya" },
+ { "name": "Digitalisasi Desa" }
+]
diff --git a/prisma/data/desa/berita/kategori-berita.json b/prisma/data/desa/berita/kategori-berita.json
new file mode 100644
index 00000000..4d777965
--- /dev/null
+++ b/prisma/data/desa/berita/kategori-berita.json
@@ -0,0 +1,8 @@
+[
+ { "name": "Pemerintahan" },
+ { "name": "Pembangunan" },
+ { "name": "Ekonomi" },
+ { "name": "Sosial" },
+ { "name": "Budaya" },
+ { "name": "Teknologi" }
+]
diff --git a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json
new file mode 100644
index 00000000..d39b6e98
--- /dev/null
+++ b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "name": "Pelayanan Penduduk Non-Permanent",
+ "deskripsi": "
Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json
new file mode 100644
index 00000000..42e7ab7a
--- /dev/null
+++ b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json
@@ -0,0 +1,8 @@
+[
+ {
+ "id": "edit",
+ "name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
+ "deskripsi": "Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.
",
+ "link" : "https://oss.go.id/"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/layanan/pelayananSuratKeterangan.json b/prisma/data/desa/layanan/pelayananSuratKeterangan.json
new file mode 100644
index 00000000..9f13159c
--- /dev/null
+++ b/prisma/data/desa/layanan/pelayananSuratKeterangan.json
@@ -0,0 +1,57 @@
+[
+ {
+ "id" : "cmdxyb9zi0010vniiaeyi55ui",
+ "name" : "Surat Keterangan Beda Biodata Diri",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP atau Kartu Keluarga
Fotocopy dokumen bersangkutan yang terdapat perbedaan biodata diri misal : Sertifikat Tanah/Ijazah/Polis Asuransi dan lainnya.
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxycqz40014vniidftrixvf",
+ "name" : "Surat Keterangan Yatim Piatu",
+ "deskripsi" : "Persyaratan Dokumen :
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdwx3wph0003vnr74us2t7h7",
+ "name" : "Surat Keterangan Domisili Organisasi",
+ "deskripsi" : "Persyaratan Dokumen:
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy Surat Keterangan Terdaftar (SKT) organisasi atau Pengukuhan Kelompok
Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap dengan Kop Organisasi
Tanggal berdiri/Tahun berdiri/Sejak kapan berdirinya organisasi
Alur Pelayanan:
"
+ },
+ {
+ "id" : "cmdxxv3i80004vniidg1mrucc",
+ "name" : "Surat Keterangan Penghasilan",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP orang tua atau Fotocopy Kartu keluarga
Membuat Surat Pernyataan Penghasilan bermaterai (disertai jumlah penghasilan)
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxxwp070008vnii9jbdcto7",
+ "name" : "Surat Keterangan Tidak Mampu",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP/KIA atau Kartu Keluarga
Fotocopy Kartu Indonesia Pintar/Kartu Perlindungan Sosial/Terdaftar dalam DTKS
Jika tidak memiliki Kartu tersebut diatas diwajibkan membuat Surat Pernyataan Tidak Mampu
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxxyfkl000cvnii1bxinnfi",
+ "name" : "Surat Keterangan Kelahiran",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy Surat lahir dari dokter/bidan (jika ada)
Fotocopy Kartu Keluarga
Fotocopy KTP 2 orang saksi
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxy23pl000gvniihsg38aq4",
+ "name" : "Surat Keterangan Usaha",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP atau Kartu Keluarga
Foto Lokasi dan Kegiatan Usaha di cetak dalam selembar kertas (diparaf dan stempel oleh Kelian Banjar Dinas)
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxy4mgt000kvniib1nemjem",
+ "name" : "Surat Keterangan Kematian",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP atau Kartu Keluarga
Surat Kematian dari rumah sakit atau dokter (jika ada)
tanggal kematian
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxy61a1000ovniif4ytb9hs",
+ "name" : "Surat Keterangan Tempat Usaha",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP atau Kartu Keluarga
Foto Lokasi dan Kegiatan Usaha di cetak dalam selembar kertas (diparaf dan stempel oleh Kelian Banjar Dinas)
Surat Perjanjian Sewa/Kontrak atau Kwintansi Pembayaran Sewa 3 bulan terakhir bagi yang mengontrak tempat usaha, apabila tempat usaha milik sendiri lampiri dengan dokumen kepemilikan tempat usaha (dapat berupa fotocopy sppt atau Fotocopy Sertipikat Hak Milik)
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxy754q000svniiiz8oqyo0",
+ "name" : "Surat Keterangan Belum Kawin",
+ "deskripsi" : "Persyaratan Dokumen :
Pengantar Kelian Banjar Dinas di Wilayah Masing - masing
Fotocopy KTP atau Kartu Keluarga
Khusus bagi yang berstatus duda atau janda melampirkan fotocopy akta cerai atau dokumen pendukung lainnya
Alur Pelayanan :
"
+ },
+ {
+ "id" : "cmdxy8pi2000wvnii48fc1sxd",
+ "name" : "Surat Keterangan Kelakuan Baik",
+ "deskripsi" : "Persyaratan Dokumen :
Alur Pelayanan :
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/layanan/pelayananTelunjukSaktiDesa.json b/prisma/data/desa/layanan/pelayananTelunjukSaktiDesa.json
new file mode 100644
index 00000000..aac11c24
--- /dev/null
+++ b/prisma/data/desa/layanan/pelayananTelunjukSaktiDesa.json
@@ -0,0 +1,20 @@
+[
+ {
+ "id": "cmdy0dwx10000vnnb6nmt06rv",
+ "name": "Telunjuk Sakti Desa Akta Kelahiran (Petunjuk Pengajuan pada link berikut : Download",
+ "deskripsi": "Akta Kelahiran",
+ "link": "https://darmasaba.desa.id/storage/files/PERSYARATAN%20DAN%20ALUR%20PENGAJUAN%20AKTA%20KELAHIRAN_(dengan%20contoh%20Formulir).pdf"
+ },
+ {
+ "id": "cmdy0ttpz0001vnnbrvr9jb3z",
+ "name": "Telunjuk Sakti Desa Akta Perkawinan (Petunjuk Pengajuan pada link berikut : Download",
+ "deskripsi": "Akta Perkawinan",
+ "link": "https://darmasaba.desa.id/storage/files/PERSYARATAN%20DAN%20ALUR%20PENGAJUAN%20AKTA%20PERKAWINAN_(dengan%20contoh%20Formulir).pdf"
+ },
+ {
+ "id": "cmdy0vjic0002vnnbcp0e9lgq",
+ "name": "Telunjuk Sakti Desa Akta Kematian (Petunjuk Pengajuan pada link berikut : Download",
+ "deskripsi": "Akta Kematian",
+ "link": "https://darmasaba.desa.id/storage/files/PERSYARATAN%20DAN%20ALUR%20PENGAJUAN%20AKTA%20KEMATIAN_(dengan%20contoh%20Formulir).pdf"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/potensi/potensi-desa.json b/prisma/data/desa/potensi/potensi-desa.json
new file mode 100644
index 00000000..9c6c8b66
--- /dev/null
+++ b/prisma/data/desa/potensi/potensi-desa.json
@@ -0,0 +1,74 @@
+[
+ {
+ "id": "cmdyamai40004vnw3sdjbvn48",
+ "name": "TPS3R Pudak Mesari",
+ "deskripsi": "TPS 3R Pudak Mesari Darmasaba layak mendapat penghargaan demikian apresiasi dari Delterra Sosial Indonesia nie Semeton Darmasaba!, Hal tersebut dikarenakan walaupun baru berdiri namun TPS 3R kebanggaan Desa Darmasaba tersebut sudah berjalan dengan sangat baik.",
+ "content": "TPS3R Pudak Mesari adalah Tempat Pengolahan Sampah dengan konsep Reduce, Reuse, dan Recycle (TPS3R) yang berlokasi di Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali. Fasilitas ini berperan penting dalam pengelolaan sampah berbasis masyarakat, dengan tujuan mengurangi volume sampah yang masuk ke Tempat Pembuangan Akhir (TPA) dan meningkatkan kesadaran warga tentang pentingnya pengelolaan sampah yang berkelanjutan.
Potensi Desa melalui TPS3R Pudak Mesari:
Peningkatan Kesehatan Lingkungan:
Dengan pengelolaan sampah yang efektif, desa dapat menjaga kebersihan lingkungan, mengurangi risiko penyakit, dan menciptakan suasana yang lebih nyaman bagi warga.
Pemberdayaan Ekonomi Masyarakat:
TPS3R membuka peluang usaha bagi warga melalui pemilahan dan pengolahan sampah, seperti produksi kompos dari sampah organik dan kerajinan tangan dari sampah anorganik yang dapat meningkatkan pendapatan masyarakat.
Edukasi dan Kesadaran Lingkungan:
Fasilitas ini dapat menjadi pusat edukasi bagi masyarakat tentang pentingnya pengelolaan sampah, mendorong partisipasi aktif dalam menjaga kelestarian lingkungan.
Pengembangan Pariwisata Berkelanjutan:
Dengan lingkungan yang bersih dan asri, Desa Darmasaba memiliki potensi untuk menarik wisatawan yang tertarik pada ekowisata dan budaya lokal, sehingga meningkatkan perekonomian desa.
"
+ },
+ {
+ "id": "cmdyb7h440003vngjapbc84f7",
+ "name": "Bumdes Pudak Mesari",
+ "deskripsi": "Bumdes Pudak Mesari sangat membantu warga desa Darmasaba dalam mengelola dan membangun sebuah desa yang lebih baik lagi",
+ "content": "Badan Usaha Milik Desa (BUMDes) Pudak Mesari adalah lembaga ekonomi desa yang berperan penting dalam pengembangan potensi dan kesejahteraan masyarakat Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali. BUMDes ini berfungsi sebagai motor penggerak perekonomian desa melalui berbagai unit usaha yang dikelola secara profesional.
Potensi dan Peran BUMDes Pudak Mesari:
Pengembangan Usaha Mikro dan Kecil:
BUMDes Pudak Mesari menyediakan layanan bagi pelaku usaha mikro dan kecil di desa, seperti penyediaan konsumsi dan snack kotak untuk berbagai acara.
Pengelolaan Sampah Berbasis Masyarakat:
Melalui kolaborasi dengan komunitas pemuda peduli lingkungan, BUMDes Pudak Mesari aktif dalam pengelolaan sampah berbasis masyarakat.
Peningkatan Kapasitas dan Transparansi:
Untuk memastikan pengelolaan yang akuntabel, BUMDes Pudak Mesari rutin mengadakan rapat koordinasi dan pendampingan penyusunan laporan pertanggungjawaban.
Kolaborasi Internasional:
Desa Darmasaba, melalui BUMDes Pudak Mesari, menerima kunjungan dari tim Osaki Jepang untuk memperkuat pengelolaan sampah dan lingkungan.
Dengan berbagai inisiatif tersebut, BUMDes Pudak Mesari menunjukkan perannya sebagai pilar utama dalam pengembangan ekonomi dan kesejahteraan masyarakat Desa Darmasaba, sekaligus menjaga kelestarian lingkungan melalui program-program inovatif dan kolaboratif.
"
+ },
+ {
+ "id": "cmdybb53i0007vngjet38spn8",
+ "name": "Taman Beji Cengana",
+ "deskripsi": "Tirta Klebutan di Pura Taman Beji Cengana di Desa Adat Darmasaba, Badung, selain dipercaya nunas Taksu serta pembersihan diri. Tersemat juga asal usul cerita ditemukannya Tirta Klebutan yang tepat berada di pinggir Tukad Cengana tersebut.",
+ "content": "Taman Beji Cengana, terletak di Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali, adalah situs suci yang memiliki nilai spiritual dan sejarah yang tinggi. Tempat ini dikenal sebagai lokasi untuk ritual pembersihan diri (melukat) dan peribadatan oleh umat Hindu Bali. Keberadaan mata air suci (Tirta Klebutan) di Taman Beji Cengana dipercaya memberikan berkah dan penyucian bagi mereka yang datang untuk berdoa dan melakukan ritual.
Potensi Desa melalui Taman Beji Cengana:
Pengembangan Pariwisata Spiritual:
Taman Beji Cengana memiliki potensi besar sebagai destinasi wisata spiritual. Wisatawan yang mencari pengalaman spiritual dan ketenangan batin dapat tertarik untuk mengunjungi tempat ini, mengikuti ritual melukat, dan merasakan suasana sakral yang ditawarkan.
Pelestarian Budaya dan Tradisi:
Dengan mempromosikan Taman Beji Cengana sebagai pusat kegiatan budaya dan ritual tradisional, desa dapat memastikan bahwa warisan budaya dan tradisi lokal tetap lestari.
Pendidikan dan Penelitian:
Taman Beji Cengana dapat dijadikan sebagai pusat pendidikan dan penelitian bagi akademisi, peneliti, dan pelajar yang tertarik mempelajari budaya, agama, dan sejarah Bali.
Pengembangan Ekonomi Kreatif:
Dengan meningkatnya jumlah pengunjung ke Taman Beji Cengana, peluang bagi pengembangan ekonomi kreatif juga terbuka lebar. Masyarakat lokal dapat mengembangkan produk kerajinan tangan, kuliner khas, dan suvenir yang mencerminkan budaya dan tradisi desa.
Konservasi Lingkungan:
Sebagai situs suci dengan mata air alami, Taman Beji Cengana memiliki peran penting dalam konservasi lingkungan. Upaya menjaga kebersihan dan kelestarian mata air serta lingkungan sekitarnya dapat menjadi contoh praktik konservasi yang baik.
Dengan memanfaatkan potensi yang dimiliki Taman Beji Cengana, Desa Darmasaba dapat mengembangkan sektor pariwisata, budaya, pendidikan, ekonomi, dan lingkungan secara berkelanjutan, yang pada gilirannya akan meningkatkan kesejahteraan masyarakat dan pelestarian warisan budaya.
"
+ },
+ {
+ "id": "cmdybckcz000avngjfzpy60uk",
+ "name": "Gumuh Sari Water Park",
+ "deskripsi": "Gumuh Sari Rekreasi atau waterpark, tempat wisata yang asyik dan seru untuk kamu sekeluarga! Tempat liburan di Bali memang seakan nggak ada habisnya. Selalu ada aja destinasi wisata seru yang bisa jadi wishlist. Ada banyak banget tempat wisata yang kamu kunjungi di Bali, mulai dari wisata alam, wisata modern, sampai wisata air.",
+ "content": "Gumuh Sari Waterpark, terletak di Jl. Tegal Gumuh No. 9, Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali, adalah destinasi rekreasi yang menawarkan berbagai fasilitas untuk pengunjung dari segala usia. Taman rekreasi ini tidak hanya menyediakan wahana air yang menyenangkan, tetapi juga fasilitas olahraga dan kuliner, menjadikannya tempat ideal untuk rekreasi keluarga dan komunitas.
Potensi Desa melalui Gumuh Sari Waterpark:
Pengembangan Pariwisata Lokal:
Dengan adanya destinasi seperti Gumuh Sari Waterpark, Desa Darmasaba dapat menarik lebih banyak wisatawan lokal maupun mancanegara. Kehadiran pengunjung ini berpotensi meningkatkan pendapatan desa dan membuka peluang usaha baru bagi masyarakat setempat.
Peningkatan Ekonomi Masyarakat:
Fasilitas seperti restoran dan pusat olahraga di dalam kompleks waterpark memberikan peluang bagi warga lokal untuk terlibat dalam sektor jasa dan perdagangan. Hal ini dapat menciptakan lapangan pekerjaan dan mendukung pertumbuhan ekonomi desa.
Pengembangan Fasilitas Olahraga dan Kesehatan:
Dengan adanya pusat futsal dan gym, Gumuh Sari Waterpark mendorong masyarakat untuk berpartisipasi dalam kegiatan olahraga, yang dapat meningkatkan kesehatan dan kesejahteraan warga.
Pemberdayaan Komunitas Melalui Event dan Acara:
Waterpark ini sering menjadi tuan rumah berbagai acara komunitas, seperti pesta busa dan bola, yang dapat mempererat hubungan antarwarga dan menciptakan lingkungan yang harmonis.
Peningkatan Infrastruktur dan Aksesibilitas:
Dengan meningkatnya jumlah pengunjung, infrastruktur desa seperti jalan, transportasi, dan layanan umum lainnya akan berkembang untuk memenuhi kebutuhan tersebut, yang pada gilirannya meningkatkan kualitas hidup masyarakat setempat.
Melalui pengelolaan dan pengembangan yang tepat, Gumuh Sari Waterpark dapat menjadi motor penggerak bagi kemajuan Desa Darmasaba, meningkatkan kesejahteraan masyarakat, dan menjadikan desa ini sebagai destinasi wisata yang dikenal luas.
"
+ },
+ {
+ "id": "cmdyjuij40002vns5qyyjmzf4",
+ "name": "Pertanian",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi pertanian yang besar sebagai bagian dari warisan agraris yang telah diwariskan secara turun-temurun. Dengan kondisi tanah yang subur serta sistem irigasi tradisional subak, pertanian di Darmasaba memainkan peran penting dalam ekonomi dan keberlanjutan lingkungan desa.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi pertanian yang besar sebagai bagian dari warisan agraris yang telah diwariskan secara turun-temurun. Dengan kondisi tanah yang subur serta sistem irigasi tradisional subak, pertanian di Darmasaba memainkan peran penting dalam ekonomi dan keberlanjutan lingkungan desa.
Potensi Desa melalui Pertanian:
Potensi dan Komoditas Unggulan
Pertanian di Desa Darmasaba mengandalkan berbagai jenis tanaman yang memiliki nilai ekonomi tinggi, di antaranya:
- Padi : Sebagai salah satu desa yang masih mempertahankan sistem subak, Darmasaba menjadi bagian dari lumbung pangan di Bali.
- Sayur-mayur : Beberapa jenis sayuran seperti kangkung, bayam, cabai, dan tomat banyak dibudidayakan oleh petani lokal.
- Buah-buahan tropis : Termasuk pisang, mangga, dan kelapa, yang menjadi sumber pendapatan tambahan bagi petani.
- Tanaman obat dan rempah : Seperti jahe, kunyit, dan lengkuas, yang memiliki permintaan tinggi baik untuk kebutuhan rumah tangga maupun industri herbal.
Sistem Irigasi Tradisional Subak:
Sebagai bagian dari warisan budaya Bali, sistem irigasi subak masih diterapkan di Darmasaba. Sistem ini memungkinkan distribusi air yang adil di antara lahan pertanian dan membantu menjaga keberlanjutan produksi pangan desa.
Pengembangan Pertanian Organik:
Dengan meningkatnya kesadaran akan pentingnya produk sehat dan ramah lingkungan, beberapa petani di Darmasaba mulai beralih ke metode pertanian organik. Hal ini membuka peluang bagi desa untuk mengembangkan produk-produk pertanian yang memiliki nilai jual lebih tinggi.
Agrowisata sebagai Sumber Pendapatan Baru:
Potensi pertanian Darmasaba juga dapat dikembangkan menjadi agrowisata, di mana wisatawan dapat merasakan pengalaman langsung dalam bertani, mengikuti workshop bercocok tanam, serta menikmati hasil pertanian segar. Hal ini dapat menarik wisatawan lokal maupun mancanegara, meningkatkan perekonomian desa.
Pemberdayaan Petani dan UMKM Berbasis Pertanian:
Dengan adanya BUMDes Pudak Mesari dan dukungan dari pemerintah setempat, petani di Darmasaba dapat diberikan pelatihan dan akses pasar yang lebih luas. Produk pertanian dapat diolah menjadi berbagai produk turunan seperti keripik pisang, sambal khas desa, hingga minuman herbal yang dapat dipasarkan ke luar daerah.
Dengan berbagai potensi yang dimiliki, pertanian di Desa Darmasaba dapat terus berkembang melalui inovasi dan pemanfaatan teknologi pertanian modern. Dukungan dari masyarakat, pemerintah, dan lembaga terkait sangat penting untuk menjaga keberlanjutan sektor pertanian dan meningkatkan kesejahteraan petani di desa ini.
"
+ },
+ {
+ "id": "cmdykerrq0002vn6fy4sv7uvm",
+ "name": "Kawasan Kuliner",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi besar dalam sektor kuliner. Sebagai desa yang strategis dan terus berkembang, Darmasaba mulai dikenal sebagai destinasi kuliner yang menawarkan beragam makanan khas Bali hingga makanan modern yang menarik minat wisatawan dan masyarakat lokal.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi besar dalam sektor kuliner. Sebagai desa yang strategis dan terus berkembang, Darmasaba mulai dikenal sebagai destinasi kuliner yang menawarkan beragam makanan khas Bali hingga makanan modern yang menarik minat wisatawan dan masyarakat lokal.
Potensi Desa melalui Kawasan Kuliner:
Ragam Kuliner Khas Bali
Darmasaba memiliki banyak warung dan rumah makan yang menyajikan hidangan khas Bali yang otentik, seperti:
- Babi Guling : Salah satu kuliner favorit di Bali yang banyak ditemukan di Darmasaba.
- Ayam Betutu : Hidangan ayam berbumbu khas yang dimasak dengan teknik khas Bali.
- Lawar : Campuran daging dan sayuran berbumbu khas Bali.
- Sate Lilit : Sate khas Bali yang terbuat dari daging cincang yang dibalut pada batang serai.
Wisata Kuliner Modern & Cafe Kekinian:
Selain makanan tradisional, Darmasaba juga mulai berkembang dengan hadirnya cafe dan resto kekinian yang menyajikan menu modern seperti kopi spesial, burger, pizza, dan aneka dessert yang digemari anak muda. Keberadaan tempat-tempat ini menjadikan Darmasaba sebagai pilihan destinasi kuliner bagi wisatawan maupun warga sekitar.
Pasar Kuliner Malam:
Salah satu daya tarik Darmasaba adalah pusat kuliner malam yang menghadirkan aneka makanan kaki lima seperti nasi jinggo, tipat cantok, bakso, dan berbagai jajanan khas Bali. Suasana yang ramai dan harga yang terjangkau membuat pasar kuliner ini menjadi tempat favorit bagi masyarakat lokal.
Potensi Ekonomi & UMKM Kuliner:
Dengan berkembangnya sektor kuliner, banyak pelaku UMKM di Darmasaba mulai merintis usaha makanan, baik dalam bentuk warung makan, katering, hingga produksi makanan ringan seperti keripik, sambal, dan minuman tradisional. Potensi ini dapat terus dikembangkan dengan dukungan pemerintah desa dan promosi melalui media sosial.
Kawasan Kuliner Berbasis Pariwisata:
Untuk menarik lebih banyak pengunjung, Darmasaba berpotensi mengembangkan kawasan kuliner berbasis wisata yang menggabungkan pengalaman makan dengan konsep alam terbuka, pertunjukan seni, dan edukasi kuliner khas Bali. Hal ini dapat menjadi daya tarik tambahan bagi wisatawan yang ingin merasakan pengalaman kuliner yang lebih autentik.
Dengan kekayaan kuliner yang dimiliki, Desa Darmasaba berpotensi menjadi kawasan kuliner unggulan di Kabupaten Badung. Dukungan dari masyarakat, pemerintah desa, serta promosi yang lebih luas dapat menjadikan Darmasaba sebagai destinasi kuliner yang semakin dikenal dan berkembang.
"
+ },
+ {
+ "id": "cmdykhfhf0005vn6fnz25kify",
+ "name": "IKM Berbasis Pengolahan Pangan",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam Industri Kecil dan Menengah (IKM) berbasis pengolahan pangan. Dengan sumber daya alam yang melimpah dan warisan kuliner khas Bali, Darmasaba dapat mengembangkan sektor ini untuk meningkatkan kesejahteraan masyarakat dan menciptakan lapangan kerja baru.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam Industri Kecil dan Menengah (IKM) berbasis pengolahan pangan. Dengan sumber daya alam yang melimpah dan warisan kuliner khas Bali, Darmasaba dapat mengembangkan sektor ini untuk meningkatkan kesejahteraan masyarakat dan menciptakan lapangan kerja baru.
Potensi dan Peran IKM Berbasis Pengolahan Pangan:
Produk Unggulan Pengolahan Pangan
Beberapa produk olahan pangan yang potensial dikembangkan di Darmasaba meliputi:
- Keripik dan Snack Tradisional : Seperti keripik pisang, keripik singkong, dan rengginang.
- Sambal Khas Bali : Seperti sambal matah dan sambal embe yang banyak diminati pasar lokal dan nasional.
- Minuman Herbal dan Jamu : Berbasis rempah seperti kunyit asam, beras kencur, dan wedang jahe.
- Olahan Makanan Berbasis Kelapa : Seperti virgin coconut oil (VCO), serundeng, dan gula aren.
- Kue Tradisional Bali : Seperti jaje laklak, jaje uli, dan klepon yang dapat dikemas secara modern.
Peluang Ekonomi dan Pemberdayaan UMKM:
IKM berbasis pengolahan pangan dapat membuka peluang bagi masyarakat, terutama ibu rumah tangga dan pemuda desa, untuk berwirausaha. Dengan dukungan modal dan pelatihan dari pemerintah desa atau BUMDes Pudak Mesari, usaha kecil ini dapat berkembang menjadi industri yang lebih besar.
Digitalisasi dan Pemasaran Online:
Darmasaba dapat mengembangkan kawasan sentra IKM sebagai pusat produksi, pelatihan, dan pemasaran produk olahan pangan. Dengan adanya fasilitas ini, para pelaku usaha dapat lebih mudah berkolaborasi, meningkatkan kualitas produk, serta mendapatkan akses ke permodalan dan distribusi yang lebih luas.
Pengembangan Kawasan Sentra IKM:
Dengan berkembangnya sektor kuliner, banyak pelaku UMKM di Darmasaba mulai merintis usaha makanan, baik dalam bentuk warung makan, katering, hingga produksi makanan ringan seperti keripik, sambal, dan minuman tradisional. Potensi ini dapat terus dikembangkan dengan dukungan pemerintah desa dan promosi melalui media sosial.
Sinergi dengan Pariwisata dan Agrowisata:
Dengan berkembangnya sektor wisata di Darmasaba, produk olahan pangan dapat dijadikan suvenir khas desa. Pengunjung dapat membeli oleh-oleh seperti sambal kemasan, jajanan khas, atau minuman herbal sebagai bagian dari pengalaman wisata mereka.
IKM berbasis pengolahan pangan memiliki potensi besar untuk menjadi sektor unggulan di Desa Darmasaba. Dengan inovasi, dukungan teknologi, serta pemasaran yang baik, produk-produk lokal dapat bersaing di pasar yang lebih luas, meningkatkan kesejahteraan masyarakat, dan menjadikan Darmasaba sebagai pusat industri pangan kreatif di Kabupaten Badung.
"
+ },
+ {
+ "id": "cmdykjgmv0008vn6fwg0rr2nh",
+ "name": "Genteng",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam industri genteng yang dikelola oleh Usaha Mikro, Kecil, dan Menengah (UMKM). Sebagai desa yang masih mempertahankan nilai-nilai tradisional dalam pembangunan, industri genteng di Darmasaba berperan penting dalam penyediaan bahan bangunan berkualitas bagi masyarakat lokal maupun luar daerah.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam industri genteng yang dikelola oleh Usaha Mikro, Kecil, dan Menengah (UMKM). Sebagai desa yang masih mempertahankan nilai-nilai tradisional dalam pembangunan, industri genteng di Darmasaba berperan penting dalam penyediaan bahan bangunan berkualitas bagi masyarakat lokal maupun luar daerah.
Potensi dan Peran UMKM Genteng:
Genteng Tradisional Berkualitas Tinggi
UMKM di Darmasaba memproduksi genteng dari bahan baku pilihan seperti tanah liat berkualitas, yang menghasilkan genteng dengan daya tahan tinggi, kuat, dan cocok untuk iklim tropis. Beberapa jenis genteng yang dihasilkan meliputi:
- Genteng Tanah Liat : Kuat, tahan lama, dan memiliki estetika khas tradisional.
- Genteng Beton : Cocok untuk bangunan modern dengan ketahanan lebih tinggi.
- Genteng Keramik : Memberikan tampilan elegan dan daya serap air yang lebih rendah.
Peluang Ekonomi dan Pemberdayaan Masyarakat:
Industri genteng di Darmasaba memberikan peluang kerja bagi masyarakat setempat, terutama dalam bidang produksi, distribusi, hingga pemasaran. UMKM genteng juga mendukung keberlanjutan ekonomi desa dengan meningkatkan pendapatan warga serta mengurangi angka pengangguran.
Inovasi dan Pengembangan Teknologi
Beberapa pengrajin genteng di Darmasaba telah mulai mengadopsi teknologi modern dalam proses produksi, seperti:
- Penggunaan cetakan dan oven pembakaran efisien untuk meningkatkan kualitas dan kapasitas produksi.
- Teknik pelapisan anti bocor dan anti lumut untuk membuat genteng lebih tahan lama.
- Desain genteng inovatif yang lebih ringan dan mudah dipasang.
Pemasaran dan Ekspansi Pasar
Dengan meningkatnya pembangunan perumahan dan proyek konstruksi di Bali, permintaan akan genteng berkualitas terus bertambah. UMKM genteng Darmasaba dapat memperluas pasarnya dengan:
- Menjalin kerja sama dengan kontraktor dan pengembang properti.
- Mempromosikan produk melalui media sosial dan marketplace online.
- Menyediakan layanan custom sesuai kebutuhan pelanggan.
Keberlanjutan dan Ramah Lingkungan:
Industri genteng di Darmasaba berpotensi dikembangkan secara lebih ramah lingkungan dengan menerapkan metode produksi yang mengurangi limbah dan emisi. Pemanfaatan energi alternatif serta daur ulang bahan limbah dapat membantu menciptakan industri yang lebih berkelanjutan.
UMKM genteng di Desa Darmasaba memiliki potensi besar untuk terus berkembang sebagai sektor industri unggulan. Dengan inovasi, pemasaran yang lebih luas, serta dukungan dari pemerintah dan masyarakat, industri ini dapat meningkatkan kesejahteraan warga dan memperkuat perekonomian desa.
"
+ },
+ {
+ "id": "cmdyklax3000bvn6fdu53f3xq",
+ "name": "Peternakan Ikan Lele",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor peternakan lele. Dengan kondisi lingkungan yang mendukung serta meningkatnya permintaan ikan lele di pasaran, budidaya ikan lele dapat menjadi salah satu sektor ekonomi unggulan yang mampu meningkatkan kesejahteraan masyarakat desa.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor peternakan lele. Dengan kondisi lingkungan yang mendukung serta meningkatnya permintaan ikan lele di pasaran, budidaya ikan lele dapat menjadi salah satu sektor ekonomi unggulan yang mampu meningkatkan kesejahteraan masyarakat desa.
Potensi dan Peran Peternakan Ikan Lele:
Kondisi Lingkungan yang Mendukung
Darmasaba memiliki sumber air yang cukup serta iklim yang cocok untuk budidaya ikan lele. Kolam-kolam budidaya dapat dibuat dengan berbagai sistem, seperti:
- Kolam Terpal : Mudah dibuat dan lebih efisien dalam perawatan.
- Kolam Beton : Lebih tahan lama dan cocok untuk produksi skala besar.
- Sistem Bioflok : Teknologi modern yang dapat meningkatkan kepadatan ikan dan mengurangi limbah.
Permintaan Pasar yang Tinggi:
Lele merupakan salah satu jenis ikan yang memiliki permintaan tinggi di Bali, baik untuk konsumsi rumah tangga, warung makan, hingga restoran. Produk olahan seperti lele goreng, pecel lele, dan abon lele semakin diminati, membuka peluang besar bagi peternak lele di Darmasaba.
Inovasi dan Pengembangan Teknologi
Beberapa pengrajin genteng di Darmasaba telah mulai mengadopsi teknologi modern dalam proses produksi, seperti:
- Penggunaan cetakan dan oven pembakaran efisien untuk meningkatkan kualitas dan kapasitas produksi.
- Teknik pelapisan anti bocor dan anti lumut untuk membuat genteng lebih tahan lama.
- Desain genteng inovatif yang lebih ringan dan mudah dipasang.
Pemasaran dan Ekspansi Pasar
Dengan meningkatnya pembangunan perumahan dan proyek konstruksi di Bali, permintaan akan genteng berkualitas terus bertambah. UMKM genteng Darmasaba dapat memperluas pasarnya dengan:
- Menjalin kerja sama dengan kontraktor dan pengembang properti.
- Mempromosikan produk melalui media sosial dan marketplace online.
- Menyediakan layanan custom sesuai kebutuhan pelanggan.
Keberlanjutan dan Ramah Lingkungan:
Industri genteng di Darmasaba berpotensi dikembangkan secara lebih ramah lingkungan dengan menerapkan metode produksi yang mengurangi limbah dan emisi. Pemanfaatan energi alternatif serta daur ulang bahan limbah dapat membantu menciptakan industri yang lebih berkelanjutan.
UMKM genteng di Desa Darmasaba memiliki potensi besar untuk terus berkembang sebagai sektor industri unggulan. Dengan inovasi, pemasaran yang lebih luas, serta dukungan dari pemerintah dan masyarakat, industri ini dapat meningkatkan kesejahteraan warga dan memperkuat perekonomian desa.
"
+ },
+ {
+ "id": "cmdykpkwf000gvn6ftas2cjje",
+ "name": "Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi wisata olahraga dan rekreasi melalui Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa. Jalur jogging ini tidak hanya menjadi fasilitas olahraga bagi warga, tetapi juga berpotensi dikembangkan sebagai destinasi wisata sehat berbasis alam yang menarik bagi wisatawan lokal maupun luar daerah.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi wisata olahraga dan rekreasi melalui Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa. Jalur jogging ini tidak hanya menjadi fasilitas olahraga bagi warga, tetapi juga berpotensi dikembangkan sebagai destinasi wisata sehat berbasis alam yang menarik bagi wisatawan lokal maupun luar daerah.
Potensi dan Peran Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa:
Keindahan Alam dan Udara Segar:
Jogging track yang membentang di Tegeh Aban, Karang Gadon, dan Munduk Uma Desa menawarkan pemandangan alam yang asri dengan udara segar khas pedesaan. Jalur ini melewati area persawahan hijau, perkebunan, serta hutan kecil yang memberikan pengalaman jogging yang lebih menyenangkan dan menenangkan.
Fasilitas Olahraga dan Rekreasi
Selain untuk jogging, jalur ini juga cocok digunakan untuk:
- Bersepeda santai : Jalur yang nyaman untuk pecinta sepeda.
- Trekking ringan : Cocok bagi wisatawan yang ingin menikmati suasana pedesaan.
- Meditasi dan Yoga : Area yang tenang dan alami, ideal untuk relaksasi.
Destinasi Wisata Sehat dan Edukasi:
Jogging track ini berpotensi dikembangkan sebagai wisata sehat berbasis alam, di mana pengunjung bisa menikmati udara segar sambil berolahraga. Selain itu, jalur ini dapat dijadikan sebagai rute edukasi lingkungan, mengenalkan keanekaragaman hayati, pertanian, serta kehidupan masyarakat desa.
Potensi Ekonomi bagi Masyarakat
Dengan meningkatnya jumlah pengunjung, masyarakat sekitar dapat memanfaatkan peluang usaha seperti:
- Warung sehat dan kuliner lokal : Menyediakan makanan dan minuman sehat bagi para pengunjung.
- Jasa penyewaan sepeda : Menarik bagi wisatawan yang ingin berkeliling lebih jauh.
- Pemandu wisata lokal : Memberikan pengalaman lebih bagi wisatawan yang ingin mengenal sejarah dan budaya desa.
Pengembangan Berkelanjutan:
Agar semakin menarik, jogging track ini bisa dilengkapi dengan fasilitas tambahan seperti tempat istirahat, spot foto alami, papan informasi tentang flora dan fauna, serta area taman bunga untuk mempercantik jalur jogging.
Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa memiliki potensi besar sebagai destinasi wisata sehat dan olahraga berbasis alam. Dengan pengelolaan yang baik serta dukungan dari pemerintah desa dan masyarakat, jalur ini bisa menjadi ikon baru Desa Darmasaba yang menarik bagi wisatawan serta meningkatkan perekonomian warga setempat.
"
+ },
+ {
+ "id": "cmdykr76v000jvn6fqngibbmq",
+ "name": "Dam Tanah Putih",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi wisata olahraga dan rekreasi melalui Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa. Jalur jogging ini tidak hanya menjadi fasilitas olahraga bagi warga, tetapi juga berpotensi dikembangkan sebagai destinasi wisata sehat berbasis alam yang menarik bagi wisatawan lokal maupun luar daerah.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki Dam Tanah Putih sebagai salah satu potensi desa yang bernilai strategis. Selain berfungsi sebagai infrastruktur pengairan, dam ini juga memiliki potensi untuk dikembangkan sebagai destinasi wisata alam, edukasi, dan rekreasi bagi masyarakat lokal maupun wisatawan.
Potensi dan Peran Dam Tanah Putih:
Fungsi Utama Sebagai Sumber Pengairan
Dam Tanah Putih memiliki peran penting dalam sistem irigasi yang menopang sektor pertanian di Darmasaba. Air dari dam ini digunakan untuk:
- Mengairi sawah dan ladang : Menjamin ketersediaan air bagi petani sepanjang tahun.
- Menjaga keseimbangan ekosistem : Menjadi habitat bagi ikan air tawar dan berbagai biota air.
- Menampung air hujan : Membantu mengurangi risiko banjir dan kekeringan.
Potensi Wisata Alam dan Rekreasi
Dengan pemandangan alam yang asri dan suasana yang sejuk, Dam Tanah Putih memiliki potensi besar untuk dikembangkan sebagai tempat wisata alam. Beberapa kegiatan yang bisa dikembangkan di area ini antara lain:
- Trekking dan jogging di sekitar dam : Menikmati udara segar dan pemandangan indah.
- Berkemah dan piknik : Cocok untuk keluarga dan komunitas yang ingin menikmati alam.
- Wisata air : Seperti pemancingan atau wisata perahu kecil yang dapat menarik wisatawan.
- Spot fotografi alam : Keindahan dam dan sekitarnya menjadi latar yang menarik bagi para fotografer.
Potensi Ekonomi dan UMKM Lokal
Dengan pengembangan dam sebagai destinasi wisata, masyarakat sekitar dapat memanfaatkan peluang usaha seperti:
- Warung makan dan jajanan tradisional : Menyediakan makanan khas Bali bagi wisatawan.
- Jasa penyewaan alat rekreasi : Seperti pancing atau perahu kecil.
- Produk kerajinan tangan dan suvenir : Oleh-oleh khas Darmasaba yang menarik bagi pengunjung.
Pengembangan Konservasi dan Edukasi Lingkungan
Dam Tanah Putih juga bisa menjadi tempat edukasi lingkungan dengan konsep konservasi, di mana pengunjung bisa belajar tentang:
- Pengelolaan sumber daya air yang berkelanjutan.
- Keanekaragaman hayati di sekitar dam.
- Pentingnya ekosistem perairan bagi pertanian dan kehidupan masyarakat.
Dengan berbagai fungsi dan keindahannya, Dam Tanah Putih memiliki potensi besar untuk dikembangkan sebagai destinasi wisata alam, rekreasi, serta edukasi lingkungan. Dengan pengelolaan yang baik dan dukungan dari masyarakat serta pemerintah desa, dam ini dapat menjadi aset penting bagi Darmasaba, baik dari sisi ekonomi maupun kelestarian lingkungan.
"
+ },
+ {
+ "id": "cmdyku9qh000mvn6ft76322sv",
+ "name": "UMKM",
+ "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor Usaha Mikro, Kecil, dan Menengah (UMKM). Keberadaan UMKM di desa ini tidak hanya menjadi motor penggerak ekonomi lokal, tetapi juga mendukung pelestarian budaya dan kearifan lokal melalui berbagai produk unggulan.",
+ "content": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor Usaha Mikro, Kecil, dan Menengah (UMKM). Keberadaan UMKM di desa ini tidak hanya menjadi motor penggerak ekonomi lokal, tetapi juga mendukung pelestarian budaya dan kearifan lokal melalui berbagai produk unggulan.
Potensi dan Peran UMKM:
Kerajinan Tangan dan Produk Lokal
Darmasaba memiliki banyak pengrajin yang menghasilkan produk unik dengan nilai seni tinggi, seperti:
- Genteng dan bahan bangunan tradisional : Genteng khas Darmasaba yang berkualitas tinggi.
- Kerajinan anyaman dan ukiran : Produk berbasis rotan dan kayu yang banyak diminati pasar lokal dan internasional.
- Pakaian adat dan kain tradisional : Mendukung pelestarian budaya Bali.
Industri Kuliner Khas Darmasaba
Kuliner khas desa ini memiliki potensi besar untuk dikembangkan sebagai bisnis UMKM, seperti:
- Babi Guling : Salah satu kuliner favorit yang banyak diminati wisatawan.
- Jajanan tradisional Bali : Seperti laklak, jaja uli, dan klepon yang masih dibuat dengan cara tradisional.
- Olahan ikan lele : Seperti abon lele, lele asap, dan pecel lele yang memiliki pasar luas.
UMKM Berbasis Pengolahan Pangan
Beberapa UMKM di Darmasaba mengolah hasil pertanian dan peternakan menjadi produk bernilai tambah, seperti:
- Keripik singkong dan pisang : Camilan sehat berbasis bahan lokal.
- Olahan kelapa : Seperti minyak kelapa murni dan gula aren.
- Produk herbal dan jamu : Menggunakan bahan-bahan alami dari tanaman lokal.
Dukungan dan Pengembangan UMKM
Agar UMKM di Darmasaba semakin berkembang, perlu adanya:
- Pelatihan dan pendampingan usaha : Untuk meningkatkan kualitas produk dan manajemen usaha.
- Pemasaran digital : Menggunakan media sosial dan e-commerce untuk menjangkau pasar lebih luas.
- Kerja sama dengan BUMDes Pudak Mesari : Untuk membantu akses modal dan pengelolaan bisnis yang lebih profesional.
UMKM di Desa Darmasaba memiliki potensi besar dalam berbagai sektor, mulai dari kerajinan tangan, kuliner, hingga wisata berbasis masyarakat. Dengan inovasi, pemasaran yang lebih luas, dan dukungan dari pemerintah desa serta masyarakat, UMKM Darmasaba dapat berkembang pesat dan menjadi tulang punggung perekonomian desa.
"
+ }
+]
diff --git a/prisma/data/desa/profile/lambang_desa.json b/prisma/data/desa/profile/lambang_desa.json
new file mode 100644
index 00000000..7827289a
--- /dev/null
+++ b/prisma/data/desa/profile/lambang_desa.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Lambang Desa",
+ "deskripsi" : "Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama. Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi. Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government. Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan. Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat. Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM. Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup. Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa. Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata. Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa. Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata. "
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/profile/maskot_desa.json b/prisma/data/desa/profile/maskot_desa.json
new file mode 100644
index 00000000..b405ff8f
--- /dev/null
+++ b/prisma/data/desa/profile/maskot_desa.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Maskot Desa",
+ "deskripsi" : "Pudak adalah bunga dari tanaman sejenis pandan (Pandanaceae). Bentuk bunga ini tersusun dalam beberapa lapisan, terbungkus oleh kelopak warna putih (semacam daun lonjong) yang ujungnya meruncing.
Bunga Pudak berwarna kuning dan akan terlihat jika kelopak atau pelepahnya telah mekar. Kekhasan dari bunga pudak, yaitu mempunyai aroma wangi yang semerbak nan lembut (tidak menyengat), dan dapat menebar keharuman sepanjang pagi atau pun sore hari. Tanaman ini dapat tumbuh di sepanjang pantai, aliran sungai, di atas batu-batu karang, dan juga di tanah ladang.
Dalam Kamus Jawa Kuna- Indonesia kata “Pudak” berarti bunga pandan atau Pandanus Moschatus (Mardiwarsito: 1981: 442). Selain itu bunga pudak juga dapat disebut ketaka atau ketaki (Mardiwarsito, 1981: 276). Sedangkan kata “Sategal” berasal dari kata dasar “Tegal” yang berarti ladang (Mardiwarsito, 1981: 593). Jadi Pudak Sategal dapat diartikan sebagai satu ladang luas yang dipenuhi bunga pudak dan menabar keharuman.
Pada sebuah kesempatan, Ida Pedanda Putu Pemaron menjelaskan mengenai makna dari istilah Pudak Sategal dengan sebuah analogi bahwa, sekuntum bunga pudak memiliki aroma wangi atau keharuman yang sangat kuat, apalagi jika satu ladang penuh bunga pudak, maka dapat dipastikan aroma keharumannya akan membumbung menyebar ke segala penjuru (Wawancara, 18 Mei 2019 di Geria Putra Mandara Kenderan, Tegallalang). “Pudak” ialah sebuah bunga yang memiliki aroma wangi atau keharuman yang semerbak, lembut, dan khas.
Garapan Tari Maskot Desa Darmasaba Sekar Pudak diwujudkan ke dalam bentuk tari kreasi yang ditarikan secara berkelompok dengan jumlah lima orang penari perempuan (putri).
Pemilihan penari perempuan dimaksudkan untuk mempresentasikan keindahan, keluwesan, dan keharuman dari bunga pudak. Sedangkan penetapan jumlah penari lima orang didasarkan atas pertimbangan kebutuhan koreografi agar dapat membentuk desain-desain komposisi lantai yang menarik dan dinamis, baik ketika ditarikan di area panggung yang luas atau pun area panggung yang kecil. Penyajian tari maskot ini dirancang dengan durasi waktu 9 menit.
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/profile/profil_perbekel.json b/prisma/data/desa/profile/profil_perbekel.json
new file mode 100644
index 00000000..efaf0021
--- /dev/null
+++ b/prisma/data/desa/profile/profil_perbekel.json
@@ -0,0 +1,9 @@
+[
+ {
+ "id": "edit",
+ "biodata": "I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.
",
+ "pengalaman": "2021 - 2027: Perbekel Desa Darmasaba 2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates 2020 - Sekarang: Founder Ugawa Record Music Studio 2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar ",
+ "pengalamanOrganisasi": " 1996 – 1997: Ketua OSIS SMP Negeri 1 Abiansemal 1999 – 2000: Ketua OSIS SMA Negeri 1 Mengwi 2008 – 2009: Ketua BEM Universitas Mahasaraswati Denpasar 2008 – 2010: Ketua Sekaa Taruna Sila Dharma, Banjar Tengah, Desa Adat Tegal, Darmasaba 2020 – Sekarang: Pengurus Young Lawyer Committee Peradi Denpasar 2021 – Sekarang: Dewan Kehormatan Himpunan Pengusaha Muda Indonesia (HIPMI) Badung 2023 – 2028: Komite Tetap Advokasi – Bidang Hukum dan Regulasi Kamar Dagang dan Industri Badung ",
+ "programUnggulan": "Pemberdayaan Ekonomi dan UMKM Pelatihan dan pendampingan UMKM lokal Program bantuan modal usaha bagi pelaku usaha kecil Digitalisasi UMKM untuk meningkatkan pemasaran produk lokal "
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/profile/sejarah_desa.json b/prisma/data/desa/profile/sejarah_desa.json
new file mode 100644
index 00000000..347194e5
--- /dev/null
+++ b/prisma/data/desa/profile/sejarah_desa.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Sejarah Desa",
+ "deskripsi": "Asal – usul nama Darmasaba tertuang dalam lontar Usada Bali. Seperti di tulis dalam monografi Desa Darmasaba tahun 1980 silam, nama Darmasaba berkaitan dengan keturunan Danghyang Nirarta diceritakan, Sang kawi-wiku asal Daha (Jawa Timur) itu memiliki cucu bernama Ida Pedanda Sakti Manuaba yang tigggal di Desa Kendran Tegalalang Gianyar. Merasa tidak disenangi sang ayah, Ida Pedanda Sakti Manuaba pergi mengembara bersama dua orang pengiringnya. Pengembaraan sang pendeta sampai di pura Sarin Buana di Jimbaran. Saat mengadakan semedi di tempat ini sang pendeta melihat sinar api. Yang sangat jauh di utara. Timbul keinginan Ida Pedanda Manuaba untuk mengunjungi tempat itu. Sampailah sang Pedanda di pura Batan Bila Peguyangan. Disini Ida Pedanda Manuaba singgah menghadap Ida Pedanda Budha yang tinggal disana. Selanjutnya, kedua pendeta bersama-sama menuju arah utara dan singgah di Taman Cang Ana, sebuah taman milik Arya Lanang Blusung. Di tempat ini kedua pendeta bersama-sama melaksanakan semedi dan menetap untuk sementara waktu.
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/desa/profile/visi_misi_desa.json b/prisma/data/desa/profile/visi_misi_desa.json
new file mode 100644
index 00000000..5fd0b7bc
--- /dev/null
+++ b/prisma/data/desa/profile/visi_misi_desa.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id" : "edit",
+ "visi" : "Mewujudkan Desa Darmasaba yang sejahtera, unggul, religius, berbudaya, dan aman dengan berlandaskan Tri Hita Karana
",
+ "misi" : "Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama. Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi. Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government. Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan. Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat. Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM. Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup. Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa. Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata. "
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json b/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json
new file mode 100644
index 00000000..f64b6977
--- /dev/null
+++ b/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json
@@ -0,0 +1,99 @@
+[
+ {
+ "month": "Jan",
+ "year": 2025,
+ "totalUnemployment": 160,
+ "educatedUnemployment": 95,
+ "uneducatedUnemployment": 65,
+ "percentageChange": 0.0
+ },
+ {
+ "month": "Feb",
+ "year": 2025,
+ "totalUnemployment": 158,
+ "educatedUnemployment": 93,
+ "uneducatedUnemployment": 65,
+ "percentageChange": -1.25
+ },
+ {
+ "month": "Mar",
+ "year": 2025,
+ "totalUnemployment": 155,
+ "educatedUnemployment": 91,
+ "uneducatedUnemployment": 64,
+ "percentageChange": -1.90
+ },
+ {
+ "month": "Apr",
+ "year": 2025,
+ "totalUnemployment": 152,
+ "educatedUnemployment": 89,
+ "uneducatedUnemployment": 63,
+ "percentageChange": -1.94
+ },
+ {
+ "month": "Mei",
+ "year": 2025,
+ "totalUnemployment": 150,
+ "educatedUnemployment": 88,
+ "uneducatedUnemployment": 62,
+ "percentageChange": -1.32
+ },
+ {
+ "month": "Jun",
+ "year": 2025,
+ "totalUnemployment": 148,
+ "educatedUnemployment": 87,
+ "uneducatedUnemployment": 61,
+ "percentageChange": -1.33
+ },
+ {
+ "month": "Jul",
+ "year": 2025,
+ "totalUnemployment": 145,
+ "educatedUnemployment": 85,
+ "uneducatedUnemployment": 60,
+ "percentageChange": -2.03
+ },
+ {
+ "month": "Agu",
+ "year": 2025,
+ "totalUnemployment": 142,
+ "educatedUnemployment": 84,
+ "uneducatedUnemployment": 58,
+ "percentageChange": -2.07
+ },
+ {
+ "month": "Sep",
+ "year": 2025,
+ "totalUnemployment": 140,
+ "educatedUnemployment": 83,
+ "uneducatedUnemployment": 57,
+ "percentageChange": -1.41
+ },
+ {
+ "month": "Okt",
+ "year": 2025,
+ "totalUnemployment": 138,
+ "educatedUnemployment": 82,
+ "uneducatedUnemployment": 56,
+ "percentageChange": -1.43
+ },
+ {
+ "month": "Nov",
+ "year": 2025,
+ "totalUnemployment": 135,
+ "educatedUnemployment": 80,
+ "uneducatedUnemployment": 55,
+ "percentageChange": -2.17
+ },
+ {
+ "month": "Des",
+ "year": 2025,
+ "totalUnemployment": 132,
+ "educatedUnemployment": 78,
+ "uneducatedUnemployment": 54,
+ "percentageChange": -2.22
+ }
+]
+
\ No newline at end of file
diff --git a/prisma/data/ekonomi/pasar-desa/kategori-produk.json b/prisma/data/ekonomi/pasar-desa/kategori-produk.json
new file mode 100644
index 00000000..c8c9b59e
--- /dev/null
+++ b/prisma/data/ekonomi/pasar-desa/kategori-produk.json
@@ -0,0 +1,10 @@
+[
+ {
+ "id": "4b95bge6-012e-5ged-9552-4d8g65d44959",
+ "nama": "Makanan"
+ },
+ {
+ "id": "5c06chf7-123f-6hfe-0663-5e9h76e55060",
+ "nama": "Minuman"
+ }
+]
diff --git a/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json b/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json
new file mode 100644
index 00000000..0976a812
--- /dev/null
+++ b/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json
@@ -0,0 +1,91 @@
+[
+ {
+ "id": "cmgewz4gt000704ib91i3f169",
+ "namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
+ "gelarAkademik": "S.H.,M.H.,NL.P.",
+ "tanggalMasuk": "2020-01-01T00:00:00.000Z",
+ "email": "bagus@desa.id",
+ "telepon": "081234567891",
+ "alamat": "Jl. Raya Desa No. 1",
+ "posisiId": "kepala_desa",
+ "isActive": true
+ },
+ {
+ "id": "cmgewxfvw000004ibee5013f4",
+ "namaLengkap": "I Ketut Suwanta",
+ "gelarAkademik": "S.Pt",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "suwanta@desa.id",
+ "telepon": "081234567892",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "sekretaris_desa",
+ "isActive": true
+ },
+ {
+ "id": "cmgewxvqw000104ibgm5l8fzs",
+ "namaLengkap": "Ni Wayan Supardiati",
+ "gelarAkademik": "S.Pd",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "supardiati@desa.id",
+ "telepon": "081234567892",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kaur_keuangan",
+ "isActive": true
+ },
+ {
+ "id": "cmgewy1g9000204ib2n7hbx0i",
+ "namaLengkap": "I Wayan Agus Juni Artha Saputra",
+ "gelarAkademik": "S.T.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "agus@desa.id",
+ "telepon": "081234567892",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_menesa",
+ "isActive": true
+ },
+ {
+ "id": "cmgewybah000304ibgqhn1gm2",
+ "namaLengkap": "I Wayan Sueca",
+ "gelarAkademik": "S.H.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "sueca@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_darmasaba",
+ "isActive": true
+ },
+ {
+ "id": "cmgewygqz000404ib20sv8nvg",
+ "namaLengkap": "Si Gede Ketut Astawa",
+ "gelarAkademik": "S.T.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "astawa@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_bucu",
+ "isActive": true
+ },
+ {
+ "id": "cmgewyos1000504ibcu8o2gyk",
+ "namaLengkap": "I Kadek Arya Minarta",
+ "gelarAkademik": "S.T.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "minarta@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_gulingan",
+ "isActive": true
+ },
+ {
+ "id": "cmgewyxk7000604ib8djs3i6c",
+ "namaLengkap": "I Gede Andika Pradnya Diputra",
+ "gelarAkademik": "S.E.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "diputra@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_taman",
+ "isActive": true
+ }
+
+]
\ No newline at end of file
diff --git a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json
new file mode 100644
index 00000000..4a1699d7
--- /dev/null
+++ b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json
@@ -0,0 +1,159 @@
+[
+ [
+ {
+ "id": "kepala_desa",
+ "nama": "Kepala Desa",
+ "deskripsi": "Pemimpin desa Darmasaba",
+ "hierarki": 1,
+ "parentId": null
+ },
+ {
+ "id": "kepala_urusan",
+ "nama": "Kepala Urusan",
+ "deskripsi": "Pemimpin urusan desa Darmasaba",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "sekretaris_desa",
+ "nama": "Sekretaris Desa",
+ "deskripsi": "Pengelola administrasi desa",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kaur_keuangan",
+ "nama": "Kaur Keuangan",
+ "deskripsi": "Pengelola keuangan desa",
+ "hierarki": 3,
+ "parentId": "kaur_umum"
+ },
+ {
+ "id": "kaur_perencanaan",
+ "nama": "Kaur Perencanaan",
+ "deskripsi": "Penyusun program kerja desa",
+ "hierarki": 3,
+ "parentId": "kaur_umum"
+ },
+ {
+ "id": "kaur_umum",
+ "nama": "Kaur Umum & TU",
+ "deskripsi": "Pelayanan umum dan administrasi",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kasi_pemerintahan",
+ "nama": "Kasi Pemerintahan",
+ "deskripsi": "Urusan pemerintahan dan keamanan",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kasi_pelayanan",
+ "nama": "Kasi Pelayanan",
+ "deskripsi": "Urusan pelayanan masyarakat",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kasi_kesejahteraan",
+ "nama": "Kasi Kesejahteraan",
+ "deskripsi": "Urusan sosial dan kesejahteraan",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_cabe",
+ "nama": "Kepala Dusun Banjar Dinas Cabe",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Cabe",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_menesa",
+ "nama": "Kepala Dusun Banjar Dinas Menesa",
+ "deskripsi": "Pimpinan wilayah Banjar Menesa",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_penenjoan",
+ "nama": "Kepala Dusun Banjar Dinas Penenjoan",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Penenjoan",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_telanga",
+ "nama": "Kepala Dusun Banjar Dinas Telanga",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Telanga",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_tengah",
+ "nama": "Kepala Dusun Banjar Dinas Tengah",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Tengah",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_baler_pasar",
+ "nama": "Kepala Dusun Banjar Dinas Baler Pasar",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Baler Pasar",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_bucu",
+ "nama": "Kepala Dusun Banjar Dinas Bucu",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Bucu",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_gulingan",
+ "nama": "Kepala Dusun Banjar Dinas Gulingan",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Gulingan",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_bersih",
+ "nama": "Kepala Dusun Banjar Dinas Bersih",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Bersih",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_umahanyar",
+ "nama": "Kepala Dusun Banjar Dinas Umahanyar",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Umahanyar",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_taman",
+ "nama": "Kepala Dusun Banjar Dinas Taman",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Taman",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_darmasaba",
+ "nama": "Kepala Dusun Banjar Dinas Darmasaba",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Darmasaba",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "staf_desa",
+ "nama": "Staf Desa",
+ "deskripsi": "Staf Desa",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ }
+ ]
+ ]
+
\ No newline at end of file
diff --git a/prisma/data/file-storage.json b/prisma/data/file-storage.json
new file mode 100644
index 00000000..53c27a0b
--- /dev/null
+++ b/prisma/data/file-storage.json
@@ -0,0 +1,137 @@
+[
+ {
+ "id": "cmff0rr4z0002vn0twp333m2",
+ "name": "S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
+ "realName": "bares.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/S6RIjFaPvdQm3oq4rM4X9-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff0tnf00003vn0t3kgzi0u0",
+ "name": "_pVNEmThU5ICGa8gv3gh_-desktop.webp",
+ "realName": "bicara-darma.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/_pVNEmThU5ICGa8gv3gh_-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff0uykf0004vn0trmmxpgfh",
+ "name": "bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
+ "realName": "daves.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/bv6rdKvjxkkjUSGLQ0lvB-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff0z34f0005vn0tjtvq519p",
+ "name": "Z4hWaV04CvoE20MjccQsV-desktop.webp",
+ "realName": "mangan.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/Z4hWaV04CvoE20MjccQsV-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff38cyq000bvn0t9f01cz3f",
+ "name": "LvLAtOqWojx4sn6NjJWB9-desktop.webp",
+ "realName": "gelah-melah.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/LvLAtOqWojx4sn6NjJWB9-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff0zqvd0007vn0tv6o5hjcq",
+ "name": "gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
+ "realName": "inovasi-desa-darmasaba.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/gR2mcvAQVgJ2-rM5coYJj-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff1013m0008vn0th7t0d64d",
+ "name": "JpL-9F8-IGztMn8E2ce02-desktop.webp",
+ "realName": "pdkt.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/JpL-9F8-IGztMn8E2ce02-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff10cwq0009vn0tse8dzu3j",
+ "name": "bxAk4AsGbJTC705_IVdes-desktop.webp",
+ "realName": "sajjiana-dharma-raksaka.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/bxAk4AsGbJTC705_IVdes-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff2w5ly000avn0telhct71k",
+ "name": "Vbj_osnMJUkGEQGDTLwV--desktop.webp",
+ "realName": "perbekel.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/Vbj_osnMJUkGEQGDTLwV--desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff3joae0000vn6h8sgs0ilg",
+ "name": "7hox9spUxj56hY_EBYLnj-desktop.webp",
+ "realName": "youtube.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/7hox9spUxj56hY_EBYLnj-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff3ll130001vn6hkhls3f5y",
+ "name": "ChihV7_1eS-AGtSg9UwMv-desktop.webp",
+ "realName": "gmail.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/ChihV7_1eS-AGtSg9UwMv-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff3mtat0002vn6hs8vyyhdd",
+ "name": "z8v9ZREwOJHKGIRYauROt-desktop.webp",
+ "realName": "facebook.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/z8v9ZREwOJHKGIRYauROt-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff3nv180003vn6h5jvedidq",
+ "name": "BLjMxTKoCNE31uOURR3IU-desktop.webp",
+ "realName": "telephone-call.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/BLjMxTKoCNE31uOURR3IU-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff3oouh0004vn6hd94brzv9",
+ "name": "hkJYAeTNWK_vYaYS20w3I-desktop.webp",
+ "realName": "instagram.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/hkJYAeTNWK_vYaYS20w3I-desktop.webp",
+ "category": "image"
+ },
+ {
+ "id": "cmff3q12g0005vn6h5ojov2qa",
+ "name": "6XEoZ9SFu59COpil03Gya-desktop.webp",
+ "realName": "tiktok.png",
+ "path": "uploads/images",
+ "mimeType": "image/webp",
+ "link": "/api/fileStorage/findUnique/6XEoZ9SFu59COpil03Gya-desktop.webp",
+ "category": "image"
+ }
+]
diff --git a/prisma/data/landing-page/apbdes/apbdes.json b/prisma/data/landing-page/apbdes/apbdes.json
new file mode 100644
index 00000000..15ab0242
--- /dev/null
+++ b/prisma/data/landing-page/apbdes/apbdes.json
@@ -0,0 +1,22 @@
+[
+ {
+ "id": "cmdwq7qp60008vntw67s4j6sq",
+ "name": "Pembiayaan",
+ "jumlah": "295 M"
+ },
+ {
+ "id": "cmdwpsprc0003vntw9o4d33dr",
+ "name": "Pendapatan",
+ "jumlah": "495 M"
+ },
+ {
+ "id": "cmdwqe8xl000cvntwcuqpvdhp",
+ "name": "Belanja",
+ "jumlah": "395 M"
+ },
+ {
+ "id": "cmdwqq4b6000gvntwm07rinx4",
+ "name": "Pangan",
+ "jumlah": "285 M"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/landing-page/desa-anti-korupsi/desaantiKorpusi.json b/prisma/data/landing-page/desa-anti-korupsi/desaantiKorpusi.json
new file mode 100644
index 00000000..8c01d425
--- /dev/null
+++ b/prisma/data/landing-page/desa-anti-korupsi/desaantiKorpusi.json
@@ -0,0 +1,110 @@
+[
+ {
+ "id": "cmds9h9ko000pvnberdjnx64b",
+ "name": "1.1 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA",
+ "deskripsi": "ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA
",
+ "kategoriId": "cmds9es2o000ivnbe1o0swrvh"
+ },
+ {
+ "id": "cmds9sjmz000svnbesv2133of",
+ "name": "1.2 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA",
+ "deskripsi": "ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA
",
+ "kategoriId": "cmds9es2o000ivnbe1o0swrvh"
+ },
+ {
+ "id": "cmds9tcpi000vvnbev3ebtlnt",
+ "name": "1.3 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
+ "deskripsi": "ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN
",
+ "kategoriId": "cmds9es2o000ivnbe1o0swrvh"
+ },
+ {
+ "id": "cmds9twvj000yvnbep0pq8dzf",
+ "name": "1.4 PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA",
+ "deskripsi": "PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA
",
+ "kategoriId": "cmds9es2o000ivnbe1o0swrvh"
+ },
+ {
+ "id": "cmds9ugap0011vnbe118yv871",
+ "name": "1.5 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA",
+ "deskripsi": "ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA
",
+ "kategoriId": "cmds9es2o000ivnbe1o0swrvh"
+ },
+ {
+ "id": "cmdsa35310014vnbe6qy6l1rz",
+ "name": "2.1 ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA",
+ "deskripsi": "ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA
",
+ "kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
+ },
+ {
+ "id": "cmdsa46590017vnbepp3noso1",
+ "name": "2.2 ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH",
+ "deskripsi": "ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH
",
+ "kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
+ },
+ {
+ "id": "cmdsa5m7z001avnbe4cvfrcz0",
+ "name": "2.3 TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI",
+ "deskripsi": "TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI
",
+ "kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
+ },
+ {
+ "id": "cmdsa8q5q001dvnbemch8j24x",
+ "name": "3.1 ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT",
+ "deskripsi": "ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT
",
+ "kategoriId": "cmds9fr73000kvnbe6w281dcl"
+ },
+ {
+ "id": "cmdsa9lbi001gvnbequn2ba7m",
+ "name": "3.2 ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA",
+ "deskripsi": "ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA
",
+ "kategoriId": "cmds9fr73000kvnbe6w281dcl"
+ },
+ {
+ "id": "cmdsaa7aq001jvnbeizh04e67",
+ "name": "3.3 ADANYA KETERBUKAAN AKSES MASYARAKAT TERHADAP INFORMASI LAYANAN PEMERINTAH DESA (KESEHATAN, PENDIDIKAN, SOSIAL, LINGKUNGAN, TRANTIBUMLINMAS, PEKERJAAN UMUM) PEMBANGUNAN, KEPENDUDUKAN, KEUANGAN, DAN PELAYANAN LAINNYA",
+ "deskripsi": "ADANYA KETERBUKAAN AKSES MASYARAKAT TERHADAP INFORMASI LAYANAN PEMERINTAH DESA (KESEHATAN, PENDIDIKAN, SOSIAL, LINGKUNGAN, TRANTIBUMLINMAS, PEKERJAAN UMUM) PEMBANGUNAN, KEPENDUDUKAN, KEUANGAN, DAN PELAYANAN LAINNYA
",
+ "kategoriId": "cmds9fr73000kvnbe6w281dcl"
+ },
+ {
+ "id": "cmdsaaw8d001mvnbek3tfefrk",
+ "name": "3.4 ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT",
+ "deskripsi": "ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT
",
+ "kategoriId": "cmds9fr73000kvnbe6w281dcl"
+ },
+ {
+ "id": "cmdsabhif001pvnbepm06hry6",
+ "name": "3.5 ADANYA MAKLUMAT PELAYANAN",
+ "deskripsi": "ADANYA MAKLUMAT PELAYANAN
",
+ "kategoriId": "cmds9fr73000kvnbe6w281dcl"
+ },
+ {
+ "id": "cmdsag40b001svnbe7krq9khc",
+ "name": "4.1 ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA",
+ "deskripsi": "ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA
",
+ "kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
+ },
+ {
+ "id": "cmdsagkaf001vvnbejo26w8sa",
+ "name": "4.2 ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
+ "deskripsi": "ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN
",
+ "kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
+ },
+ {
+ "id": "cmdsah4qe001yvnbeiy3mwrvb",
+ "name": "4.3 ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA",
+ "deskripsi": "ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA
",
+ "kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
+ },
+ {
+ "id": "cmdsak5vn0021vnbemg86aab4",
+ "name": "5.1 ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
+ "deskripsi": "ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI
",
+ "kategoriId": "cmds9govb000mvnbesq8b4y99"
+ },
+ {
+ "id": "cmdsalc800024vnbezgulhgrb",
+ "name": "5.2 ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
+ "deskripsi": "ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI
",
+ "kategoriId": "cmds9govb000mvnbesq8b4y99"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json b/prisma/data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json
new file mode 100644
index 00000000..eaed17c4
--- /dev/null
+++ b/prisma/data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json
@@ -0,0 +1,22 @@
+[
+ {
+ "id": "cmds9es2o000ivnbe1o0swrvh",
+ "name": "PENGUATAN TATA LAKSANA"
+ },
+ {
+ "id": "cmds9f2ua000jvnbeksu1sfwm",
+ "name": "PENGUATAN PENGAWASAN"
+ },
+ {
+ "id": "cmds9fr73000kvnbe6w281dcl",
+ "name": "PENGUATAN KUALITAS PELAYANAN PUBLIK"
+ },
+ {
+ "id": "cmds9g5ow000lvnbel3rkkwrv",
+ "name": "PENGUATAN PARTISIPASI MASYARAKAT"
+ },
+ {
+ "id": "cmds9govb000mvnbesq8b4y99",
+ "name": "KEARIFAN LOKAL"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/landing-page/penghargaan/penghargaan.json b/prisma/data/landing-page/penghargaan/penghargaan.json
new file mode 100644
index 00000000..cf728be9
--- /dev/null
+++ b/prisma/data/landing-page/penghargaan/penghargaan.json
@@ -0,0 +1,26 @@
+[
+ {
+ "id" : "cmdzdewrs0003vnp0yh1klh0m",
+ "name" : "Penghargaan Bhawana Sewaka Nugraha kepada Perbekel Darmasaba",
+ "juara" : "Penghargaan Bhawana Sewaka Nugraha kepada Perbekel Darmasaba",
+ "deskripsi" : "Pada hari Jumat, 27 Desember 2024, sebuah momen membanggakan tercipta bagi Desa Darmasaba. Perbekel Darmasaba, Ida Bagus Surya Prabhawa Manuaba, S.H., M.H., NL.P., menerima Penghargaan Bhawana Sewaka Nugraha sebagai Penggiat Lingkungan Hidup. Penghargaan bergengsi ini diserahkan langsung oleh Bapak Irjen. Pol. (Purn.) Drs. Sang Made Mahendra Jaya, M.H., selaku Pejabat Gubernur Provinsi Bali, dalam sebuah acara resmi yang berlangsung di Gedung Jaya Sabha, Denpasar. Penghargaan ini merupakan pengakuan atas dedikasi, kerja keras, dan komitmen Perbekel Darmasaba dalam menjaga kelestarian lingkungan serta menginspirasi masyarakat untuk menciptakan desa yang lebih hijau, bersih, dan berkelanjutan. Semoga prestasi ini tidak hanya menjadi kebanggaan bagi Desa Darmasaba, tetapi juga memotivasi kita semua untuk terus menjaga lingkungan demi masa depan yang lebih baik. Mari bersama-sama wujudkan Bali yang lestari dan berkelanjutan!
"
+ },
+ {
+ "id" : "cmdzdlwqe0006vnp0lsrp5ybn",
+ "name" : "DESA DARMASABA KEMBALI MERAIH PERINGKAT V DALAM AJANG MANGUPURA AWARD",
+ "juara" : "5",
+ "deskripsi" : "Bangga, Darmasaba Berprestasi!
Pada hari Senin, 18 November 2024, Desa Darmasaba kembali mencetak prestasi gemilang dengan meraih peringkat V dalam ajang bergengsi Mangupura Award. Penganugerahan ini berlangsung dalam rangkaian acara HUT Kota Mangupura ke-15 yang dihadiri oleh Drs. I Ketut Suiasa, S.H., Plt Bupati Badung, serta para tokoh penting lainnya.
Penghargaan ini merupakan buah kerja keras seluruh elemen masyarakat Darmasaba yang telah berpartisipasi aktif dalam memajukan desa. Proses menuju penghargaan ini melalui tahapan yang menantang, yaitu: - Presentasi Desa pada tanggal 19 Juli 2024 - Verifikasi Faktual Lapangan oleh Tim Juri Mangupura Award pada tanggal 5 September 2024
✨ Penghargaan ini bukan hanya sebuah pengakuan, tetapi juga menjadi motivasi bagi Desa Darmasaba untuk terus meningkatkan inovasi, pelayanan, pembangunan serta transformasi digital. Mari bersama kita lanjutkan semangat kolaborasi dan inovasi untuk menjadikan Darmasaba sebagai desa yang semakin unggul dan inspiratif!
#DesaDarmasaba #MangupuraAward2024 #BanggaDarmasaba #InovasiDesa #HUTKotaMangupura15 #KemajuanDesa #PemdesDarmasaba #PerbekelDarmasaba #DarmasabaBisa #DarmasabaJuara @surya_prabhawa @kecamatanabiansemal @dpmdbadungkab @pemkabbadung @prokompimbadung @seputar_darmasaba
"
+ },
+ {
+ "id" : "cmdzdorcb000avnp0ldlsx73f",
+ "name" : "JEGEG DARMASABA MERAIH JUARA 2 DUTA INVESTASI BADUNG 2024",
+ "juara" : "2",
+ "deskripsi" : "JUARA 2
DUTA INVESTASI KAB. BADUNG
TAHUN 2024
Selamat kepada Ni Made Amelia Prasetya Putri (Jegeg Darmasaba Th 2023) telah berhasil mengharumkan nama Desa Darmasaba dengan meraih Juara 2 Duta Invenstasi Kab. Badung dalam ajang Badung Investment Week 2024. Setelah bersaing dengan 40an peserta lainnya dan support penuh dari Pemdes Darmasaba, usaha memang tidak pernah menghianati hasil.
"
+ },
+ {
+ "id" : "cmdzdq8ns000dvnp0v917pevj",
+ "name" : "DARMASABA BORONG JUARA BADUNG INVESTMENT AWARD 2024",
+ "juara" : "DARMASABA BORONG JUARA BADUNG INVESTMENT AWARD 2024",
+ "deskripsi" : "DARMASABA MEMBORONG JUARA
Darmasaba bisa, Darmasaba juara,
Pemdes Darmasaba tak henti-henti menorehkan prestasi nie Semeton Darmasaba!
Pemdes Darmasaba berpartisipasi aktif dalam kegiatan Badung Invesment Week Tahun 2024 yang diselenggarakan oleh Dinas Penanaman Modal dan Pelayanan Terpadu Satu Pintu Kab. Badung. Pemdes Darmasaba melalui talenta-talenta terbaiknya mampu meraih prestasi dalam ajang tersebut diantaranya:
1. Juara 2 Lomba Video Pendek Potensi dan Peluang Investasi di Desa Kabupaten Badung 2024.
2. Juara 2 Duta Invenstasi Kabupaten Badung 2024.
3. Juara Favorit Lomba Video Pendek Potensi dan Peluang Investasi di Desa Kabupaten Badung 2024.
Penyerahan penghargaan lomba-lomba tersebut diserahkan dalam acara Badung Invesment Award 2024 pada hari Jumat (18/10/2024) bertempat di Balai Budaya Giri Nata Mandala Puspem Kab. Badung. Penghargaan Juara Lomba diserahkan langsung oleh Plt. Bupati Badung Drs. I Ketut Suiasa, S.H. didampingi Kadis DPMPTSP Kab. Badung Dr. Ir. I Made Agus Aryawan ST., MT.
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/landing-page/prestasi-desa/kategori-prestasi.json b/prisma/data/landing-page/prestasi-desa/kategori-prestasi.json
new file mode 100644
index 00000000..47791c6d
--- /dev/null
+++ b/prisma/data/landing-page/prestasi-desa/kategori-prestasi.json
@@ -0,0 +1,14 @@
+[
+ {
+ "id": "cmdwrolsl0000vnd3e24q5440",
+ "name": "Olahraga dan Kepemudaan"
+ },
+ {
+ "id": "cmdwrot900001vnd30b5kj96g",
+ "name": "Hukum dan Kesadaran Masyarakat"
+ },
+ {
+ "id": "cmdwrp0pr0002vnd35w6nkjh0",
+ "name": "Tata Kelola dan Inovasi Desa"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/landing-page/prestasi-desa/prestasi-desa.json b/prisma/data/landing-page/prestasi-desa/prestasi-desa.json
new file mode 100644
index 00000000..f9d9bff7
--- /dev/null
+++ b/prisma/data/landing-page/prestasi-desa/prestasi-desa.json
@@ -0,0 +1,20 @@
+[
+ {
+ "id": "cmdwrrxkh0005vnd3p5rxkiev",
+ "name": "Tim Bola Voli Putri Dharma Temaja meraih juara 3 dalam Turnamen Bola Voli Mangupura Cup 2024 kategori Putri Se-Bali",
+ "deskripsi": "Tim Bola Voli Putri Dharma Temaja meraih juara 3 dalam Turnamen Bola Voli Mangupura Cup 2024 kategori Putri Se-Bali
",
+ "kategoriId": "cmdwrolsl0000vnd3e24q5440"
+ },
+ {
+ "id": "cmdwrzs740008vnd329ysez5x",
+ "name": "Prestasi Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung Tahun 2024",
+ "deskripsi": "Prestasi Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung Tahun 2024
",
+ "kategoriId": "cmdwrot900001vnd30b5kj96g"
+ },
+ {
+ "id": "cmdws0sgq000bvnd32o7m94im",
+ "name": "Peringkat 5 Dalam Ajang Bergengsi Mangupura Award",
+ "deskripsi": "Peringkat 5 Dalam Ajang Bergengsi Mangupura Award
",
+ "kategoriId": "cmdwrp0pr0002vnd35w6nkjh0"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/landing-page/profile/mediaSosial.json b/prisma/data/landing-page/profile/mediaSosial.json
new file mode 100644
index 00000000..9af092a0
--- /dev/null
+++ b/prisma/data/landing-page/profile/mediaSosial.json
@@ -0,0 +1,26 @@
+[
+ {
+ "id": "cmds9023u0008vnbe3oxmhwyf",
+ "name": "Desa Darmasaba",
+ "iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg",
+ "imageId": "cmff3joae0000vn6h8sgs0ilg"
+ },
+ {
+ "id": "cmds90oul000bvnbe2bqkptoi",
+ "name": "Pemerintah Desa Darmasaba",
+ "iconUrl": "https://www.facebook.com/DarmasabaDesaku",
+ "imageId": "cmff3mtat0002vn6hs8vyyhdd"
+ },
+ {
+ "id": "cmds91i4e000evnbe8gtf1gub",
+ "name": "ddarmasaba",
+ "iconUrl": "https://www.instagram.com/ddarmasaba/",
+ "imageId": "cmff3oouh0004vn6hd94brzv9"
+ },
+ {
+ "id": "cmds92de5000hvnbemlu6sq5x",
+ "name": "desa.darmasaba",
+ "iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc",
+ "imageId": "cmff3q12g0005vn6h5ojov2qa"
+ }
+]
diff --git a/prisma/data/landing-page/profile/profile.json b/prisma/data/landing-page/profile/profile.json
new file mode 100644
index 00000000..a83a8f62
--- /dev/null
+++ b/prisma/data/landing-page/profile/profile.json
@@ -0,0 +1,8 @@
+[
+ {
+ "id": "edit",
+ "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
+ "position": "Perbekel Darmasaba periode 2021-2027",
+ "imageId": "cmff2w5ly000avn0telhct71k"
+ }
+]
diff --git a/prisma/data/landing-page/profile/programInovasi.json b/prisma/data/landing-page/profile/programInovasi.json
new file mode 100644
index 00000000..f6603f6d
--- /dev/null
+++ b/prisma/data/landing-page/profile/programInovasi.json
@@ -0,0 +1,51 @@
+[
+ {
+ "id": "cmdr755pf0005vn5rp8tyuubw",
+ "name": "Dmangan",
+ "description": "Darmasaba Aman Pangan",
+ "link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024",
+ "imageId" : "cmff0z34f0005vn0tjtvq519p"
+ },
+ {
+ "id": "cmdr76nqk0008vn5rdddvcxnr",
+ "name": "Bicara Darmasaba",
+ "description": "Bicara Darmasaba",
+ "link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba",
+ "imageId" : "cmff0tnf00003vn0t3kgzi0u0"
+ },
+ {
+ "id": "cmdr77vbw000bvn5rvpmoq31s",
+ "name": "Bares",
+ "description": "Darmasaba Recycling Stock/Exchange",
+ "link": "http://darmasaba.desa.id/berita/56722-bares",
+ "imageId" : "cmff0rr4z0002vn0twp333m2"
+ },
+ {
+ "id": "cmdr7bxtp000evn5rmy85wihx",
+ "name": "Sajjana Dharma Raksaka",
+ "description": "Sajjana Dharma Raksaka",
+ "link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf",
+ "imageId" : "cmff10cwq0009vn0tse8dzu3j"
+ },
+ {
+ "id": "cmdr7dlnk000hvn5r9lur3z35",
+ "name": "PDKT",
+ "description": "Perangkat Desa Kuat Teknologi",
+ "link": "https://darmasaba.desa.id/berita/53752-p-d-k-t",
+ "imageId" : "cmff1013m0008vn0th7t0d64d"
+ },
+ {
+ "id": "cmdr7ftob000mvn5rfhgdtg8v",
+ "name": "GM",
+ "description": "Galah Melah",
+ "link": "https://darmasaba.desa.id/berita/52880-galah-melah",
+ "imageId" : "cmff38cyq000bvn0t9f01cz3f"
+ },
+ {
+ "id": "cmdr7glue000pvn5r6onzslju",
+ "name": "Inovasi Desa Darmasaba",
+ "description": "Inovasi Desa Darmasaba",
+ "link": "https://darmasaba.desa.id/produk-lokal-desa",
+ "imageId" : "cmff0zqvd0007vn0tv6o5hjcq"
+ }
+]
diff --git a/prisma/data/landing-page/sdgs-desa/sdgs-desa.json b/prisma/data/landing-page/sdgs-desa/sdgs-desa.json
new file mode 100644
index 00000000..9f1fc623
--- /dev/null
+++ b/prisma/data/landing-page/sdgs-desa/sdgs-desa.json
@@ -0,0 +1,114 @@
+[
+ {
+ "id": "cmdsjzdl30002vneknuvo4irv",
+ "name": "Desa Tanpa Kemiskinan",
+ "jumlah": "52.62",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskargd0005vnek0mu2ofk9",
+ "name": "Desa Tanpa Kelaparan",
+ "jumlah": "35.75",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskbvl0008vnek5dmieatb",
+ "name": "Desa Sehat Dan Sejahtera",
+ "jumlah": "77.37",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskcx91000bvneko7tuaoqa",
+ "name": "Pendidikan Desa Berkualitas",
+ "jumlah": "34.11",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskjare000evnek1hglu0x8",
+ "name": "Keterlibatan Perempuan Desa",
+ "jumlah": "45.70",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskqcpc0002vnvnqjkqgm92",
+ "name": "Desa Layak Air Bersih Dan Sanitasi",
+ "jumlah": "48.54",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsktl3x0005vnvne15seefw",
+ "name": "Desa Berenergi Bersih Dan Terbarukan",
+ "jumlah": "99.64",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskuncw0008vnvcsdqoeog",
+ "name": "Pertumbuhan Ekonomi Desa Merata",
+ "jumlah": "40.92",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskw83j000bvvn9szqrea6",
+ "name": "Infrastruktur Dan Inovasi Desa Sesuai Kebutuhan",
+ "jumlah": "35.37",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskwrq7000envnvy0c5nbgf",
+ "name": "Desa Tanpa Kesenjangan",
+ "jumlah": "35.47",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskxivx000hnvnvsx520gv1",
+ "name": "Kawasan Pemukiman Desa Aman Dan Nyaman",
+ "jumlah": "40.35",
+ "imageId": ""
+ },
+ {
+ "id": "cmdskzg4c000kvnnkiv61gkt",
+ "name": "Konsumsi Dan Produksi Desa Sadar Lingkungan",
+ "jumlah": "16.67",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsl07lk000nvnnvnrepsdy5m",
+ "name": "Desa Tanggap Perubahan Iklim",
+ "jumlah": "0.00",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsl10rq000qvnvnlch9c1yv",
+ "name": "Desa Peduli Lingkungan Laut",
+ "jumlah": "50.00",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsl1mc2000tvnvn357n8usi",
+ "name": "Desa Peduli Lingkungan Darat",
+ "jumlah": "0.00",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsl2bx3000wvnvntshi4gnj",
+ "name": "Desa Damai Berkeadilan",
+ "jumlah": "78.65",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsl2yz3000zvnvnmf60ok7q",
+ "name": "Kemitraan Untuk Pembangunan Desa",
+ "jumlah": "20.00",
+ "imageId": ""
+ },
+ {
+ "id": "cmdsl492h0012vnvnmckm3n2x",
+ "name": "Kelembagaan Desa Dinamis Dan Budaya Desa Adaptif",
+ "jumlah": "47.22",
+ "imageId": ""
+ }
+]
+
+
+
+
diff --git a/prisma/data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json b/prisma/data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json
new file mode 100644
index 00000000..a596e339
--- /dev/null
+++ b/prisma/data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Contoh Kegiatan di Desa Darmasaba",
+ "deskripsi": "Pelatihan membuat kompos dari sampah rumah tangga
Gerakan Jumat Bersih rutin
Workshop membuat ecobrick
Lomba kebersihan antar banjar
Sosialisasi lingkungan di sekolah dan posyandu
"
+ }
+ ]
\ No newline at end of file
diff --git a/prisma/data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json b/prisma/data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json
new file mode 100644
index 00000000..4311e767
--- /dev/null
+++ b/prisma/data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Materi Edukasi yang Diberikan",
+ "deskripsi": "Pengelolaan Sampah (Pilah sampah organik dan anorganik)
Pencegahan pencemaran lingkungan (air, udara, dan tanah)
Pemanfaatan lahan hijau dan penghijauan desa
Daur ulang dan kreatifitas dari sampah
Bahaya pembakaran sampah sembarangan
"
+ }
+]
diff --git a/prisma/data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json b/prisma/data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json
new file mode 100644
index 00000000..94621b82
--- /dev/null
+++ b/prisma/data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Tujuan Edukasi Lingkungan",
+ "deskripsi": "Meningkatkan kesadaran masyarakat tentang pentingnya lingkungan bersih dan sehat
Mendorong partisipasi warga dalam kegiatan pengelolaan sampah, penghijauan, dan konservasi
Mengurangi dampak negatif terhadap lingkungan dari kegiatan manusia
Membentuk generasi muda yang peduli terhadap isu-isu lingkungan
"
+ }
+]
diff --git a/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json
new file mode 100644
index 00000000..874e2e32
--- /dev/null
+++ b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json
@@ -0,0 +1,6 @@
+[
+ { "nama": "Kebersihan" },
+ { "nama": "Infrastruktur" },
+ { "nama": "Sosial" },
+ { "nama": "Lingkungan" }
+ ]
\ No newline at end of file
diff --git a/prisma/data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json b/prisma/data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json
new file mode 100644
index 00000000..f4bb253f
--- /dev/null
+++ b/prisma/data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Bentuk Konservasi Berdasarkan Adat",
+ "deskripsi": "Pelestarian Hutan Adat seperti Alas Pala Sangeh atau Wana Kerthi
Subak: Sistem pengelolaan irigasi tradisional yang menjunjung kebersamaan dan keberlanjutan
Hari Raya Tumpek Uduh: Perayaan khusus untuk menghormati pohon dan tumbuhan
Perarem dan Awig-Awig: Aturan adat desa yang mengatur larangan menebang pohon sembarangan, membuang limbah ke sungai, dll.
Ritual penyucian alam seperti Melasti, Piodalan Segara, dan lainnya
"
+ }
+ ]
\ No newline at end of file
diff --git a/prisma/data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json b/prisma/data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json
new file mode 100644
index 00000000..fbbc5366
--- /dev/null
+++ b/prisma/data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Filosofi Tri Hita Karuna",
+ "deskripsi": "Parahyangan: Hubungan manusia dengan Tuhan
Pawongan: Hubungan antar manusia
Palemahan: Hubungan manusia dengan alam
"
+ }
+ ]
\ No newline at end of file
diff --git a/prisma/data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json b/prisma/data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json
new file mode 100644
index 00000000..5c75792c
--- /dev/null
+++ b/prisma/data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Nilai Konservasi Adat",
+ "deskripsi": "Menjaga keseimbangan ekosistem
Melestarikan spiritualitas lokal dan kesucian alam
Menumbuhkan kesadaran kolektif untuk hidup selaras dengan lingkungan
Menjaga keberlangsungan sumber daya alam untuk generasi mendatang
"
+ }
+]
diff --git a/prisma/data/list-caraMemperolehInformasi.json b/prisma/data/list-caraMemperolehInformasi.json
new file mode 100644
index 00000000..c3f7fdad
--- /dev/null
+++ b/prisma/data/list-caraMemperolehInformasi.json
@@ -0,0 +1,5 @@
+[
+ {"name": "Melihat/Membaca/Mendengarkan/Mencatat"},
+ {"name": "Mendapatkan Salinan Informasi (Hardcopy)"},
+ {"name": "Mendapatkan Salinan Informasi (Softcopy)"}
+]
\ No newline at end of file
diff --git a/prisma/data/list-caraMemperolehSalinanInformasi.json b/prisma/data/list-caraMemperolehSalinanInformasi.json
new file mode 100644
index 00000000..6587f648
--- /dev/null
+++ b/prisma/data/list-caraMemperolehSalinanInformasi.json
@@ -0,0 +1,5 @@
+[
+ { "name": "Mengambil Langsung" },
+ { "name": "Dikirim Via Post" },
+ { "name": "Dikirim Via Email" }
+]
diff --git a/prisma/data/list-jenisInfromasi.json b/prisma/data/list-jenisInfromasi.json
new file mode 100644
index 00000000..393523e8
--- /dev/null
+++ b/prisma/data/list-jenisInfromasi.json
@@ -0,0 +1,6 @@
+[
+ { "name": "Keuangan Desa" },
+ { "name": "Pembangunan Desa" },
+ { "name": "Data Demografi" },
+ { "name": "Lainnya" }
+]
diff --git a/prisma/data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json b/prisma/data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json
new file mode 100644
index 00000000..8b34a3e6
--- /dev/null
+++ b/prisma/data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Fasilitas yang Disediakan",
+ "deskripsi": "Buku-buku pelajaran dan alat tulis
Ruang belajar nyaman dan kondusif
Modul latihan dan pendampingan tugas
Minuman ringan dan dukungan motivasi belajar
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json b/prisma/data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json
new file mode 100644
index 00000000..4a12868f
--- /dev/null
+++ b/prisma/data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Lokasi dan Jadwal",
+ "deskripsi": "Lokasi: Balai Banjar / Balai Desa Darmasaba / Perpustakaan Desa
Jadwal: Setiap hari Senin, Rabu, dan Jumat pukul 16.00–18.00 WITA
Peserta: Terbuka untuk semua siswa SD–SMP di wilayah desa
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json b/prisma/data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json
new file mode 100644
index 00000000..26d87bc2
--- /dev/null
+++ b/prisma/data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Tujuan Program",
+ "deskripsi": "Memberikan pendampingan belajar secara gratis bagi siswa SD hingga SMP
Membantu siswa dalam menghadapi ujian dan menyelesaikan tugas sekolah
Menumbuhkan kepercayaan diri dan kemandirian dalam belajar
Meningkatkan kesetaraan pendidikan untuk seluruh anak desa
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json b/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json
new file mode 100644
index 00000000..a2d63947
--- /dev/null
+++ b/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json
@@ -0,0 +1,9 @@
+[
+ { "id": "cmghqwjs4000404l8c5uvc300", "nama": "PAUD" },
+ { "id": "cmghqwjs4000404l8c5uvc301", "nama": "TK" },
+ { "id": "cmghqwjs4000404l8c5uvc302", "nama": "SD" },
+ { "id": "cmghqwjs4000404l8c5uvc303", "nama": "SMP" },
+ { "id": "cmghqwjs4000404l8c5uvc304", "nama": "SMA" },
+ { "id": "cmghqwjs4000404l8c5uvc305", "nama": "SMK" }
+ ]
+
\ No newline at end of file
diff --git a/prisma/data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json b/prisma/data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json
new file mode 100644
index 00000000..84e33716
--- /dev/null
+++ b/prisma/data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Tempat Kegiatan",
+ "deskripsi": "Program Pendidikan Non Formal yang diselenggarakan di Desa Darmasaba meliputi:
1) Keaksaraan Fungsional
2) Pendidikan Kesetaraan (Paket A, B, C)
3) Pelatihan Keterampilan
Menjahit, memasak, sablon, pertanian, peternakan, hingga teknologi digital
4) Kursus & Pelatihan Soft Skill
5) Pendidikan Keluarga & Parenting
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json b/prisma/data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json
new file mode 100644
index 00000000..138a014b
--- /dev/null
+++ b/prisma/data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Tempat Kegiatan",
+ "deskripsi": "Balai Desa Darmasaba
TPK, Perpustakaan Desa, atau Posyandu
Bisa juga dilakukan secara mobile atau door to door
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/pendidikan-non-formal/tujuan-program2.json b/prisma/data/pendidikan/pendidikan-non-formal/tujuan-program2.json
new file mode 100644
index 00000000..c82c5c20
--- /dev/null
+++ b/prisma/data/pendidikan/pendidikan-non-formal/tujuan-program2.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Tujuan Program",
+ "deskripsi": "Memberikan kesempatan belajar yang fleksibel bagi warga desa
Meningkatkan keterampilan hidup dan kemandirian ekonomi
Mendorong partisipasi masyarakat dalam pembangunan desa
Mengurangi angka putus sekolah dan meningkatkan kualitas SDM
"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/program-pendidikan-anak/program-unggulan.json b/prisma/data/pendidikan/program-pendidikan-anak/program-unggulan.json
new file mode 100644
index 00000000..b7d4291f
--- /dev/null
+++ b/prisma/data/pendidikan/program-pendidikan-anak/program-unggulan.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Program Unggulan",
+ "deskripsi": "Bimbingan Belajar Gratis: Untuk siswa kurang mampu
Gerakan Literasi Desa: Meningkatkan minat baca sejak dini
Pelatihan Digital untuk Anak dan Remaja
Beasiswa Anak Berprestasi & Kurang Mampu
"
+ }
+]
diff --git a/prisma/data/pendidikan/program-pendidikan-anak/tujuan-program.json b/prisma/data/pendidikan/program-pendidikan-anak/tujuan-program.json
new file mode 100644
index 00000000..8da34bb2
--- /dev/null
+++ b/prisma/data/pendidikan/program-pendidikan-anak/tujuan-program.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "edit",
+ "judul": "Tujuan Program",
+ "deskripsi": "Meningkatkan akses pendidikan yang merata dan berkualitas
Menumbuhkan semangat belajar sejak dini
Membentuk karakter anak yang berakhlak dan berwawasan lingkungan
Mendukung tumbuh kembang anak melalui pendekatan pendidikan yang holistik
"
+ }
+]
diff --git a/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json b/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json
new file mode 100644
index 00000000..731ccdb9
--- /dev/null
+++ b/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json
@@ -0,0 +1,14 @@
+[
+ {
+ "id": "cmeppcwzk0000vn5exmudcipd",
+ "jenisInformasi": "Potensi Desa",
+ "deskripsi": "“Potensi desa adalah segenap sumber daya alam dan sumber daya manusia yang dimiliki desa sebagai modal dasar yang perlu dikelola dan dikembangkan bagi kelangsungan dan perkembangan desa. Adapun potensi yang dimiliki Desa Darmasaba yaitu:
TPS3R Pudak Mesari
Bumdes Pudak Mesari
Pertanian
Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa
Taman Beji Cengana
Dam Tanah Putih
Gumuh Sari Water Park
UMKM
Kawasan Kuliner
IKM berbasis Pengolahan Pangan
Genteng
Peternakan Ikan Lele
Pemotongan Daging”
",
+ "tanggal": "2021-05-25"
+ },
+ {
+ "id": "cmeppieay0001vn5e8qe658ub",
+ "jenisInformasi": "Layanan Surat Keterangan Desa",
+ "deskripsi": "“Desa Darmasaba menyediakan berbagai jenis layanan surat keterangan untuk kebutuhan administratif, antara lain:
Surat Keterangan Domisili Organisasi
Surat Keterangan Penghasilan
Surat Keterangan Tidak Mampu
Surat Keterangan Kelahiran
Surat Keterangan Usaha
Surat Keterangan Tempat Usaha
Surat Keterangan Belum Kawin
Surat Keterangan Kelakuan Baik (Pengantar SKCK)
Surat Keterangan Kematian
Surat Keterangan Perbedaan Biodata Diri
Surat Keterangan Yatim/Piatu/Yatim Piatu Untuk surat keterangan lainnya, masyarakat dapat berkonsultasi langsung ke kantor Perbekel Darmasaba.”(Sumber: Laman Layanan Desa Darmasaba)
",
+ "tanggal": "2025-02-21"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/ppid/dasar-hukum-ppid/dasarhukumPPID.json b/prisma/data/ppid/dasar-hukum-ppid/dasarhukumPPID.json
new file mode 100644
index 00000000..a81befaf
--- /dev/null
+++ b/prisma/data/ppid/dasar-hukum-ppid/dasarhukumPPID.json
@@ -0,0 +1,7 @@
+[
+ {
+ "id": "1",
+ "judul": "DASAR HUKUM PEMBENTUKAN PPID DESA DARMASABA",
+ "content" : "UU Nomor 14 Tahun 2008 tentang Keterbukaan Informasi Publik PP Nomor 61 Tahun 2010 tentang Pelaksanaan UU 14 Tahun 2008 tentang Keterbukaan Informasi Publik Permendagri Nomor 3 Tahun 2017 tentang Pedoman Pengelolaan Pelayanan Informasi dan Dokumentasi di Lingkungan Kemendagri dan Pemerintah Daerah Peraturan Komisi Informasi Nomor 1 Tahun 2010 tentang Standar Layanan Informasi Publik Peraturan Komisi Informasi Nomor 1 Tahun 2010 tentang Standar Layanan Informasi Publik Peraturan Bupati Badung No. 42 Tahun 2017 tentang Pedoman Pengelolaan Pelayanan Informasi Publik dan Dokumentasi di Lingkungan Pemerintah Kabupaten Badung Keputusan Bupati Badung Nomor 99/049/HK/2019 tentang Pengelola Layanan Informasi dan Dokumentasi Kabupaten Badung Keputusan Perbekel Darmasaba Nomor 101 Tahun 2019 tentang Penetapan Pelaksana Teknis/Administrasi Pengelola Layanan Informasi Dan Dokumentasi di Desa Punggul Peraturan Perbekel Darmasaba Nomor 12 Tahun 2019 tentang Pedoman Pengelolaan Pelayanan Informasi Publik dan Dokumentasi di Lingkungan Pemerintah Desa Darmasaba "
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/ppid/ikm/jenis-kelamin/jenis-kelamin.json b/prisma/data/ppid/ikm/jenis-kelamin/jenis-kelamin.json
new file mode 100644
index 00000000..6998305e
--- /dev/null
+++ b/prisma/data/ppid/ikm/jenis-kelamin/jenis-kelamin.json
@@ -0,0 +1,10 @@
+[
+ {
+ "id": "cme8bt5o5000007lb9xp11unb",
+ "name": "Laki-laki"
+ },
+ {
+ "id": "cme8btctl000107lbh2hocgg8",
+ "name": "Perempuan"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/ppid/ikm/pilihan-rating-responden/rating-responden.json b/prisma/data/ppid/ikm/pilihan-rating-responden/rating-responden.json
new file mode 100644
index 00000000..92f9406f
--- /dev/null
+++ b/prisma/data/ppid/ikm/pilihan-rating-responden/rating-responden.json
@@ -0,0 +1,18 @@
+[
+ {
+ "id": "cme8buup6000207lb54q9b0az",
+ "name": "Sangat Baik"
+ },
+ {
+ "id": "cme8bv15o000307lbft9b0vzy",
+ "name": "Baik"
+ },
+ {
+ "id": "cme8bvjvu000507lbgfsveog6",
+ "name": "Kurang Baik"
+ },
+ {
+ "id": "cme8bvvm6000607lbh6rn2ubm",
+ "name": "Sangat Kurang Baik"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/ppid/ikm/umur-responden/umur-responden.json b/prisma/data/ppid/ikm/umur-responden/umur-responden.json
new file mode 100644
index 00000000..f71add3c
--- /dev/null
+++ b/prisma/data/ppid/ikm/umur-responden/umur-responden.json
@@ -0,0 +1,14 @@
+[
+ {
+ "id": "cme8bwgwu000707lbawc6fz3a",
+ "name": "Muda"
+ },
+ {
+ "id": "cme8hnx09000b07jl3ipifb1k",
+ "name": "Dewasa"
+ },
+ {
+ "id": "cme8ho7dv000c07jlc7lr4b4w",
+ "name": "Lansia"
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/ppid/profile-ppid/profilePPid.json b/prisma/data/ppid/profile-ppid/profilePPid.json
new file mode 100644
index 00000000..0c6828f7
--- /dev/null
+++ b/prisma/data/ppid/profile-ppid/profilePPid.json
@@ -0,0 +1,10 @@
+[
+ {
+ "id": "edit",
+ "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
+ "biodata": "I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar, serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.
",
+ "riwayat": " 2021 - 2027: Perbekel Desa Darmasaba 2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates 2020 - Sekarang: Founder Ugawa Record Music Studio 2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar ",
+ "pengalaman": " 1996 – 1997: Ketua OSIS SMP Negeri 1 Abiansemal 1999 – 2000: Ketua OSIS SMA Negeri 1 Mengwi 2008 – 2009: Ketua BEM Universitas Mahasaraswati Denpasar 2008 – 2010: Ketua Sekaa Taruna Sila Dharma, Banjar Tengah, Desa Adat Tegal, Darmasaba 2020 – Sekarang: Pengurus Young Lawyer Committee Peradi Denpasar 2021 – Sekarang: Dewan Kehormatan Himpunan Pengusaha Muda Indonesia (HIPMI) Badung 2023 – 2028: Komite Tetap Advokasi – Bidang Hukum dan Regulasi Kamar Dagang dan Industri Badung ",
+ "unggulan": "Pemberdayaan Ekonomi dan UMKM Pelatihan dan pendampingan UMKM lokal Program bantuan modal usaha bagi pelaku usaha kecil Digitalisasi UMKM untuk meningkatkan pemasaran produk lokal "
+ }
+]
diff --git a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json
new file mode 100644
index 00000000..713cb799
--- /dev/null
+++ b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json
@@ -0,0 +1,91 @@
+[
+ {
+ "id": "cmgewz4gt000704ib91i3f169",
+ "namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
+ "gelarAkademik": "S.H.,M.H.,NL.P.",
+ "tanggalMasuk": "2020-01-01T00:00:00.000Z",
+ "email": "bagus@desa.id",
+ "telepon": "081234567891",
+ "alamat": "Jl. Raya Desa No. 1",
+ "posisiId": "kepala_desa",
+ "isActive": true
+ },
+ {
+ "id": "cmgewxfvw000004ibee5013f4",
+ "namaLengkap": "I Ketut Suwanta",
+ "gelarAkademik": "S.Pt",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "suwanta@desa.id",
+ "telepon": "081234567892",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "sekretaris_desa",
+ "isActive": true
+ },
+ {
+ "id": "cmgewxvqw000104ibgm5l8fzs",
+ "namaLengkap": "Ni Wayan Supardiati",
+ "gelarAkademik": "S.Pd",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "supardiati@desa.id",
+ "telepon": "081234567892",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kaur_keuangan",
+ "isActive": true
+ },
+ {
+ "id": "cmgewy1g9000204ib2n7hbx0i",
+ "namaLengkap": "I Wayan Agus Juni Artha Saputra",
+ "gelarAkademik": "S.T.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "agus@desa.id",
+ "telepon": "081234567892",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_menesa",
+ "isActive": true
+ },
+ {
+ "id": "cmgewybah000304ibgqhn1gm2",
+ "namaLengkap": "I Wayan Sueca",
+ "gelarAkademik": "S.H.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "sueca@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_darmasaba",
+ "isActive": true
+ },
+ {
+ "id": "cmgewygqz000404ib20sv8nvg",
+ "namaLengkap": "Si Gede Ketut Astawa",
+ "gelarAkademik": "S.T.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "astawa@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_bucu",
+ "isActive": true
+ },
+ {
+ "id": "cmgewyos1000504ibcu8o2gyk",
+ "namaLengkap": "I Kadek Arya Minarta",
+ "gelarAkademik": "S.T.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "minarta@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_gulingan",
+ "isActive": true
+ },
+ {
+ "id": "cmgewyxk7000604ib8djs3i6c",
+ "namaLengkap": "I Gede Andika Pradnya Diputra",
+ "gelarAkademik": "S.E.",
+ "tanggalMasuk": "2020-02-01T00:00:00.000Z",
+ "email": "diputra@desa.id",
+ "telepon": "081234567893",
+ "alamat": "Jl. Raya Desa No. 2",
+ "posisiId": "kadus_banjar_dinas_taman",
+ "isActive": true
+ }
+
+ ]
\ No newline at end of file
diff --git a/prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json b/prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json
new file mode 100644
index 00000000..a186460c
--- /dev/null
+++ b/prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json
@@ -0,0 +1,158 @@
+[
+ [
+ {
+ "id": "kepala_desa",
+ "nama": "Kepala Desa",
+ "deskripsi": "Pemimpin desa Darmasaba",
+ "hierarki": 1,
+ "parentId": null
+ },
+ {
+ "id": "kepala_urusan",
+ "nama": "Kepala Urusan",
+ "deskripsi": "Pemimpin urusan desa Darmasaba",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "sekretaris_desa",
+ "nama": "Sekretaris Desa",
+ "deskripsi": "Pengelola administrasi desa",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kaur_keuangan",
+ "nama": "Kaur Keuangan",
+ "deskripsi": "Pengelola keuangan desa",
+ "hierarki": 3,
+ "parentId": "kaur_umum"
+ },
+ {
+ "id": "kaur_perencanaan",
+ "nama": "Kaur Perencanaan",
+ "deskripsi": "Penyusun program kerja desa",
+ "hierarki": 3,
+ "parentId": "kaur_umum"
+ },
+ {
+ "id": "kaur_umum",
+ "nama": "Kaur Umum & TU",
+ "deskripsi": "Pelayanan umum dan administrasi",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kasi_pemerintahan",
+ "nama": "Kasi Pemerintahan",
+ "deskripsi": "Urusan pemerintahan dan keamanan",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kasi_pelayanan",
+ "nama": "Kasi Pelayanan",
+ "deskripsi": "Urusan pelayanan masyarakat",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kasi_kesejahteraan",
+ "nama": "Kasi Kesejahteraan",
+ "deskripsi": "Urusan sosial dan kesejahteraan",
+ "hierarki": 2,
+ "parentId": "kepala_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_cabe",
+ "nama": "Kepala Dusun Banjar Dinas Cabe",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Cabe",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_menesa",
+ "nama": "Kepala Dusun Banjar Dinas Menesa",
+ "deskripsi": "Pimpinan wilayah Banjar Menesa",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_penenjoan",
+ "nama": "Kepala Dusun Banjar Dinas Penenjoan",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Penenjoan",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_telanga",
+ "nama": "Kepala Dusun Banjar Dinas Telanga",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Telanga",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_tengah",
+ "nama": "Kepala Dusun Banjar Dinas Tengah",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Tengah",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_baler_pasar",
+ "nama": "Kepala Dusun Banjar Dinas Baler Pasar",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Baler Pasar",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_bucu",
+ "nama": "Kepala Dusun Banjar Dinas Bucu",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Bucu",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_gulingan",
+ "nama": "Kepala Dusun Banjar Dinas Gulingan",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Gulingan",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_bersih",
+ "nama": "Kepala Dusun Banjar Dinas Bersih",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Bersih",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_umahanyar",
+ "nama": "Kepala Dusun Banjar Dinas Umahanyar",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Umahanyar",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_taman",
+ "nama": "Kepala Dusun Banjar Dinas Taman",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Taman",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "kadus_banjar_dinas_darmasaba",
+ "nama": "Kepala Dusun Banjar Dinas Darmasaba",
+ "deskripsi": "Pimpinan wilayah Banjar Dinas Darmasaba",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ },
+ {
+ "id": "staf_desa",
+ "nama": "Staf Desa",
+ "deskripsi": "Staf Desa",
+ "hierarki": 3,
+ "parentId": "sekretaris_desa"
+ }
+ ]
+]
diff --git a/prisma/data/ppid/visi-misi-ppid/visimisiPPID.json b/prisma/data/ppid/visi-misi-ppid/visimisiPPID.json
new file mode 100644
index 00000000..a0eb4ab3
--- /dev/null
+++ b/prisma/data/ppid/visi-misi-ppid/visimisiPPID.json
@@ -0,0 +1,8 @@
+[
+ {
+ "id": "1",
+ "misi": "Meningkatkan pengelolaan dan pelayanan informasi yang berkualitas, benar dan bertanggung jawab. Membangun dan mengembangkan sistem penyediaan dan layanan informasi. Meningkatkan dan mengembangkan kompetensi dan kualitas SDM dalam bidang pelayanan informasi. Mewujudkan keterbukaan informasi Pemerintah Desa Punggul dengan proses yang cepat, tepat, mudah dan sederhana. ",
+ "visi": "Memberikan pelayanan informasi yanng transparan dan akuntabel untuk memenuhi hak pemohon informasi sesuai dengan ketentuan peraturan perundang-undangan yang berlaku."
+
+ }
+]
\ No newline at end of file
diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json
new file mode 100644
index 00000000..b79f3928
--- /dev/null
+++ b/prisma/data/user/roles.json
@@ -0,0 +1,23 @@
+[
+ {
+ "id": "role-1",
+ "name": "ADMIN DESA",
+ "description": "Administrator Desa",
+ "permissions": ["manage_users", "manage_content", "view_reports"],
+ "isActive": true
+ },
+ {
+ "id": "role-2",
+ "name": "ADMIN KESEHATAN",
+ "description": "Administrator Bidang Kesehatan",
+ "permissions": ["manage_health_data", "view_reports"],
+ "isActive": true
+ },
+ {
+ "id": "role-3",
+ "name": "ADMIN SEKOLAH",
+ "description": "Administrator Sekolah",
+ "permissions": ["manage_school_data", "view_reports"],
+ "isActive": true
+ }
+ ]
\ No newline at end of file
diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json
new file mode 100644
index 00000000..eea2a98a
--- /dev/null
+++ b/prisma/data/user/users.json
@@ -0,0 +1,23 @@
+[
+ {
+ "id": "user-1",
+ "nama": "Admin Desa",
+ "nomor": "089647037426",
+ "roleId": "role-1",
+ "isActive": true
+ },
+ {
+ "id": "user-2",
+ "nama": "Admin Kesehatan",
+ "nomor": "082339004198",
+ "roleId": "role-2",
+ "isActive": true
+ },
+ {
+ "id": "user-3",
+ "nama": "Admin Sekolah",
+ "nomor": "085237157222",
+ "roleId": "role-3",
+ "isActive": true
+ }
+]
diff --git a/prisma/migrations/20250423072406_init/migration.sql b/prisma/migrations/20250423072406_init/migration.sql
new file mode 100644
index 00000000..ea8db494
--- /dev/null
+++ b/prisma/migrations/20250423072406_init/migration.sql
@@ -0,0 +1,355 @@
+-- CreateTable
+CREATE TABLE "Layanan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "Layanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Potensi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "Potensi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LandingPage_Layanan" (
+ "id" TEXT NOT NULL,
+ "deksripsi" TEXT NOT NULL,
+
+ CONSTRAINT "LandingPage_Layanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AppMenu" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "link" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "AppMenu_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AppMenuChild" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "link" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "appMenuId" TEXT,
+
+ CONSTRAINT "AppMenuChild_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Berita" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "image" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "katagoryBeritaId" TEXT,
+
+ CONSTRAINT "Berita_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KatagoryBerita" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KatagoryBerita_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Pengumuman" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "categoryPengumumanId" TEXT,
+
+ CONSTRAINT "Pengumuman_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "CategoryPengumuman" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "CategoryPengumuman_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Images" (
+ "id" TEXT NOT NULL,
+ "url" TEXT NOT NULL,
+ "label" TEXT NOT NULL DEFAULT 'null',
+ "active" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Images_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Videos" (
+ "id" TEXT NOT NULL,
+ "url" TEXT NOT NULL,
+ "label" TEXT NOT NULL DEFAULT 'null',
+ "active" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Videos_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GalleryFoto" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "image" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "imagesId" TEXT,
+
+ CONSTRAINT "GalleryFoto_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GalleryVideo" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "video" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "videosId" TEXT,
+
+ CONSTRAINT "GalleryVideo_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataKematian_Kelahiran" (
+ "id" SERIAL NOT NULL,
+ "tahun" TEXT NOT NULL,
+ "kematianKasar" TEXT NOT NULL,
+ "kematianBayi" TEXT NOT NULL,
+ "kelahiranKasar" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "DataKematian_Kelahiran_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FasilitasKesehatan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FasilitasKesehatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "InformasiUmum" (
+ "id" TEXT NOT NULL,
+ "fasilitas" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "jamOperasional" TEXT NOT NULL,
+ "fasilitasKesehatanId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "InformasiUmum_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LayananUnggulan" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "LayananUnggulan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DokterdanTenagaMedis" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DokterdanTenagaMedis_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FasilitasPendukung" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FasilitasPendukung_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProsedurPendaftaran" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProsedurPendaftaran_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_FasilitasKesehatanToLayananUnggulan" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_FasilitasKesehatanToLayananUnggulan_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_FasilitasKesehatanToFasilitasPendukung" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_FasilitasKesehatanToFasilitasPendukung_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_FasilitasKesehatanToProsedurPendaftaran" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_FasilitasKesehatanToProsedurPendaftaran_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_DokterdanTenagaMedisToFasilitasKesehatan" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_DokterdanTenagaMedisToFasilitasKesehatan_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Layanan_name_key" ON "Layanan"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Potensi_name_key" ON "Potensi"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AppMenu_name_key" ON "AppMenu"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AppMenuChild_name_key" ON "AppMenuChild"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KatagoryBerita_name_key" ON "KatagoryBerita"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CategoryPengumuman_name_key" ON "CategoryPengumuman"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GalleryFoto_imagesId_key" ON "GalleryFoto"("imagesId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GalleryVideo_videosId_key" ON "GalleryVideo"("videosId");
+
+-- CreateIndex
+CREATE INDEX "_FasilitasKesehatanToLayananUnggulan_B_index" ON "_FasilitasKesehatanToLayananUnggulan"("B");
+
+-- CreateIndex
+CREATE INDEX "_FasilitasKesehatanToFasilitasPendukung_B_index" ON "_FasilitasKesehatanToFasilitasPendukung"("B");
+
+-- CreateIndex
+CREATE INDEX "_FasilitasKesehatanToProsedurPendaftaran_B_index" ON "_FasilitasKesehatanToProsedurPendaftaran"("B");
+
+-- CreateIndex
+CREATE INDEX "_DokterdanTenagaMedisToFasilitasKesehatan_B_index" ON "_DokterdanTenagaMedisToFasilitasKesehatan"("B");
+
+-- AddForeignKey
+ALTER TABLE "AppMenuChild" ADD CONSTRAINT "AppMenuChild_appMenuId_fkey" FOREIGN KEY ("appMenuId") REFERENCES "AppMenu"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Berita" ADD CONSTRAINT "Berita_katagoryBeritaId_fkey" FOREIGN KEY ("katagoryBeritaId") REFERENCES "KatagoryBerita"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Pengumuman" ADD CONSTRAINT "Pengumuman_categoryPengumumanId_fkey" FOREIGN KEY ("categoryPengumumanId") REFERENCES "CategoryPengumuman"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "GalleryFoto" ADD CONSTRAINT "GalleryFoto_imagesId_fkey" FOREIGN KEY ("imagesId") REFERENCES "Images"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "GalleryVideo" ADD CONSTRAINT "GalleryVideo_videosId_fkey" FOREIGN KEY ("videosId") REFERENCES "Videos"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InformasiUmum" ADD CONSTRAINT "InformasiUmum_fasilitasKesehatanId_fkey" FOREIGN KEY ("fasilitasKesehatanId") REFERENCES "FasilitasKesehatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToLayananUnggulan" ADD CONSTRAINT "_FasilitasKesehatanToLayananUnggulan_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToLayananUnggulan" ADD CONSTRAINT "_FasilitasKesehatanToLayananUnggulan_B_fkey" FOREIGN KEY ("B") REFERENCES "LayananUnggulan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToFasilitasPendukung" ADD CONSTRAINT "_FasilitasKesehatanToFasilitasPendukung_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToFasilitasPendukung" ADD CONSTRAINT "_FasilitasKesehatanToFasilitasPendukung_B_fkey" FOREIGN KEY ("B") REFERENCES "FasilitasPendukung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToProsedurPendaftaran" ADD CONSTRAINT "_FasilitasKesehatanToProsedurPendaftaran_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToProsedurPendaftaran" ADD CONSTRAINT "_FasilitasKesehatanToProsedurPendaftaran_B_fkey" FOREIGN KEY ("B") REFERENCES "ProsedurPendaftaran"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_DokterdanTenagaMedisToFasilitasKesehatan" ADD CONSTRAINT "_DokterdanTenagaMedisToFasilitasKesehatan_A_fkey" FOREIGN KEY ("A") REFERENCES "DokterdanTenagaMedis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_DokterdanTenagaMedisToFasilitasKesehatan" ADD CONSTRAINT "_DokterdanTenagaMedisToFasilitasKesehatan_B_fkey" FOREIGN KEY ("B") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250602082355_perubahan_dikategori_berita/migration.sql b/prisma/migrations/20250602082355_perubahan_dikategori_berita/migration.sql
new file mode 100644
index 00000000..345a6de3
--- /dev/null
+++ b/prisma/migrations/20250602082355_perubahan_dikategori_berita/migration.sql
@@ -0,0 +1,582 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `image` on the `Berita` table. All the data in the column will be lost.
+ - You are about to drop the column `katagoryBeritaId` on the `Berita` table. All the data in the column will be lost.
+ - You are about to drop the column `name` on the `FasilitasPendukung` table. All the data in the column will be lost.
+ - You are about to drop the column `fasilitasKesehatanId` on the `InformasiUmum` table. All the data in the column will be lost.
+ - You are about to drop the `KatagoryBerita` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `imageId` to the `Berita` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `jadwal` to the `DokterdanTenagaMedis` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `specialist` to the `DokterdanTenagaMedis` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `content` to the `FasilitasPendukung` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "Berita" DROP CONSTRAINT "Berita_katagoryBeritaId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "InformasiUmum" DROP CONSTRAINT "InformasiUmum_fasilitasKesehatanId_fkey";
+
+-- AlterTable
+ALTER TABLE "Berita" DROP COLUMN "image",
+DROP COLUMN "katagoryBeritaId",
+ADD COLUMN "imageId" TEXT NOT NULL,
+ADD COLUMN "kategoriBeritaId" TEXT;
+
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
+
+-- AlterTable
+ALTER TABLE "DokterdanTenagaMedis" ADD COLUMN "jadwal" TEXT NOT NULL,
+ADD COLUMN "specialist" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "FasilitasPendukung" DROP COLUMN "name",
+ADD COLUMN "content" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "InformasiUmum" DROP COLUMN "fasilitasKesehatanId";
+
+-- DropTable
+DROP TABLE "KatagoryBerita";
+
+-- CreateTable
+CREATE TABLE "FileStorage" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "realName" TEXT NOT NULL,
+ "path" TEXT NOT NULL,
+ "mimeType" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "link" TEXT NOT NULL,
+
+ CONSTRAINT "FileStorage_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "VisiMisiPPID" (
+ "id" TEXT NOT NULL,
+ "visi" TEXT NOT NULL,
+ "misi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "VisiMisiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DasarHukumPPID" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DasarHukumPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProfilePPID" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "biodata" TEXT NOT NULL,
+ "riwayat" TEXT NOT NULL,
+ "pengalaman" TEXT NOT NULL,
+ "unggulan" TEXT NOT NULL,
+ "imageUrl" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProfilePPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DaftarInformasiPublik" (
+ "id" TEXT NOT NULL,
+ "nomor" SERIAL NOT NULL,
+ "jenisInformasi" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "tanggal" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DaftarInformasiPublik_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PermohonanInformasiPublik" (
+ "id" TEXT NOT NULL,
+ "nomor" SERIAL NOT NULL,
+ "name" TEXT NOT NULL,
+ "nik" TEXT NOT NULL,
+ "notelp" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "jenisInformasiDimintaId" TEXT,
+ "caraMemperolehInformasiId" TEXT,
+ "caraMemperolehSalinanInformasiId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PermohonanInformasiPublik_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisInformasiDiminta" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenisInformasiDiminta_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "CaraMemperolehInformasi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "CaraMemperolehInformasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "CaraMemperolehSalinanInformasi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "CaraMemperolehSalinanInformasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FormulirPermohonanKeberatan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "notelp" TEXT NOT NULL,
+ "alasan" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FormulirPermohonanKeberatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "IndeksKepuasanMasyarakat" (
+ "id" SERIAL NOT NULL,
+ "label" TEXT NOT NULL,
+ "kepuasan" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "IndeksKepuasanMasyarakat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GrafikBerdasarkanJenisKelamin" (
+ "id" TEXT NOT NULL,
+ "perempuan" TEXT NOT NULL,
+ "laki" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikBerdasarkanJenisKelamin_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GrafikBerdasarkanResponden" (
+ "id" TEXT NOT NULL,
+ "sangatbaik" TEXT NOT NULL,
+ "baik" TEXT NOT NULL,
+ "kurangbaik" TEXT NOT NULL,
+ "tidakbaik" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikBerdasarkanResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GrafikBerdasarkanUmur" (
+ "id" TEXT NOT NULL,
+ "remaja" TEXT NOT NULL,
+ "dewasa" TEXT NOT NULL,
+ "orangtua" TEXT NOT NULL,
+ "lansia" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikBerdasarkanUmur_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProfileDesa" (
+ "id" TEXT NOT NULL,
+ "sejarah" TEXT NOT NULL,
+ "visi" TEXT NOT NULL,
+ "misi" TEXT NOT NULL,
+ "lambang" TEXT NOT NULL,
+ "maskot" TEXT NOT NULL,
+ "profilPerbekelId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProfileDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProfilPerbekel" (
+ "id" TEXT NOT NULL,
+ "biodata" TEXT NOT NULL,
+ "pengalaman" TEXT NOT NULL,
+ "pengalamanOrganisasi" TEXT NOT NULL,
+ "programUnggulan" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProfilPerbekel_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriBerita" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriBerita_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PotensiDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategori" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PotensiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TarifDanLayanan" (
+ "id" TEXT NOT NULL,
+ "layanan" TEXT NOT NULL,
+ "tarif" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TarifDanLayanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "InformasiJadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "tanggal" TEXT NOT NULL,
+ "waktu" TEXT NOT NULL,
+ "lokasi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "InformasiJadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DeskripsiJadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DeskripsiJadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LayananJadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "LayananJadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SyaratKetentuanJadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "SyaratKetentuanJadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DokumenJadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DokumenJadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PendaftaranJadwalKegiatan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "tanggal" TEXT NOT NULL,
+ "namaOrangtua" TEXT NOT NULL,
+ "nomor" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "catatan" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PendaftaranJadwalKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GrafikKepuasan" (
+ "id" SERIAL NOT NULL,
+ "label" TEXT NOT NULL,
+ "jumlah" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikKepuasan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ArtikelKesehatan" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ArtikelKesehatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Introduction" (
+ "id" SERIAL NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Introduction_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Symptom" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Symptom_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Prevention" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Prevention_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FirstAid" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FirstAid_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MythVsFact" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "mitos" TEXT NOT NULL,
+ "fakta" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MythVsFact_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DoctorSign" (
+ "id" SERIAL NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DoctorSign_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_FasilitasKesehatanToInformasiUmum" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_FasilitasKesehatanToInformasiUmum_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_FasilitasKesehatanToTarifDanLayanan" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "FileStorage_name_key" ON "FileStorage"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "JenisInformasiDiminta_name_key" ON "JenisInformasiDiminta"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CaraMemperolehInformasi_name_key" ON "CaraMemperolehInformasi"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "CaraMemperolehSalinanInformasi_name_key" ON "CaraMemperolehSalinanInformasi"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KategoriBerita_name_key" ON "KategoriBerita"("name");
+
+-- CreateIndex
+CREATE INDEX "_FasilitasKesehatanToInformasiUmum_B_index" ON "_FasilitasKesehatanToInformasiUmum"("B");
+
+-- CreateIndex
+CREATE INDEX "_FasilitasKesehatanToTarifDanLayanan_B_index" ON "_FasilitasKesehatanToTarifDanLayanan"("B");
+
+-- AddForeignKey
+ALTER TABLE "PermohonanInformasiPublik" ADD CONSTRAINT "PermohonanInformasiPublik_jenisInformasiDimintaId_fkey" FOREIGN KEY ("jenisInformasiDimintaId") REFERENCES "JenisInformasiDiminta"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PermohonanInformasiPublik" ADD CONSTRAINT "PermohonanInformasiPublik_caraMemperolehInformasiId_fkey" FOREIGN KEY ("caraMemperolehInformasiId") REFERENCES "CaraMemperolehInformasi"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PermohonanInformasiPublik" ADD CONSTRAINT "PermohonanInformasiPublik_caraMemperolehSalinanInformasiId_fkey" FOREIGN KEY ("caraMemperolehSalinanInformasiId") REFERENCES "CaraMemperolehSalinanInformasi"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProfileDesa" ADD CONSTRAINT "ProfileDesa_profilPerbekelId_fkey" FOREIGN KEY ("profilPerbekelId") REFERENCES "ProfilPerbekel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Berita" ADD CONSTRAINT "Berita_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Berita" ADD CONSTRAINT "Berita_kategoriBeritaId_fkey" FOREIGN KEY ("kategoriBeritaId") REFERENCES "KategoriBerita"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToInformasiUmum" ADD CONSTRAINT "_FasilitasKesehatanToInformasiUmum_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToInformasiUmum" ADD CONSTRAINT "_FasilitasKesehatanToInformasiUmum_B_fkey" FOREIGN KEY ("B") REFERENCES "InformasiUmum"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToTarifDanLayanan" ADD CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_FasilitasKesehatanToTarifDanLayanan" ADD CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_B_fkey" FOREIGN KEY ("B") REFERENCES "TarifDanLayanan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250616155255_16_jun/migration.sql b/prisma/migrations/20250616155255_16_jun/migration.sql
new file mode 100644
index 00000000..fff7b159
--- /dev/null
+++ b/prisma/migrations/20250616155255_16_jun/migration.sql
@@ -0,0 +1,193 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `nomor` on the `DaftarInformasiPublik` table. All the data in the column will be lost.
+ - You are about to drop the column `image` on the `GalleryFoto` table. All the data in the column will be lost.
+ - You are about to drop the column `video` on the `GalleryVideo` table. All the data in the column will be lost.
+ - You are about to drop the column `videosId` on the `GalleryVideo` table. All the data in the column will be lost.
+ - The primary key for the `IndeksKepuasanMasyarakat` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - You are about to drop the column `profilPerbekelId` on the `ProfileDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `imageUrl` on the `ProfilePPID` table. All the data in the column will be lost.
+ - You are about to drop the `Images` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `Videos` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `deskripsi` to the `GalleryFoto` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `deskripsi` to the `GalleryVideo` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `linkVideo` to the `GalleryVideo` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "GalleryFoto" DROP CONSTRAINT "GalleryFoto_imagesId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "GalleryVideo" DROP CONSTRAINT "GalleryVideo_videosId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "ProfileDesa" DROP CONSTRAINT "ProfileDesa_profilPerbekelId_fkey";
+
+-- DropIndex
+DROP INDEX "GalleryVideo_videosId_key";
+
+-- AlterTable
+ALTER TABLE "DaftarInformasiPublik" DROP COLUMN "nomor";
+
+-- AlterTable
+ALTER TABLE "GalleryFoto" DROP COLUMN "image",
+ADD COLUMN "deskripsi" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "GalleryVideo" DROP COLUMN "video",
+DROP COLUMN "videosId",
+ADD COLUMN "deskripsi" TEXT NOT NULL,
+ADD COLUMN "linkVideo" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "IndeksKepuasanMasyarakat" DROP CONSTRAINT "IndeksKepuasanMasyarakat_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "IndeksKepuasanMasyarakat_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "IndeksKepuasanMasyarakat_id_seq";
+
+-- AlterTable
+ALTER TABLE "ProfileDesa" DROP COLUMN "profilPerbekelId";
+
+-- AlterTable
+ALTER TABLE "ProfilePPID" DROP COLUMN "imageUrl",
+ADD COLUMN "imageId" TEXT;
+
+-- DropTable
+DROP TABLE "Images";
+
+-- DropTable
+DROP TABLE "Videos";
+
+-- CreateTable
+CREATE TABLE "StrukturPPID" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "StrukturPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProfileDesaImage" (
+ "id" TEXT NOT NULL,
+ "label" TEXT NOT NULL,
+ "profileDesaId" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+
+ CONSTRAINT "ProfileDesaImage_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PelayananSuratKeterangan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PelayananSuratKeterangan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PelayananTelunjukSaktiDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "link" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PelayananTelunjukSaktiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PelayananPerizinanBerusaha" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "link" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PelayananPerizinanBerusaha_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PelayananPendudukNonPermanen" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PelayananPendudukNonPermanen_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Penghargaan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "juara" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Penghargaan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Posyandu" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "nomor" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Posyandu_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProfilePPID" ADD CONSTRAINT "ProfilePPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_profileDesaId_fkey" FOREIGN KEY ("profileDesaId") REFERENCES "ProfileDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "GalleryFoto" ADD CONSTRAINT "GalleryFoto_imagesId_fkey" FOREIGN KEY ("imagesId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Posyandu" ADD CONSTRAINT "Posyandu_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250617083234_17jun/migration.sql b/prisma/migrations/20250617083234_17jun/migration.sql
new file mode 100644
index 00000000..2ff042ca
--- /dev/null
+++ b/prisma/migrations/20250617083234_17jun/migration.sql
@@ -0,0 +1,78 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `profileDesaId` on the `ProfileDesaImage` table. All the data in the column will be lost.
+ - You are about to drop the `ProfileDesa` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `maskotDesaId` to the `ProfileDesaImage` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "ProfileDesaImage" DROP CONSTRAINT "ProfileDesaImage_profileDesaId_fkey";
+
+-- AlterTable
+ALTER TABLE "ProfilPerbekel" ADD COLUMN "imageId" TEXT;
+
+-- AlterTable
+ALTER TABLE "ProfileDesaImage" DROP COLUMN "profileDesaId",
+ADD COLUMN "maskotDesaId" TEXT NOT NULL;
+
+-- DropTable
+DROP TABLE "ProfileDesa";
+
+-- CreateTable
+CREATE TABLE "SejarahDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "SejarahDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "VisiMisiDesa" (
+ "id" TEXT NOT NULL,
+ "visi" TEXT NOT NULL,
+ "misi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "VisiMisiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LambangDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "LambangDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MaskotDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MaskotDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_maskotDesaId_fkey" FOREIGN KEY ("maskotDesaId") REFERENCES "MaskotDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProfilPerbekel" ADD CONSTRAINT "ProfilPerbekel_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250624034125_24_jun2025/migration.sql b/prisma/migrations/20250624034125_24_jun2025/migration.sql
new file mode 100644
index 00000000..f7dfc831
--- /dev/null
+++ b/prisma/migrations/20250624034125_24_jun2025/migration.sql
@@ -0,0 +1,139 @@
+/*
+ Warnings:
+
+ - The primary key for the `DataKematian_Kelahiran` table will be changed. If it partially fails, the table could be left without primary key constraint.
+
+*/
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" DROP CONSTRAINT "DataKematian_Kelahiran_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "DataKematian_Kelahiran_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "DataKematian_Kelahiran_id_seq";
+
+-- AlterTable
+ALTER TABLE "ProfilePPID" ADD COLUMN "imageUrl" TEXT;
+
+-- CreateTable
+CREATE TABLE "Puskesmas" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "jamId" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "kontakId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Puskesmas_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JamOperasional" (
+ "id" TEXT NOT NULL,
+ "workDays" TEXT NOT NULL,
+ "weekDays" TEXT NOT NULL,
+ "holiday" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JamOperasional_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KontakPuskesmas" (
+ "id" TEXT NOT NULL,
+ "kontakPuskesmas" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "facebook" TEXT NOT NULL,
+ "kontakUGD" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KontakPuskesmas_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramKesehatan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsiSingkat" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProgramKesehatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PenangananDarurat" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PenangananDarurat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KontakDarurat" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KontakDarurat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "InfoWabahPenyakit" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsiSingkat" TEXT NOT NULL,
+ "deskripsiLengkap" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "InfoWabahPenyakit_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "Puskesmas" ADD CONSTRAINT "Puskesmas_jamId_fkey" FOREIGN KEY ("jamId") REFERENCES "JamOperasional"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Puskesmas" ADD CONSTRAINT "Puskesmas_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Puskesmas" ADD CONSTRAINT "Puskesmas_kontakId_fkey" FOREIGN KEY ("kontakId") REFERENCES "KontakPuskesmas"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProgramKesehatan" ADD CONSTRAINT "ProgramKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PenangananDarurat" ADD CONSTRAINT "PenangananDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDarurat" ADD CONSTRAINT "KontakDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InfoWabahPenyakit" ADD CONSTRAINT "InfoWabahPenyakit_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250624061829_24jun2025_2/migration.sql b/prisma/migrations/20250624061829_24jun2025_2/migration.sql
new file mode 100644
index 00000000..9fa565af
--- /dev/null
+++ b/prisma/migrations/20250624061829_24jun2025_2/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - The primary key for the `GrafikKepuasan` table will be changed. If it partially fails, the table could be left without primary key constraint.
+
+*/
+-- AlterTable
+ALTER TABLE "GrafikKepuasan" DROP CONSTRAINT "GrafikKepuasan_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "GrafikKepuasan_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "GrafikKepuasan_id_seq";
diff --git a/prisma/migrations/20250625035558_25jun2025_1/migration.sql b/prisma/migrations/20250625035558_25jun2025_1/migration.sql
new file mode 100644
index 00000000..a97587d1
--- /dev/null
+++ b/prisma/migrations/20250625035558_25jun2025_1/migration.sql
@@ -0,0 +1,96 @@
+/*
+ Warnings:
+
+ - You are about to drop the `_DokterdanTenagaMedisToFasilitasKesehatan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_FasilitasKesehatanToFasilitasPendukung` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_FasilitasKesehatanToInformasiUmum` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_FasilitasKesehatanToLayananUnggulan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_FasilitasKesehatanToProsedurPendaftaran` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_FasilitasKesehatanToTarifDanLayanan` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `dokterdanTenagaMedisId` to the `FasilitasKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `fasilitasPendukungId` to the `FasilitasKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `informasiUmumId` to the `FasilitasKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `layananUnggulanId` to the `FasilitasKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `prosedurPendaftaranId` to the `FasilitasKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `tarifDanLayananId` to the `FasilitasKesehatan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "_DokterdanTenagaMedisToFasilitasKesehatan" DROP CONSTRAINT "_DokterdanTenagaMedisToFasilitasKesehatan_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_DokterdanTenagaMedisToFasilitasKesehatan" DROP CONSTRAINT "_DokterdanTenagaMedisToFasilitasKesehatan_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToFasilitasPendukung" DROP CONSTRAINT "_FasilitasKesehatanToFasilitasPendukung_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToFasilitasPendukung" DROP CONSTRAINT "_FasilitasKesehatanToFasilitasPendukung_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToInformasiUmum" DROP CONSTRAINT "_FasilitasKesehatanToInformasiUmum_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToInformasiUmum" DROP CONSTRAINT "_FasilitasKesehatanToInformasiUmum_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToLayananUnggulan" DROP CONSTRAINT "_FasilitasKesehatanToLayananUnggulan_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToLayananUnggulan" DROP CONSTRAINT "_FasilitasKesehatanToLayananUnggulan_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToProsedurPendaftaran" DROP CONSTRAINT "_FasilitasKesehatanToProsedurPendaftaran_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToProsedurPendaftaran" DROP CONSTRAINT "_FasilitasKesehatanToProsedurPendaftaran_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToTarifDanLayanan" DROP CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_FasilitasKesehatanToTarifDanLayanan" DROP CONSTRAINT "_FasilitasKesehatanToTarifDanLayanan_B_fkey";
+
+-- AlterTable
+ALTER TABLE "FasilitasKesehatan" ADD COLUMN "dokterdanTenagaMedisId" TEXT NOT NULL,
+ADD COLUMN "fasilitasPendukungId" TEXT NOT NULL,
+ADD COLUMN "informasiUmumId" TEXT NOT NULL,
+ADD COLUMN "layananUnggulanId" TEXT NOT NULL,
+ADD COLUMN "prosedurPendaftaranId" TEXT NOT NULL,
+ADD COLUMN "tarifDanLayananId" TEXT NOT NULL;
+
+-- DropTable
+DROP TABLE "_DokterdanTenagaMedisToFasilitasKesehatan";
+
+-- DropTable
+DROP TABLE "_FasilitasKesehatanToFasilitasPendukung";
+
+-- DropTable
+DROP TABLE "_FasilitasKesehatanToInformasiUmum";
+
+-- DropTable
+DROP TABLE "_FasilitasKesehatanToLayananUnggulan";
+
+-- DropTable
+DROP TABLE "_FasilitasKesehatanToProsedurPendaftaran";
+
+-- DropTable
+DROP TABLE "_FasilitasKesehatanToTarifDanLayanan";
+
+-- AddForeignKey
+ALTER TABLE "FasilitasKesehatan" ADD CONSTRAINT "FasilitasKesehatan_informasiUmumId_fkey" FOREIGN KEY ("informasiUmumId") REFERENCES "InformasiUmum"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FasilitasKesehatan" ADD CONSTRAINT "FasilitasKesehatan_layananUnggulanId_fkey" FOREIGN KEY ("layananUnggulanId") REFERENCES "LayananUnggulan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FasilitasKesehatan" ADD CONSTRAINT "FasilitasKesehatan_dokterdanTenagaMedisId_fkey" FOREIGN KEY ("dokterdanTenagaMedisId") REFERENCES "DokterdanTenagaMedis"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FasilitasKesehatan" ADD CONSTRAINT "FasilitasKesehatan_fasilitasPendukungId_fkey" FOREIGN KEY ("fasilitasPendukungId") REFERENCES "FasilitasPendukung"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FasilitasKesehatan" ADD CONSTRAINT "FasilitasKesehatan_prosedurPendaftaranId_fkey" FOREIGN KEY ("prosedurPendaftaranId") REFERENCES "ProsedurPendaftaran"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FasilitasKesehatan" ADD CONSTRAINT "FasilitasKesehatan_tarifDanLayananId_fkey" FOREIGN KEY ("tarifDanLayananId") REFERENCES "TarifDanLayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250625085706_25_jun_25_2/migration.sql b/prisma/migrations/20250625085706_25_jun_25_2/migration.sql
new file mode 100644
index 00000000..28a29c1e
--- /dev/null
+++ b/prisma/migrations/20250625085706_25_jun_25_2/migration.sql
@@ -0,0 +1,32 @@
+/*
+ Warnings:
+
+ - The primary key for the `DataKematian_Kelahiran` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The `id` column on the `DataKematian_Kelahiran` table would be dropped and recreated. This will lead to data loss if there is data in the column.
+ - The primary key for the `GrafikKepuasan` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The `id` column on the `GrafikKepuasan` table would be dropped and recreated. This will lead to data loss if there is data in the column.
+ - A unique constraint covering the columns `[uuid]` on the table `DataKematian_Kelahiran` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[uuid]` on the table `GrafikKepuasan` will be added. If there are existing duplicate values, this will fail.
+ - The required column `uuid` was added to the `DataKematian_Kelahiran` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
+ - The required column `uuid` was added to the `GrafikKepuasan` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
+
+*/
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" DROP CONSTRAINT "DataKematian_Kelahiran_pkey",
+ADD COLUMN "uuid" TEXT NOT NULL,
+DROP COLUMN "id",
+ADD COLUMN "id" SERIAL NOT NULL,
+ADD CONSTRAINT "DataKematian_Kelahiran_pkey" PRIMARY KEY ("id");
+
+-- AlterTable
+ALTER TABLE "GrafikKepuasan" DROP CONSTRAINT "GrafikKepuasan_pkey",
+ADD COLUMN "uuid" TEXT NOT NULL,
+DROP COLUMN "id",
+ADD COLUMN "id" SERIAL NOT NULL,
+ADD CONSTRAINT "GrafikKepuasan_pkey" PRIMARY KEY ("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DataKematian_Kelahiran_uuid_key" ON "DataKematian_Kelahiran"("uuid");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GrafikKepuasan_uuid_key" ON "GrafikKepuasan"("uuid");
diff --git a/prisma/migrations/20250626061909_26_jun_25_01/migration.sql b/prisma/migrations/20250626061909_26_jun_25_01/migration.sql
new file mode 100644
index 00000000..6e7910ce
--- /dev/null
+++ b/prisma/migrations/20250626061909_26_jun_25_01/migration.sql
@@ -0,0 +1,92 @@
+/*
+ Warnings:
+
+ - The primary key for the `ArtikelKesehatan` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The primary key for the `DoctorSign` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The primary key for the `FirstAid` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The primary key for the `Introduction` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The primary key for the `MythVsFact` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The primary key for the `Prevention` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The primary key for the `Symptom` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - Added the required column `deskripsiJadwalKegiatanId` to the `JadwalKegiatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `dokumenJadwalKegiatanId` to the `JadwalKegiatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `informasiJadwalKegiatanId` to the `JadwalKegiatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `layananJadwalKegiatanId` to the `JadwalKegiatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `pendaftaranJadwalKegiatanId` to the `JadwalKegiatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `syaratKetentuanJadwalKegiatanId` to the `JadwalKegiatan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "ArtikelKesehatan" DROP CONSTRAINT "ArtikelKesehatan_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "ArtikelKesehatan_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "ArtikelKesehatan_id_seq";
+
+-- AlterTable
+ALTER TABLE "DoctorSign" DROP CONSTRAINT "DoctorSign_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "DoctorSign_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "DoctorSign_id_seq";
+
+-- AlterTable
+ALTER TABLE "FirstAid" DROP CONSTRAINT "FirstAid_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "FirstAid_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "FirstAid_id_seq";
+
+-- AlterTable
+ALTER TABLE "Introduction" DROP CONSTRAINT "Introduction_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "Introduction_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "Introduction_id_seq";
+
+-- AlterTable
+ALTER TABLE "JadwalKegiatan" ADD COLUMN "deskripsiJadwalKegiatanId" TEXT NOT NULL,
+ADD COLUMN "dokumenJadwalKegiatanId" TEXT NOT NULL,
+ADD COLUMN "informasiJadwalKegiatanId" TEXT NOT NULL,
+ADD COLUMN "layananJadwalKegiatanId" TEXT NOT NULL,
+ADD COLUMN "pendaftaranJadwalKegiatanId" TEXT NOT NULL,
+ADD COLUMN "syaratKetentuanJadwalKegiatanId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "MythVsFact" DROP CONSTRAINT "MythVsFact_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "MythVsFact_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "MythVsFact_id_seq";
+
+-- AlterTable
+ALTER TABLE "Prevention" DROP CONSTRAINT "Prevention_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "Prevention_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "Prevention_id_seq";
+
+-- AlterTable
+ALTER TABLE "Symptom" DROP CONSTRAINT "Symptom_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "Symptom_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "Symptom_id_seq";
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_informasiJadwalKegiatanId_fkey" FOREIGN KEY ("informasiJadwalKegiatanId") REFERENCES "InformasiJadwalKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_deskripsiJadwalKegiatanId_fkey" FOREIGN KEY ("deskripsiJadwalKegiatanId") REFERENCES "DeskripsiJadwalKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_layananJadwalKegiatanId_fkey" FOREIGN KEY ("layananJadwalKegiatanId") REFERENCES "LayananJadwalKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_syaratKetentuanJadwalKegiatanId_fkey" FOREIGN KEY ("syaratKetentuanJadwalKegiatanId") REFERENCES "SyaratKetentuanJadwalKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_dokumenJadwalKegiatanId_fkey" FOREIGN KEY ("dokumenJadwalKegiatanId") REFERENCES "DokumenJadwalKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey" FOREIGN KEY ("pendaftaranJadwalKegiatanId") REFERENCES "PendaftaranJadwalKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250627155416_nico_27_jun_25_o1/migration.sql b/prisma/migrations/20250627155416_nico_27_jun_25_o1/migration.sql
new file mode 100644
index 00000000..c198dc4f
--- /dev/null
+++ b/prisma/migrations/20250627155416_nico_27_jun_25_o1/migration.sql
@@ -0,0 +1,68 @@
+/*
+ Warnings:
+
+ - The primary key for the `DataKematian_Kelahiran` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - You are about to drop the column `uuid` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - The primary key for the `GrafikKepuasan` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - You are about to drop the column `uuid` on the `GrafikKepuasan` table. All the data in the column will be lost.
+ - A unique constraint covering the columns `[id]` on the table `DataKematian_Kelahiran` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[id]` on the table `GrafikKepuasan` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `doctorSignId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `firstAidId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `introductionId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `mythVsFactId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `preventionId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `symptomId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropIndex
+DROP INDEX "DataKematian_Kelahiran_uuid_key";
+
+-- DropIndex
+DROP INDEX "GrafikKepuasan_uuid_key";
+
+-- AlterTable
+ALTER TABLE "ArtikelKesehatan" ADD COLUMN "doctorSignId" TEXT NOT NULL,
+ADD COLUMN "firstAidId" TEXT NOT NULL,
+ADD COLUMN "introductionId" TEXT NOT NULL,
+ADD COLUMN "mythVsFactId" TEXT NOT NULL,
+ADD COLUMN "preventionId" TEXT NOT NULL,
+ADD COLUMN "symptomId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" DROP CONSTRAINT "DataKematian_Kelahiran_pkey",
+DROP COLUMN "uuid",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT;
+DROP SEQUENCE "DataKematian_Kelahiran_id_seq";
+
+-- AlterTable
+ALTER TABLE "GrafikKepuasan" DROP CONSTRAINT "GrafikKepuasan_pkey",
+DROP COLUMN "uuid",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT;
+DROP SEQUENCE "GrafikKepuasan_id_seq";
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DataKematian_Kelahiran_id_key" ON "DataKematian_Kelahiran"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GrafikKepuasan_id_key" ON "GrafikKepuasan"("id");
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_introductionId_fkey" FOREIGN KEY ("introductionId") REFERENCES "Introduction"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_symptomId_fkey" FOREIGN KEY ("symptomId") REFERENCES "Symptom"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_preventionId_fkey" FOREIGN KEY ("preventionId") REFERENCES "Prevention"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_firstAidId_fkey" FOREIGN KEY ("firstAidId") REFERENCES "FirstAid"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_mythVsFactId_fkey" FOREIGN KEY ("mythVsFactId") REFERENCES "MythVsFact"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_doctorSignId_fkey" FOREIGN KEY ("doctorSignId") REFERENCES "DoctorSign"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250630030216_nico_30_jun_25_1/migration.sql b/prisma/migrations/20250630030216_nico_30_jun_25_1/migration.sql
new file mode 100644
index 00000000..08b04044
--- /dev/null
+++ b/prisma/migrations/20250630030216_nico_30_jun_25_1/migration.sql
@@ -0,0 +1,201 @@
+-- CreateEnum
+CREATE TYPE "StatusLaporan" AS ENUM ('SELESAI', 'PROSES', 'GAGAL');
+
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_pkey" PRIMARY KEY ("id");
+
+-- DropIndex
+DROP INDEX "DataKematian_Kelahiran_id_key";
+
+-- AlterTable
+ALTER TABLE "GrafikKepuasan" ADD CONSTRAINT "GrafikKepuasan_pkey" PRIMARY KEY ("id");
+
+-- DropIndex
+DROP INDEX "GrafikKepuasan_id_key";
+
+-- CreateTable
+CREATE TABLE "KeamananLingkungan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KeamananLingkungan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PolsekTerdekat" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "jarakKeDesa" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "nomorTelepon" TEXT NOT NULL,
+ "jamOperasional" TEXT NOT NULL,
+ "embedMapUrl" TEXT NOT NULL,
+ "namaTempatMaps" TEXT NOT NULL,
+ "alamatMaps" TEXT NOT NULL,
+ "linkPetunjukArah" TEXT NOT NULL,
+ "layananPolsekId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PolsekTerdekat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LayananPolsek" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "LayananPolsek_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KontakDaruratKeamanan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "kontak" TEXT NOT NULL,
+ "icon" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KontakDaruratKeamanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PencegahanKriminalitas" (
+ "id" TEXT NOT NULL,
+ "programKeamananId" TEXT NOT NULL,
+ "tipsKeamananId" TEXT NOT NULL,
+ "videoKeamananId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PencegahanKriminalitas_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramKeamanan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "deskripsi" TEXT,
+ "slug" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "ProgramKeamanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TipsKeamanan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "konten" TEXT NOT NULL,
+ "slug" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "TipsKeamanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "VideoKeamanan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT,
+ "videoUrl" TEXT NOT NULL,
+ "slug" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "VideoKeamanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LaporanPublik" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "lokasi" TEXT NOT NULL,
+ "tanggalWaktu" TIMESTAMP(3) NOT NULL,
+ "status" "StatusLaporan" NOT NULL,
+ "kronologi" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "LaporanPublik_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PenangananLaporanPublik" (
+ "id" TEXT NOT NULL,
+ "laporanId" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+
+ CONSTRAINT "PenangananLaporanPublik_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Pelapor" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "nomorTelepon" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+
+ CONSTRAINT "Pelapor_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MenuTipsKeamanan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MenuTipsKeamanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ProgramKeamanan_slug_key" ON "ProgramKeamanan"("slug");
+
+-- AddForeignKey
+ALTER TABLE "KeamananLingkungan" ADD CONSTRAINT "KeamananLingkungan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PolsekTerdekat" ADD CONSTRAINT "PolsekTerdekat_layananPolsekId_fkey" FOREIGN KEY ("layananPolsekId") REFERENCES "LayananPolsek"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PencegahanKriminalitas" ADD CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey" FOREIGN KEY ("programKeamananId") REFERENCES "ProgramKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PencegahanKriminalitas" ADD CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey" FOREIGN KEY ("tipsKeamananId") REFERENCES "TipsKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PencegahanKriminalitas" ADD CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey" FOREIGN KEY ("videoKeamananId") REFERENCES "VideoKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PenangananLaporanPublik" ADD CONSTRAINT "PenangananLaporanPublik_laporanId_fkey" FOREIGN KEY ("laporanId") REFERENCES "LaporanPublik"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Pelapor" ADD CONSTRAINT "Pelapor_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "MenuTipsKeamanan" ADD CONSTRAINT "MenuTipsKeamanan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250630081247_fix_layanan_polsek_deletedat/migration.sql b/prisma/migrations/20250630081247_fix_layanan_polsek_deletedat/migration.sql
new file mode 100644
index 00000000..3767383e
--- /dev/null
+++ b/prisma/migrations/20250630081247_fix_layanan_polsek_deletedat/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "LayananPolsek" ALTER COLUMN "deletedAt" DROP NOT NULL,
+ALTER COLUMN "deletedAt" DROP DEFAULT;
diff --git a/prisma/migrations/20250701093155_nico_1_jul_25_test1/migration.sql b/prisma/migrations/20250701093155_nico_1_jul_25_test1/migration.sql
new file mode 100644
index 00000000..6d0692f7
--- /dev/null
+++ b/prisma/migrations/20250701093155_nico_1_jul_25_test1/migration.sql
@@ -0,0 +1,75 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `deletedAt` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `deskripsi` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `isActive` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `name` on the `KontakDarurat` table. All the data in the column will be lost.
+ - Added the required column `nama` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "KontakDarurat" DROP CONSTRAINT "KontakDarurat_imageId_fkey";
+
+-- AlterTable
+ALTER TABLE "KontakDarurat" DROP COLUMN "deletedAt",
+DROP COLUMN "deskripsi",
+DROP COLUMN "imageId",
+DROP COLUMN "isActive",
+DROP COLUMN "name",
+ADD COLUMN "icon" TEXT,
+ADD COLUMN "nama" TEXT NOT NULL,
+ADD COLUMN "urutan" INTEGER;
+
+-- CreateTable
+CREATE TABLE "KontakItem" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "nomorTelepon" TEXT NOT NULL,
+ "icon" TEXT,
+ "kategoriId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "KontakItem_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PasarDesa" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "harga" INTEGER NOT NULL,
+ "satuan" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "rating" DOUBLE PRECISION NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "kategoriId" TEXT NOT NULL,
+
+ CONSTRAINT "PasarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriMakanan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriMakanan_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakDarurat"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriMakanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250701093435_nico_1_jul_25_test2/migration.sql b/prisma/migrations/20250701093435_nico_1_jul_25_test2/migration.sql
new file mode 100644
index 00000000..d8fe38ee
--- /dev/null
+++ b/prisma/migrations/20250701093435_nico_1_jul_25_test2/migration.sql
@@ -0,0 +1,38 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `icon` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `nama` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `urutan` on the `KontakDarurat` table. All the data in the column will be lost.
+ - You are about to drop the column `deletedAt` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - You are about to drop the column `isActive` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - You are about to drop the column `kontak` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - Added the required column `deskripsi` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `imageId` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `name` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey";
+
+-- AlterTable
+ALTER TABLE "KontakDarurat" DROP COLUMN "icon",
+DROP COLUMN "nama",
+DROP COLUMN "urutan",
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "deskripsi" TEXT NOT NULL,
+ADD COLUMN "imageId" TEXT NOT NULL,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "name" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "deletedAt",
+DROP COLUMN "isActive",
+DROP COLUMN "kontak",
+ADD COLUMN "urutan" INTEGER;
+
+-- AddForeignKey
+ALTER TABLE "KontakDarurat" ADD CONSTRAINT "KontakDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250703070556_3_jul_2025_1/migration.sql b/prisma/migrations/20250703070556_3_jul_2025_1/migration.sql
new file mode 100644
index 00000000..ab8d910a
--- /dev/null
+++ b/prisma/migrations/20250703070556_3_jul_2025_1/migration.sql
@@ -0,0 +1,85 @@
+/*
+ Warnings:
+
+ - The values [SELESAI,PROSES,GAGAL] on the enum `StatusLaporan` will be removed. If these variants are still used in the database, this will fail.
+ - You are about to drop the column `icon` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - You are about to drop the column `urutan` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - You are about to drop the column `icon` on the `KontakItem` table. All the data in the column will be lost.
+
+*/
+-- AlterEnum
+BEGIN;
+CREATE TYPE "StatusLaporan_new" AS ENUM ('Selesai', 'Proses', 'Gagal');
+ALTER TABLE "LaporanPublik" ALTER COLUMN "status" TYPE "StatusLaporan_new" USING ("status"::text::"StatusLaporan_new");
+ALTER TYPE "StatusLaporan" RENAME TO "StatusLaporan_old";
+ALTER TYPE "StatusLaporan_new" RENAME TO "StatusLaporan";
+DROP TYPE "StatusLaporan_old";
+COMMIT;
+
+-- AlterTable
+ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "icon",
+DROP COLUMN "urutan",
+ADD COLUMN "imageId" TEXT;
+
+-- AlterTable
+ALTER TABLE "KontakItem" DROP COLUMN "icon",
+ADD COLUMN "imageId" TEXT;
+
+-- CreateTable
+CREATE TABLE "LowonganPekerjaan" (
+ "id" TEXT NOT NULL,
+ "posisi" TEXT NOT NULL,
+ "namaPerusahaan" TEXT NOT NULL,
+ "lokasi" TEXT NOT NULL,
+ "tipePekerjaan" TEXT NOT NULL,
+ "gaji" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kualifikasi" TEXT NOT NULL,
+ "tanggalPosting" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "LowonganPekerjaan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramKemiskinan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "ikonUrl" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "statistikId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "ProgramKemiskinan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StatistikKemiskinan" (
+ "id" TEXT NOT NULL,
+ "tahun" INTEGER NOT NULL,
+ "jumlah" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "StatistikKemiskinan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ProgramKemiskinan_statistikId_key" ON "ProgramKemiskinan"("statistikId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "StatistikKemiskinan_tahun_key" ON "StatistikKemiskinan"("tahun");
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProgramKemiskinan" ADD CONSTRAINT "ProgramKemiskinan_statistikId_fkey" FOREIGN KEY ("statistikId") REFERENCES "StatistikKemiskinan"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250703082409_nico_3_jul_25_2/migration.sql b/prisma/migrations/20250703082409_nico_3_jul_25_2/migration.sql
new file mode 100644
index 00000000..1a827bb1
--- /dev/null
+++ b/prisma/migrations/20250703082409_nico_3_jul_25_2/migration.sql
@@ -0,0 +1,59 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `alamat` on the `PasarDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `deletedAt` on the `PasarDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `isActive` on the `PasarDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `kategoriId` on the `PasarDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `satuan` on the `PasarDesa` table. All the data in the column will be lost.
+ - You are about to drop the `KategoriMakanan` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `alamatUsaha` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriId_fkey";
+
+-- AlterTable
+ALTER TABLE "PasarDesa" DROP COLUMN "alamat",
+DROP COLUMN "deletedAt",
+DROP COLUMN "isActive",
+DROP COLUMN "kategoriId",
+DROP COLUMN "satuan",
+ADD COLUMN "alamatUsaha" TEXT NOT NULL,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- DropTable
+DROP TABLE "KategoriMakanan";
+
+-- CreateTable
+CREATE TABLE "KategoriProduk" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "KategoriProduk_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_ProdukToKategori" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_ProdukToKategori_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE INDEX "_ProdukToKategori_B_index" ON "_ProdukToKategori"("B");
+
+-- AddForeignKey
+ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_A_fkey" FOREIGN KEY ("A") REFERENCES "KategoriProduk"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_B_fkey" FOREIGN KEY ("B") REFERENCES "PasarDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250704023249_pivot_kategori_to_pasar/migration.sql b/prisma/migrations/20250704023249_pivot_kategori_to_pasar/migration.sql
new file mode 100644
index 00000000..0a2d4dc4
--- /dev/null
+++ b/prisma/migrations/20250704023249_pivot_kategori_to_pasar/migration.sql
@@ -0,0 +1,41 @@
+/*
+ Warnings:
+
+ - You are about to drop the `_ProdukToKategori` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "_ProdukToKategori" DROP CONSTRAINT "_ProdukToKategori_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_ProdukToKategori" DROP CONSTRAINT "_ProdukToKategori_B_fkey";
+
+-- AlterTable
+ALTER TABLE "KategoriProduk" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
+
+-- AlterTable
+ALTER TABLE "PasarDesa" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
+
+-- DropTable
+DROP TABLE "_ProdukToKategori";
+
+-- CreateTable
+CREATE TABLE "KategoriToPasar" (
+ "id" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "pasarDesaId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriToPasar_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "KategoriToPasar" ADD CONSTRAINT "KategoriToPasar_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriProduk"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KategoriToPasar" ADD CONSTRAINT "KategoriToPasar_pasarDesaId_fkey" FOREIGN KEY ("pasarDesaId") REFERENCES "PasarDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250704091225_4_jul_2025/migration.sql b/prisma/migrations/20250704091225_4_jul_2025/migration.sql
new file mode 100644
index 00000000..b8f75b46
--- /dev/null
+++ b/prisma/migrations/20250704091225_4_jul_2025/migration.sql
@@ -0,0 +1,11 @@
+/*
+ Warnings:
+
+ - Added the required column `kategoriProdukId` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "PasarDesa" ADD COLUMN "kategoriProdukId" TEXT NOT NULL;
+
+-- AddForeignKey
+ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProduk"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250707074426_add_is_active_to_pegawai/migration.sql b/prisma/migrations/20250707074426_add_is_active_to_pegawai/migration.sql
new file mode 100644
index 00000000..aa41c6e8
--- /dev/null
+++ b/prisma/migrations/20250707074426_add_is_active_to_pegawai/migration.sql
@@ -0,0 +1,78 @@
+-- CreateTable
+CREATE TABLE "posisi_organisasi" (
+ "id" VARCHAR(50) NOT NULL,
+ "nama" VARCHAR(100) NOT NULL,
+ "deskripsi" TEXT,
+ "hierarki" INTEGER NOT NULL,
+
+ CONSTRAINT "posisi_organisasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "pegawai" (
+ "id" UUID NOT NULL,
+ "namaLengkap" VARCHAR(255) NOT NULL,
+ "gelarAkademik" VARCHAR(100),
+ "imageId" TEXT,
+ "tanggalMasuk" DATE,
+ "email" VARCHAR(255),
+ "telepon" VARCHAR(20),
+ "alamat" TEXT,
+ "posisiId" VARCHAR(50) NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "pegawai_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "hubungan_organisasi" (
+ "id" UUID NOT NULL,
+ "atasanId" UUID NOT NULL,
+ "bawahanId" UUID NOT NULL,
+ "tipe" VARCHAR(50),
+
+ CONSTRAINT "hubungan_organisasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "struktur_organisasi" (
+ "id" TEXT NOT NULL,
+ "posisiOrganisasiId" VARCHAR(50) NOT NULL,
+ "pegawaiId" UUID NOT NULL,
+ "hubunganOrganisasiId" UUID NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "struktur_organisasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "pegawai_email_key" ON "pegawai"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "hubungan_organisasi_atasanId_bawahanId_key" ON "hubungan_organisasi"("atasanId", "bawahanId");
+
+-- AddForeignKey
+ALTER TABLE "pegawai" ADD CONSTRAINT "pegawai_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "pegawai" ADD CONSTRAINT "pegawai_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "posisi_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "hubungan_organisasi" ADD CONSTRAINT "hubungan_organisasi_atasanId_fkey" FOREIGN KEY ("atasanId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "hubungan_organisasi" ADD CONSTRAINT "hubungan_organisasi_bawahanId_fkey" FOREIGN KEY ("bawahanId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "posisi_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey" FOREIGN KEY ("hubunganOrganisasiId") REFERENCES "hubungan_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250708152314_nico_8_jul_25_1/migration.sql b/prisma/migrations/20250708152314_nico_8_jul_25_1/migration.sql
new file mode 100644
index 00000000..68303e56
--- /dev/null
+++ b/prisma/migrations/20250708152314_nico_8_jul_25_1/migration.sql
@@ -0,0 +1,77 @@
+-- CreateTable
+CREATE TABLE "GrafikMenganggurBerdasarkanUsia" (
+ "id" TEXT NOT NULL,
+ "usia18_25" TEXT NOT NULL,
+ "usia26_35" TEXT NOT NULL,
+ "usia36_45" TEXT NOT NULL,
+ "usia46_keatas" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikMenganggurBerdasarkanUsia_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GrafikMenganggurBerdasarkanPendidikan" (
+ "id" TEXT NOT NULL,
+ "SD" TEXT NOT NULL,
+ "SMP" TEXT NOT NULL,
+ "SMA" TEXT NOT NULL,
+ "D3" TEXT NOT NULL,
+ "S1" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikMenganggurBerdasarkanPendidikan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GrafikJumlahPendudukMiskin" (
+ "id" UUID NOT NULL,
+ "year" INTEGER NOT NULL,
+ "totalPoorPopulation" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "GrafikJumlahPendudukMiskin_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SektorUnggulanDesa" (
+ "id" UUID NOT NULL,
+ "name" VARCHAR(100) NOT NULL,
+ "description" TEXT,
+ "value" DOUBLE PRECISION,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "SektorUnggulanDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataDemografiPekerjaan" (
+ "id" TEXT NOT NULL,
+ "pekerjaan" TEXT NOT NULL,
+ "lakiLaki" INTEGER NOT NULL,
+ "perempuan" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DataDemografiPekerjaan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GrafikJumlahPendudukMiskin_year_key" ON "GrafikJumlahPendudukMiskin"("year");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "SektorUnggulanDesa_name_key" ON "SektorUnggulanDesa"("name");
diff --git a/prisma/migrations/20250709153651_nico_9_jul_25_1/migration.sql b/prisma/migrations/20250709153651_nico_9_jul_25_1/migration.sql
new file mode 100644
index 00000000..0db03642
--- /dev/null
+++ b/prisma/migrations/20250709153651_nico_9_jul_25_1/migration.sql
@@ -0,0 +1,19 @@
+-- CreateTable
+CREATE TABLE "DetailDataPengangguran" (
+ "id" UUID NOT NULL,
+ "month" VARCHAR(20) NOT NULL,
+ "year" INTEGER NOT NULL,
+ "totalUnemployment" INTEGER NOT NULL,
+ "educatedUnemployment" INTEGER NOT NULL,
+ "uneducatedUnemployment" INTEGER NOT NULL,
+ "percentageChange" DOUBLE PRECISION,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DetailDataPengangguran_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DetailDataPengangguran_month_year_key" ON "DetailDataPengangguran"("month", "year");
diff --git a/prisma/migrations/20250710032516_nico_10_jul_25_1/migration.sql b/prisma/migrations/20250710032516_nico_10_jul_25_1/migration.sql
new file mode 100644
index 00000000..eb675e2b
--- /dev/null
+++ b/prisma/migrations/20250710032516_nico_10_jul_25_1/migration.sql
@@ -0,0 +1,133 @@
+-- CreateTable
+CREATE TABLE "ApbDesa" (
+ "id" TEXT NOT NULL,
+ "tahun" INTEGER NOT NULL,
+ "pendapatanId" TEXT NOT NULL,
+ "belanjaId" TEXT NOT NULL,
+ "pembiayaanId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "ApbDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Pendapatan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "value" INTEGER NOT NULL,
+
+ CONSTRAINT "Pendapatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Belanja" (
+ "id" TEXT NOT NULL,
+ "penyelenggaraan" INTEGER NOT NULL,
+ "pelaksanaanPembangunan" INTEGER NOT NULL,
+ "pembinaanMasyarakat" INTEGER NOT NULL,
+ "pemberdayaanMasyarakat" INTEGER NOT NULL,
+ "penanggulanganBencana" INTEGER NOT NULL,
+ "total" INTEGER NOT NULL,
+
+ CONSTRAINT "Belanja_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Pembiayaan" (
+ "id" TEXT NOT NULL,
+ "silpa" INTEGER NOT NULL,
+
+ CONSTRAINT "Pembiayaan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KlasifikasiBelanja" (
+ "id" TEXT NOT NULL,
+ "jenis" TEXT NOT NULL,
+ "persen" DOUBLE PRECISION NOT NULL,
+ "total" INTEGER NOT NULL,
+ "apbDesaId" TEXT NOT NULL,
+
+ CONSTRAINT "KlasifikasiBelanja_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "RincianBelanja" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "jumlah" INTEGER NOT NULL,
+ "klasifikasiBelanjaId" TEXT NOT NULL,
+
+ CONSTRAINT "RincianBelanja_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KegiatanSubak" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "jumlah" INTEGER NOT NULL,
+ "apbDesaId" TEXT NOT NULL,
+
+ CONSTRAINT "KegiatanSubak_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_ApbDesaToKegiatanSubak" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_ApbDesaToKegiatanSubak_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_BelanjaToKlasifikasiBelanja" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_BelanjaToKlasifikasiBelanja_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_KlasifikasiBelanjaToRincianBelanja" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE INDEX "_ApbDesaToKegiatanSubak_B_index" ON "_ApbDesaToKegiatanSubak"("B");
+
+-- CreateIndex
+CREATE INDEX "_BelanjaToKlasifikasiBelanja_B_index" ON "_BelanjaToKlasifikasiBelanja"("B");
+
+-- CreateIndex
+CREATE INDEX "_KlasifikasiBelanjaToRincianBelanja_B_index" ON "_KlasifikasiBelanjaToRincianBelanja"("B");
+
+-- AddForeignKey
+ALTER TABLE "ApbDesa" ADD CONSTRAINT "ApbDesa_pendapatanId_fkey" FOREIGN KEY ("pendapatanId") REFERENCES "Pendapatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ApbDesa" ADD CONSTRAINT "ApbDesa_belanjaId_fkey" FOREIGN KEY ("belanjaId") REFERENCES "Belanja"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ApbDesa" ADD CONSTRAINT "ApbDesa_pembiayaanId_fkey" FOREIGN KEY ("pembiayaanId") REFERENCES "Pembiayaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaToKegiatanSubak" ADD CONSTRAINT "_ApbDesaToKegiatanSubak_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaToKegiatanSubak" ADD CONSTRAINT "_ApbDesaToKegiatanSubak_B_fkey" FOREIGN KEY ("B") REFERENCES "KegiatanSubak"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_BelanjaToKlasifikasiBelanja" ADD CONSTRAINT "_BelanjaToKlasifikasiBelanja_A_fkey" FOREIGN KEY ("A") REFERENCES "Belanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_BelanjaToKlasifikasiBelanja" ADD CONSTRAINT "_BelanjaToKlasifikasiBelanja_B_fkey" FOREIGN KEY ("B") REFERENCES "KlasifikasiBelanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" ADD CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_A_fkey" FOREIGN KEY ("A") REFERENCES "KlasifikasiBelanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" ADD CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_B_fkey" FOREIGN KEY ("B") REFERENCES "RincianBelanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250710034841_nico_10_jul_25_2/migration.sql b/prisma/migrations/20250710034841_nico_10_jul_25_2/migration.sql
new file mode 100644
index 00000000..29f6151d
--- /dev/null
+++ b/prisma/migrations/20250710034841_nico_10_jul_25_2/migration.sql
@@ -0,0 +1,34 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `pelaksanaanPembangunan` on the `Belanja` table. All the data in the column will be lost.
+ - You are about to drop the column `pemberdayaanMasyarakat` on the `Belanja` table. All the data in the column will be lost.
+ - You are about to drop the column `pembinaanMasyarakat` on the `Belanja` table. All the data in the column will be lost.
+ - You are about to drop the column `penanggulanganBencana` on the `Belanja` table. All the data in the column will be lost.
+ - You are about to drop the column `penyelenggaraan` on the `Belanja` table. All the data in the column will be lost.
+ - Added the required column `name` to the `Belanja` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `updatedAt` to the `Belanja` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `value` to the `Belanja` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `total` to the `Pendapatan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `updatedAt` to the `Pendapatan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "Belanja" DROP COLUMN "pelaksanaanPembangunan",
+DROP COLUMN "pemberdayaanMasyarakat",
+DROP COLUMN "pembinaanMasyarakat",
+DROP COLUMN "penanggulanganBencana",
+DROP COLUMN "penyelenggaraan",
+ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "name" TEXT NOT NULL,
+ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
+ADD COLUMN "value" INTEGER NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Pendapatan" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "total" INTEGER NOT NULL,
+ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
diff --git a/prisma/migrations/20250710035010_nico_10_jul_25_3/migration.sql b/prisma/migrations/20250710035010_nico_10_jul_25_3/migration.sql
new file mode 100644
index 00000000..b06289f6
--- /dev/null
+++ b/prisma/migrations/20250710035010_nico_10_jul_25_3/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `total` on the `Belanja` table. All the data in the column will be lost.
+ - You are about to drop the column `total` on the `Pendapatan` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Belanja" DROP COLUMN "total";
+
+-- AlterTable
+ALTER TABLE "Pendapatan" DROP COLUMN "total";
diff --git a/prisma/migrations/20250710040514_nico_10_jul_25_4/migration.sql b/prisma/migrations/20250710040514_nico_10_jul_25_4/migration.sql
new file mode 100644
index 00000000..9338179a
--- /dev/null
+++ b/prisma/migrations/20250710040514_nico_10_jul_25_4/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `silpa` on the `Pembiayaan` table. All the data in the column will be lost.
+ - Added the required column `name` to the `Pembiayaan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `value` to the `Pembiayaan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "Pembiayaan" DROP COLUMN "silpa",
+ADD COLUMN "name" TEXT NOT NULL,
+ADD COLUMN "value" INTEGER NOT NULL;
diff --git a/prisma/migrations/20250715072239_nico_15_jul_25_1/migration.sql b/prisma/migrations/20250715072239_nico_15_jul_25_1/migration.sql
new file mode 100644
index 00000000..6fb73b27
--- /dev/null
+++ b/prisma/migrations/20250715072239_nico_15_jul_25_1/migration.sql
@@ -0,0 +1,245 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `belanjaId` on the `ApbDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `pembiayaanId` on the `ApbDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `pendapatanId` on the `ApbDesa` table. All the data in the column will be lost.
+ - You are about to drop the `KegiatanSubak` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `KlasifikasiBelanja` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `RincianBelanja` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_ApbDesaToKegiatanSubak` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_BelanjaToKlasifikasiBelanja` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `_KlasifikasiBelanjaToRincianBelanja` table. If the table is not empty, all the data it contains will be lost.
+ - Changed the type of `tanggal` on the `DaftarInformasiPublik` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
+ - Added the required column `updatedAt` to the `Pembiayaan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "ApbDesa" DROP CONSTRAINT "ApbDesa_belanjaId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "ApbDesa" DROP CONSTRAINT "ApbDesa_pembiayaanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "ApbDesa" DROP CONSTRAINT "ApbDesa_pendapatanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_ApbDesaToKegiatanSubak" DROP CONSTRAINT "_ApbDesaToKegiatanSubak_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_ApbDesaToKegiatanSubak" DROP CONSTRAINT "_ApbDesaToKegiatanSubak_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_BelanjaToKlasifikasiBelanja" DROP CONSTRAINT "_BelanjaToKlasifikasiBelanja_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_BelanjaToKlasifikasiBelanja" DROP CONSTRAINT "_BelanjaToKlasifikasiBelanja_B_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" DROP CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_A_fkey";
+
+-- DropForeignKey
+ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" DROP CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_B_fkey";
+
+-- AlterTable
+ALTER TABLE "ApbDesa" DROP COLUMN "belanjaId",
+DROP COLUMN "pembiayaanId",
+DROP COLUMN "pendapatanId",
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
+
+-- AlterTable
+ALTER TABLE "DaftarInformasiPublik" DROP COLUMN "tanggal",
+ADD COLUMN "tanggal" DATE NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Pembiayaan" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
+
+-- DropTable
+DROP TABLE "KegiatanSubak";
+
+-- DropTable
+DROP TABLE "KlasifikasiBelanja";
+
+-- DropTable
+DROP TABLE "RincianBelanja";
+
+-- DropTable
+DROP TABLE "_ApbDesaToKegiatanSubak";
+
+-- DropTable
+DROP TABLE "_BelanjaToKlasifikasiBelanja";
+
+-- DropTable
+DROP TABLE "_KlasifikasiBelanjaToRincianBelanja";
+
+-- CreateTable
+CREATE TABLE "DesaDigital" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DesaDigital_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramKreatif" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "slug" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "icon" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProgramKreatif_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KolaborasiInovasi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "tahun" INTEGER NOT NULL,
+ "slug" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kolaborator" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KolaborasiInovasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "InfoTekno" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "InfoTekno_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AjukanIdeInovatif" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "namaIde" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "masalah" TEXT NOT NULL,
+ "benefit" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "AjukanIdeInovatif_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AdministrasiOnline" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "nomorTelepon" TEXT NOT NULL,
+ "jenisLayananId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "AdministrasiOnline_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisLayanan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenisLayanan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "_ApbDesaPembiayaan" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_ApbDesaPembiayaan_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_ApbDesaBelanja" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_ApbDesaBelanja_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateTable
+CREATE TABLE "_ApbDesaPendapatan" (
+ "A" TEXT NOT NULL,
+ "B" TEXT NOT NULL,
+
+ CONSTRAINT "_ApbDesaPendapatan_AB_pkey" PRIMARY KEY ("A","B")
+);
+
+-- CreateIndex
+CREATE INDEX "_ApbDesaPembiayaan_B_index" ON "_ApbDesaPembiayaan"("B");
+
+-- CreateIndex
+CREATE INDEX "_ApbDesaBelanja_B_index" ON "_ApbDesaBelanja"("B");
+
+-- CreateIndex
+CREATE INDEX "_ApbDesaPendapatan_B_index" ON "_ApbDesaPendapatan"("B");
+
+-- AddForeignKey
+ALTER TABLE "DesaDigital" ADD CONSTRAINT "DesaDigital_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KolaborasiInovasi" ADD CONSTRAINT "KolaborasiInovasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InfoTekno" ADD CONSTRAINT "InfoTekno_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AdministrasiOnline" ADD CONSTRAINT "AdministrasiOnline_jenisLayananId_fkey" FOREIGN KEY ("jenisLayananId") REFERENCES "JenisLayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaPembiayaan" ADD CONSTRAINT "_ApbDesaPembiayaan_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaPembiayaan" ADD CONSTRAINT "_ApbDesaPembiayaan_B_fkey" FOREIGN KEY ("B") REFERENCES "Pembiayaan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaBelanja" ADD CONSTRAINT "_ApbDesaBelanja_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaBelanja" ADD CONSTRAINT "_ApbDesaBelanja_B_fkey" FOREIGN KEY ("B") REFERENCES "Belanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaPendapatan" ADD CONSTRAINT "_ApbDesaPendapatan_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "_ApbDesaPendapatan" ADD CONSTRAINT "_ApbDesaPendapatan_B_fkey" FOREIGN KEY ("B") REFERENCES "Pendapatan"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250718031959_nico_18_jul_25/migration.sql b/prisma/migrations/20250718031959_nico_18_jul_25/migration.sql
new file mode 100644
index 00000000..f1569287
--- /dev/null
+++ b/prisma/migrations/20250718031959_nico_18_jul_25/migration.sql
@@ -0,0 +1,67 @@
+-- CreateTable
+CREATE TABLE "PengaduanMasyarakat" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "nomorTelepon" TEXT NOT NULL,
+ "nik" TEXT NOT NULL,
+ "judulPengaduan" TEXT NOT NULL,
+ "lokasiKejadian" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "deskripsiPengaduan" TEXT NOT NULL,
+ "jenisPengaduanId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PengaduanMasyarakat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisPengaduan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenisPengaduan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PengelolaanSampah" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "icon" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PengelolaanSampah_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KeteranganBankSampahTerdekat" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "namaTempatMaps" TEXT NOT NULL,
+ "linkPetunjukArah" TEXT NOT NULL,
+ "lat" DOUBLE PRECISION NOT NULL,
+ "lng" DOUBLE PRECISION NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KeteranganBankSampahTerdekat_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_jenisPengaduanId_fkey" FOREIGN KEY ("jenisPengaduanId") REFERENCES "JenisPengaduan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250721095104_nico_21_jul_25/migration.sql b/prisma/migrations/20250721095104_nico_21_jul_25/migration.sql
new file mode 100644
index 00000000..c81499ee
--- /dev/null
+++ b/prisma/migrations/20250721095104_nico_21_jul_25/migration.sql
@@ -0,0 +1,144 @@
+-- CreateTable
+CREATE TABLE "ProgramPenghijauan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "icon" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProgramPenghijauan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataLingkunganDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "jumlah" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "icon" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DataLingkunganDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KegiatanDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsiSingkat" TEXT NOT NULL,
+ "deskripsiLengkap" TEXT NOT NULL,
+ "tanggal" TIMESTAMP(3) NOT NULL,
+ "lokasi" TEXT NOT NULL,
+ "partisipan" INTEGER NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "kategoriKegiatanId" TEXT NOT NULL,
+
+ CONSTRAINT "KegiatanDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriKegiatan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanEdukasiLingkungan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanEdukasiLingkungan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MateriEdukasiLingkungan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MateriEdukasiLingkungan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ContohEdukasiLingkungan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ContohEdukasiLingkungan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FilosofiTriHita" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FilosofiTriHita_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "BentukKonservasiBerdasarkanAdat" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "BentukKonservasiBerdasarkanAdat_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "NilaiKonservasiAdat" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "NilaiKonservasiAdat_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_kategoriKegiatanId_fkey" FOREIGN KEY ("kategoriKegiatanId") REFERENCES "KategoriKegiatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/20250722071634_nico_22_jul_25/migration.sql b/prisma/migrations/20250722071634_nico_22_jul_25/migration.sql
new file mode 100644
index 00000000..1379f639
--- /dev/null
+++ b/prisma/migrations/20250722071634_nico_22_jul_25/migration.sql
@@ -0,0 +1,56 @@
+-- CreateTable
+CREATE TABLE "PejabatDesa" (
+ "id" TEXT NOT NULL,
+ "name" VARCHAR(255) NOT NULL,
+ "position" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PejabatDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramInovasi" (
+ "id" TEXT NOT NULL,
+ "name" VARCHAR(255) NOT NULL,
+ "description" TEXT,
+ "imageId" TEXT,
+ "link" VARCHAR(255),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProgramInovasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MediaSosial" (
+ "id" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "iconUrl" VARCHAR(255),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MediaSosial_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PejabatDesa_name_key" ON "PejabatDesa"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ProgramInovasi_name_key" ON "ProgramInovasi"("name");
+
+-- AddForeignKey
+ALTER TABLE "PejabatDesa" ADD CONSTRAINT "PejabatDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ProgramInovasi" ADD CONSTRAINT "ProgramInovasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 00000000..648c57fd
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/prisma/safeseedUnique.ts b/prisma/safeseedUnique.ts
new file mode 100644
index 00000000..92d16071
--- /dev/null
+++ b/prisma/safeseedUnique.ts
@@ -0,0 +1,30 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// helpers/safeSeedUnique.ts
+import { PrismaClient } from "@prisma/client";
+
+const prisma = new PrismaClient();
+
+/**
+ * Helper generic buat seed dengan upsert aman
+ */
+export async function safeSeedUnique(
+ model: T,
+ where: Record,
+ data: Record
+) {
+ const m = prisma[model];
+
+ if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`);
+
+ try {
+ // @ts-expect-error upsert dynamic
+ await m.upsert({
+ where,
+ update: data,
+ create: { ...where, ...data },
+ });
+ console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`);
+ } catch (err) {
+ console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err);
+ }
+}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index b3991450..a52a0cb1 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -16,3 +16,2186 @@ model Potensi {
id String @id @default(cuid())
name String @unique
}
+
+model LandingPage_Layanan {
+ id String @id @default(cuid())
+ deksripsi String
+}
+
+// ========================================= APPMENU ========================================= //
+model AppMenu {
+ id String @id @default(cuid())
+ name String @unique
+ link String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ AppMenuChild AppMenuChild[]
+}
+
+// ========================================= APPMENUCHILD ========================================= //
+model AppMenuChild {
+ id String @id @default(cuid())
+ name String @unique
+ link String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ AppMenu AppMenu? @relation(fields: [appMenuId], references: [id])
+ appMenuId String?
+}
+
+// ========================================= FILE STORAGE ========================================= //
+
+model FileStorage {
+ id String @id @default(cuid())
+ name String @unique
+ realName String
+ path String
+ mimeType String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+ link String
+ category String // "image" / "document" / "other"
+ Berita Berita[]
+ PotensiDesa PotensiDesa[]
+ Posyandu Posyandu[]
+ StrukturPPID StrukturPPID[]
+ GalleryFoto GalleryFoto[]
+ Pelapor Pelapor[]
+ Penghargaan Penghargaan[]
+ ProfileDesaImage ProfileDesaImage[]
+ ProfilePPID ProfilePPID[]
+ ProfilPerbekel ProfilPerbekel[]
+ Puskesmas Puskesmas[]
+ ProgramKesehatan ProgramKesehatan[]
+ PenangananDarurat PenangananDarurat[]
+ KontakDarurat KontakDarurat[]
+ InfoWabahPenyakit InfoWabahPenyakit[]
+ KeamananLingkungan KeamananLingkungan[]
+ MenuTipsKeamanan MenuTipsKeamanan[]
+ PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
+ PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
+ PasarDesa PasarDesa[]
+ PegawaiBumDes PegawaiBumDes[]
+ DesaDigital DesaDigital[]
+ InfoTekno InfoTekno[]
+ PengaduanMasyarakat PengaduanMasyarakat[]
+ KegiatanDesa KegiatanDesa[]
+ ProgramInovasi ProgramInovasi[]
+ PejabatDesa PejabatDesa[]
+ MediaSosial MediaSosial[]
+ DesaAntiKorupsi DesaAntiKorupsi[]
+ SDGSDesa SdgsDesa[]
+ APBDesImage APBDes[] @relation("APBDesImage")
+ APBDesFile APBDes[] @relation("APBDesFile")
+ PrestasiDesa PrestasiDesa[]
+ DataPerpustakaan DataPerpustakaan[]
+ PegawaiPPID PegawaiPPID[]
+ PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
+
+ MitraKolaborasi MitraKolaborasi[]
+
+ ArtikelKesehatan ArtikelKesehatan[]
+ StrukturBumDes StrukturBumDes[]
+}
+
+//========================================= MENU LANDING PAGE ========================================= //
+//========================================= PROFILE ========================================= //
+model PejabatDesa {
+ id String @id @default(cuid())
+ name String @unique @db.VarChar(255)
+ position String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model ProgramInovasi {
+ id String @id @default(cuid())
+ name String @unique @db.VarChar(255)
+ description String? @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ link String? @db.VarChar(255)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime? @default(now())
+ isActive Boolean @default(true)
+}
+
+model MediaSosial {
+ id String @id @default(cuid())
+ name String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ iconUrl String? @db.VarChar(255)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+}
+
+//========================================= DESA ANTI KORUPSI ========================================= //
+model DesaAntiKorupsi {
+ id String @id @default(cuid())
+ name String @unique
+ deskripsi String @db.Text
+ kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
+ kategoriId String
+ file FileStorage? @relation(fields: [fileId], references: [id])
+ fileId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model KategoriDesaAntiKorupsi {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ DesaAntiKorupsi DesaAntiKorupsi[]
+}
+
+//========================================= SDGS Desa ========================================= //
+model SdgsDesa {
+ id String @id @default(cuid())
+ name String
+ jumlah String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+//========================================= APBDes ========================================= //
+model APBDes {
+ id String @id @default(cuid())
+ name String
+ jumlah String
+ image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
+ imageId String?
+ file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
+ fileId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+//========================================= PRESTASI DESA ========================================= //
+model PrestasiDesa {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
+ kategoriId String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model KategoriPrestasiDesa {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PrestasiDesa PrestasiDesa[]
+}
+
+//========================================= INDEKS KEPUASAAN MASYARAKAT ========================================= //
+model Responden {
+ id String @id @default(cuid())
+ name String @unique
+ tanggal DateTime @db.Date // misal: 2025-05-01
+ jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
+ jenisKelaminId String
+ rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
+ ratingId String
+ kelompokUmur UmurResponden @relation(fields: [kelompokUmurId], references: [id])
+ kelompokUmurId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model JenisKelaminResponden {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ Responden Responden[]
+}
+
+model PilihanRatingResponden {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ Responden Responden[]
+}
+
+model UmurResponden {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ Responden Responden[]
+}
+
+//========================================= MENU PPID ========================================= //
+
+//========================================= STRUKTUR PPID ========================================= //
+model StrukturPPID {
+ id String @id @default(cuid())
+ name String @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PosisiOrganisasiPPID PosisiOrganisasiPPID? @relation(fields: [posisiOrganisasiPPIDId], references: [id])
+ posisiOrganisasiPPIDId String?
+ PegawaiPPID PegawaiPPID? @relation(fields: [pegawaiPPIDId], references: [id])
+ pegawaiPPIDId String?
+}
+
+model PosisiOrganisasiPPID {
+ id String @id @default(cuid())
+ nama String @db.VarChar(100)
+ deskripsi String? @db.Text
+ hierarki Int
+ pegawai PegawaiPPID[]
+ strukturOrganisasi StrukturPPID[] // Relasi balik
+ parentId String?
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
+ children PosisiOrganisasiPPID[] @relation("Parent")
+ StrukturOrganisasiPPID StrukturOrganisasiPPID[]
+}
+
+model PegawaiPPID {
+ id String @id @default(cuid())
+ namaLengkap String @db.VarChar(255)
+ gelarAkademik String? @db.VarChar(100)
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ tanggalMasuk DateTime? @db.Date
+ email String? @unique @db.VarChar(255)
+ telepon String? @db.VarChar(20)
+ alamat String? @db.Text
+ posisiId String @db.VarChar(50)
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
+ strukturOrganisasi StrukturPPID[] // Relasi balik
+ StrukturOrganisasiPPID StrukturOrganisasiPPID[]
+}
+
+model StrukturOrganisasiPPID {
+ id String @id @default(uuid())
+ posisiOrganisasiId String @db.VarChar(50)
+ pegawaiId String
+ hubunganOrganisasiId String
+ posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
+ pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+}
+
+// ========================================= VISI MISI PPID ========================================= //
+model VisiMisiPPID {
+ id String @id @default(cuid())
+ visi String @db.Text
+ misi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= DASAR HUKUM PPID ========================================= //
+model DasarHukumPPID {
+ id String @id @default(cuid())
+ judul String @db.Text
+ content String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PROFILE PPID ========================================= //
+model ProfilePPID {
+ id String @id @default(cuid())
+ name String @db.Text
+ biodata String @db.Text
+ riwayat String @db.Text
+ pengalaman String @db.Text
+ unggulan String @db.Text
+ imageUrl String?
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= DAFTAR INFORMASI PUBLIK ========================================= //
+model DaftarInformasiPublik {
+ id String @id @default(cuid())
+ jenisInformasi String
+ deskripsi String
+ tanggal DateTime @db.Date
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+//=========================================PERMOHONAN INFORMASI PUBLIK========================= //
+model PermohonanInformasiPublik {
+ id String @id @default(cuid())
+ nomor Int @default(autoincrement())
+ name String
+ nik String
+ notelp String
+ alamat String
+ email String
+ jenisInformasiDiminta JenisInformasiDiminta? @relation(fields: [jenisInformasiDimintaId], references: [id])
+ jenisInformasiDimintaId String?
+ caraMemperolehInformasi CaraMemperolehInformasi? @relation(fields: [caraMemperolehInformasiId], references: [id])
+ caraMemperolehInformasiId String?
+ caraMemperolehSalinanInformasi CaraMemperolehSalinanInformasi? @relation(fields: [caraMemperolehSalinanInformasiId], references: [id])
+ caraMemperolehSalinanInformasiId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model JenisInformasiDiminta {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PermohonanInformasiPublik PermohonanInformasiPublik[]
+}
+
+model CaraMemperolehInformasi {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PermohonanInformasiPublik PermohonanInformasiPublik[]
+}
+
+model CaraMemperolehSalinanInformasi {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PermohonanInformasiPublik PermohonanInformasiPublik[]
+}
+
+//=========================================PERMOHONAN INFORMASI KEBERATAN PUBLIK========================= //
+model FormulirPermohonanKeberatan {
+ id String @id @default(cuid())
+ name String
+ email String
+ notelp String
+ alasan String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= IKM ========================================= //
+model IndeksKepuasanMasyarakat {
+ id String @id @default(cuid())
+ label String
+ kepuasan String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model GrafikBerdasarkanJenisKelamin {
+ id String @id @default(cuid())
+ perempuan String
+ laki String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model GrafikBerdasarkanResponden {
+ id String @id @default(cuid())
+ sangatbaik String
+ baik String
+ kurangbaik String
+ tidakbaik String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model GrafikBerdasarkanUmur {
+ id String @id @default(cuid())
+ remaja String
+ dewasa String
+ orangtua String
+ lansia String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= MENU DESA ========================================= //
+// ========================================= PROFILE DESA ========================================= //
+model SejarahDesa {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model VisiMisiDesa {
+ id String @id @default(cuid())
+ visi String @db.Text
+ misi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model LambangDesa {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model MaskotDesa {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ images ProfileDesaImage[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model ProfileDesaImage {
+ id String @id @default(cuid())
+ label String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ MaskotDesa MaskotDesa @relation(fields: [maskotDesaId], references: [id])
+ maskotDesaId String
+}
+
+model ProfilPerbekel {
+ id String @id @default(cuid())
+ biodata String @db.Text
+ pengalaman String @db.Text
+ pengalamanOrganisasi String @db.Text
+ programUnggulan String @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model PerbekelDariMasaKeMasa {
+ id String @id @default(cuid())
+ nama String @db.Text
+ periode String @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ daerah String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= BERITA ========================================= //
+model Berita {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ content String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ kategoriBerita KategoriBerita? @relation(fields: [kategoriBeritaId], references: [id])
+ kategoriBeritaId String?
+}
+
+model KategoriBerita {
+ id String @id @default(cuid())
+ name String @unique
+ beritas Berita[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= POTENSI DESA ========================================= //
+model PotensiDesa {
+ id String @id @default(cuid())
+ name String
+ deskripsi String
+ kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
+ kategoriId String?
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ content String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model KategoriPotensi {
+ id String @id @default(cuid())
+ nama String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PotensiDesa PotensiDesa[]
+}
+
+// ========================================= PENGUMUMAN ========================================= //
+model Pengumuman {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String
+ content String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ CategoryPengumuman CategoryPengumuman? @relation(fields: [categoryPengumumanId], references: [id])
+ categoryPengumumanId String?
+}
+
+model CategoryPengumuman {
+ id String @id @default(cuid())
+ name String @unique
+ pengumumans Pengumuman[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= GALLERY ========================================= //
+model GalleryFoto {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ imagesId String? @unique
+ imageGalleryFoto FileStorage? @relation(fields: [imagesId], references: [id])
+}
+
+model GalleryVideo {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ linkVideo String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= LAYANAN DESA ========================================= //
+model PelayananSuratKeterangan {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
+ imageId String?
+ image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
+ image2Id String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ AjukanPermohonan AjukanPermohonan[]
+}
+
+model PelayananTelunjukSaktiDesa {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ link String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model PelayananPerizinanBerusaha {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ link String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model PelayananPendudukNonPermanen {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model AjukanPermohonan {
+ id String @id @default(cuid())
+ nama String
+ nik String
+ alamat String
+ nomorKk String
+ kategori PelayananSuratKeterangan @relation(fields: [kategoriId], references: [id])
+ kategoriId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PENGHARGAAN ========================================= //
+model Penghargaan {
+ id String @id @default(cuid())
+ name String
+ juara String
+ deskripsi String @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= MENU KESEHATAN ========================================= //
+// ========================================= DATA KESEHATAN WARGA ========================================= //
+
+// ========================================= FASILITAS KESEHATAN ========================================= //
+model FasilitasKesehatan {
+ id String @id @default(cuid())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
+ informasiUmumId String
+ layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id])
+ layananUnggulanId String
+ dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id])
+ dokterdanTenagaMedisId String
+ fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
+ fasilitasPendukungId String
+ prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
+ prosedurPendaftaranId String
+ tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id])
+ tarifDanLayananId String
+}
+
+model InformasiUmum {
+ id String @id @default(cuid())
+ fasilitas String
+ alamat String
+ jamOperasional String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ FasilitasKesehatan FasilitasKesehatan[]
+ isActive Boolean @default(true)
+}
+
+model LayananUnggulan {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ FasilitasKesehatan FasilitasKesehatan[]
+}
+
+model DokterdanTenagaMedis {
+ id String @id @default(cuid())
+ name String
+ specialist String
+ jadwal String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ FasilitasKesehatan FasilitasKesehatan[]
+}
+
+model FasilitasPendukung {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ FasilitasKesehatan FasilitasKesehatan[]
+}
+
+model ProsedurPendaftaran {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ FasilitasKesehatan FasilitasKesehatan[]
+}
+
+model TarifDanLayanan {
+ id String @id @default(cuid())
+ layanan String
+ tarif String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ FasilitasKesehatan FasilitasKesehatan[]
+}
+
+// ========================================= JADWAL KEGIATAN ========================================= //
+model JadwalKegiatan {
+ id String @id @default(cuid())
+ content String
+ informasijadwalkegiatan InformasiJadwalKegiatan @relation(fields: [informasiJadwalKegiatanId], references: [id])
+ informasiJadwalKegiatanId String
+ deskripsijadwalkegiatan DeskripsiJadwalKegiatan @relation(fields: [deskripsiJadwalKegiatanId], references: [id])
+ deskripsiJadwalKegiatanId String
+ layananjadwalkegiatan LayananJadwalKegiatan @relation(fields: [layananJadwalKegiatanId], references: [id])
+ layananJadwalKegiatanId String
+ syaratketentuanjadwalkegiatan SyaratKetentuanJadwalKegiatan @relation(fields: [syaratKetentuanJadwalKegiatanId], references: [id])
+ syaratKetentuanJadwalKegiatanId String
+ dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
+ dokumenJadwalKegiatanId String
+ pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
+ pendaftaranJadwalKegiatanId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model InformasiJadwalKegiatan {
+ id String @id @default(cuid())
+ name String
+ tanggal String
+ waktu String
+ lokasi String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ JadwalKegiatan JadwalKegiatan[]
+}
+
+model DeskripsiJadwalKegiatan {
+ id String @id @default(cuid())
+ deskripsi String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ JadwalKegiatan JadwalKegiatan[]
+}
+
+model LayananJadwalKegiatan {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ JadwalKegiatan JadwalKegiatan[]
+}
+
+model SyaratKetentuanJadwalKegiatan {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ JadwalKegiatan JadwalKegiatan[]
+}
+
+model DokumenJadwalKegiatan {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ JadwalKegiatan JadwalKegiatan[]
+}
+
+model PendaftaranJadwalKegiatan {
+ id String @id @default(cuid())
+ name String
+ tanggal String
+ namaOrangtua String
+ nomor String
+ alamat String
+ catatan String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ JadwalKegiatan JadwalKegiatan[]
+}
+
+// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
+model DataKematian_Kelahiran {
+ id String @id @default(cuid())
+ kematian Kematian @relation(fields: [kematianId], references: [id])
+ kematianId String
+ kelahiran Kelahiran @relation(fields: [kelahiranId], references: [id])
+ kelahiranId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Kelahiran {
+ id String @id @default(cuid())
+ nama String
+ tanggal DateTime
+ jenisKelamin String
+ alamat String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ DataKematian_Kelahiran DataKematian_Kelahiran[]
+}
+
+model Kematian {
+ id String @id @default(cuid())
+ nama String
+ tanggal DateTime
+ jenisKelamin String
+ alamat String
+ penyebab String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ DataKematian_Kelahiran DataKematian_Kelahiran[]
+}
+
+// ========================================= GRAFIK KEPUASAN ========================================= //
+model GrafikKepuasan {
+ id String @id @default(cuid())
+ nama String
+ tanggal DateTime
+ jenisKelamin String
+ alamat String
+ penyakit String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= ARTIKEL KESEHATAN ========================================= //
+model ArtikelKesehatan {
+ id String @id @default(cuid())
+ title String
+ content String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ introductionId String
+ introduction Introduction @relation(fields: [introductionId], references: [id])
+ symptom Symptom @relation(fields: [symptomId], references: [id])
+ symptomId String
+ prevention Prevention @relation(fields: [preventionId], references: [id])
+ preventionId String
+ firstaid FirstAid @relation(fields: [firstAidId], references: [id])
+ firstAidId String
+ mythvsfact MythVsFact @relation(fields: [mythVsFactId], references: [id])
+ mythVsFactId String
+ doctorsign DoctorSign @relation(fields: [doctorSignId], references: [id])
+ doctorSignId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Introduction {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ ArtikelKesehatan ArtikelKesehatan[]
+}
+
+model Symptom {
+ id String @id @default(cuid())
+ title String
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ ArtikelKesehatan ArtikelKesehatan[]
+}
+
+model Prevention {
+ id String @id @default(cuid())
+ title String
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ ArtikelKesehatan ArtikelKesehatan[]
+}
+
+model FirstAid {
+ id String @id @default(cuid())
+ title String
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ ArtikelKesehatan ArtikelKesehatan[]
+}
+
+model MythVsFact {
+ id String @id @default(cuid())
+ title String
+ mitos String
+ fakta String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ ArtikelKesehatan ArtikelKesehatan[]
+}
+
+model DoctorSign {
+ id String @id @default(cuid())
+ content String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ ArtikelKesehatan ArtikelKesehatan[]
+}
+
+// ========================================= POSYANDU ========================================= //
+model Posyandu {
+ id String @id @default(cuid())
+ name String
+ nomor String
+ deskripsi String
+ jadwalPelayanan String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PUSKESMAS ========================================= //
+model Puskesmas {
+ id String @id @default(cuid())
+ name String
+ alamat String
+ jam JamOperasional @relation(fields: [jamId], references: [id])
+ jamId String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ kontak KontakPuskesmas @relation(fields: [kontakId], references: [id])
+ kontakId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model JamOperasional {
+ id String @id @default(cuid())
+ workDays String
+ weekDays String
+ holiday String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ Puskesmas Puskesmas[]
+}
+
+model KontakPuskesmas {
+ id String @id @default(cuid())
+ kontakPuskesmas String
+ email String
+ facebook String
+ kontakUGD String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ Puskesmas Puskesmas[]
+}
+
+// ========================================= PROGRAM KESSEHATAN ========================================= //
+model ProgramKesehatan {
+ id String @id @default(cuid())
+ name String
+ deskripsiSingkat String
+ deskripsi String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PENANGANAN DARURAT ========================================= //
+model PenangananDarurat {
+ id String @id @default(cuid())
+ name String
+ deskripsi String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= KONTAK DARURAT ========================================= //
+model KontakDarurat {
+ id String @id @default(cuid())
+ name String
+ deskripsi String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ whatsapp String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= INFO WABAH PENYAKIT ========================================= //
+model InfoWabahPenyakit {
+ id String @id @default(cuid())
+ name String
+ deskripsiSingkat String
+ deskripsiLengkap String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= MENU KEAMANAN ========================================= //
+// ========================================= KEAMANAN LINGKUNGAN ========================================= //
+model KeamananLingkungan {
+ id String @id @default(cuid())
+ name String @db.Text
+ deskripsi String @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= POLSEK TERDEKAT ========================================= //
+model PolsekTerdekat {
+ id String @id @default(uuid())
+ nama String
+ jarakKeDesa String
+ alamat String
+ nomorTelepon String
+ jamOperasional String
+ embedMapUrl String
+ namaTempatMaps String
+ alamatMaps String
+ linkPetunjukArah String
+ layananPolsek LayananPolsek @relation(fields: [layananPolsekId], references: [id])
+ layananPolsekId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model LayananPolsek {
+ id String @id @default(uuid())
+ nama String // contoh: "Pelayanan SKCK", "Laporan Kriminal"
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+ PolsekTerdekat PolsekTerdekat[]
+}
+
+// ========================================= KONTAK DARURAT ========================================= //
+model KontakDaruratKeamanan {
+ id String @id @default(uuid())
+ nama String
+ icon String
+ kategori KontakItem @relation(fields: [kategoriId], references: [id])
+ kategoriId String
+ kontakItems KontakDaruratToItem[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+}
+
+model KontakItem {
+ id String @id @default(uuid())
+ nama String
+ nomorTelepon String
+ icon String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ KontakDaruratToItem KontakDaruratToItem[]
+ KontakDaruratKeamanan KontakDaruratKeamanan[]
+}
+
+model KontakDaruratToItem {
+ id String @id @default(uuid())
+ kontakDaruratId String
+ kontakItemId String
+ kontakDarurat KontakDaruratKeamanan @relation(fields: [kontakDaruratId], references: [id])
+ kontakItem KontakItem @relation(fields: [kontakItemId], references: [id])
+ createdAt DateTime @default(now())
+}
+
+// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
+model PencegahanKriminalitas {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String
+ deskripsiSingkat String
+ linkVideo String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= LAPORAN PUBLIK ========================================= //
+model LaporanPublik {
+ id String @id @default(cuid())
+ judul String
+ lokasi String
+ tanggalWaktu DateTime
+ status StatusLaporan @default(Proses)
+ penanganan PenangananLaporanPublik[]
+ kronologi String? // Optional, bisa diisi detail kronologi
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model PenangananLaporanPublik {
+ id String @id @default(cuid())
+ laporanId String
+ deskripsi String
+ laporan LaporanPublik @relation(fields: [laporanId], references: [id], onDelete: Cascade)
+}
+
+enum StatusLaporan {
+ Selesai
+ Proses
+ Gagal
+}
+
+model Pelapor {
+ id String @id @default(cuid())
+ nama String
+ alamat String
+ nomorTelepon String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+}
+
+// ========================================= TIPS KEAMANAN ========================================= //
+model MenuTipsKeamanan {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= MENU EKONOMI ========================================= //
+// ========================================= PASAR DESA ========================================= //
+model PasarDesa {
+ id String @id @default(uuid())
+ nama String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ harga Int
+ rating Float
+ alamatUsaha String
+ kontak String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
+ kategoriProdukId String
+ KategoriToPasar KategoriToPasar[]
+}
+
+model KategoriProduk {
+ id String @id @default(uuid())
+ nama String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ KategoriToPasar KategoriToPasar[]
+ PasarDesa PasarDesa[]
+}
+
+model KategoriToPasar {
+ id String @id @default(uuid())
+ kategori KategoriProduk @relation(fields: [kategoriId], references: [id])
+ kategoriId String
+ pasarDesa PasarDesa @relation(fields: [pasarDesaId], references: [id])
+ pasarDesaId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= LOWONGAN KERJA LOKAL ========================================= //
+model LowonganPekerjaan {
+ id String @id @default(uuid())
+ posisi String
+ namaPerusahaan String
+ lokasi String
+ tipePekerjaan String
+ gaji String
+ deskripsi String
+ kualifikasi String
+ notelp String
+ tanggalPosting DateTime @default(now())
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+}
+
+// ========================================= STRUKTUR ORGANISASI ========================================= //
+
+model StrukturBumDes {
+ id String @id @default(cuid())
+ name String @db.Text
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PosisiOrganisasiBumDes PosisiOrganisasiBumDes? @relation(fields: [posisiOrganisasiBumDesId], references: [id])
+ posisiOrganisasiBumDesId String?
+ PegawaiBumDes PegawaiBumDes? @relation(fields: [pegawaiBumDesId], references: [id])
+ pegawaiBumDesId String?
+}
+
+model PosisiOrganisasiBumDes {
+ id String @id @default(cuid())
+ nama String @db.VarChar(100)
+ deskripsi String? @db.Text
+ hierarki Int
+ pegawai PegawaiBumDes[]
+ strukturOrganisasi StrukturBumDes[] // Relasi balik
+ parentId String?
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id])
+ children PosisiOrganisasiBumDes[] @relation("Parent")
+ StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
+}
+
+model PegawaiBumDes {
+ id String @id @default(cuid())
+ namaLengkap String @db.VarChar(255)
+ gelarAkademik String? @db.VarChar(100)
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ tanggalMasuk DateTime? @db.Date
+ email String? @unique @db.VarChar(255)
+ telepon String? @db.VarChar(20)
+ alamat String? @db.Text
+ posisiId String @db.VarChar(50)
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ posisi PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id])
+ strukturOrganisasi StrukturBumDes[] // Relasi balik
+ StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
+}
+
+model StrukturOrganisasiBumDes {
+ id String @id @default(uuid())
+ posisiOrganisasiId String @db.VarChar(50)
+ pegawaiId String
+ hubunganOrganisasiId String
+ posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
+ pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+}
+
+// ========================================= PROGRAM KEMISKINAN ========================================= //
+model ProgramKemiskinan {
+ id String @id @default(uuid())
+ nama String
+ deskripsi String
+ icon String
+ isActive Boolean @default(true)
+ statistikId String? @unique
+ statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+model StatistikKemiskinan {
+ id String @id @default(uuid())
+ tahun Int @unique
+ jumlah Int
+ programKemiskinan ProgramKemiskinan?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+// ========================================= JUMLAH PENDUDUK USIA KERJA YANG MENGANGGUR ========================================= //
+model GrafikMenganggurBerdasarkanUsia {
+ id String @id @default(cuid())
+ usia18_25 String
+ usia26_35 String
+ usia36_45 String
+ usia46_keatas String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model GrafikMenganggurBerdasarkanPendidikan {
+ id String @id @default(cuid())
+ SD String
+ SMP String
+ SMA String
+ D3 String
+ S1 String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= JUMLAH PENDUDUK MISKIN ========================================= //
+model GrafikJumlahPendudukMiskin {
+ id String @id @default(uuid()) @db.Uuid // Menggunakan UUID sebagai primary key
+ year Int @unique // Tahun data (e.g., 2024, 2025)
+ totalPoorPopulation Int // Jumlah penduduk miskin (e.g., 4800000)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= SEKTOR UNGGULAN DESA ========================================= //
+model SektorUnggulanDesa {
+ id String @id @default(uuid()) @db.Uuid // Menggunakan UUID sebagai primary key
+ name String @unique @db.VarChar(100) // Nama sektor (e.g., "Sektor Pertanian", "Sektor Peternakan")
+ description String? @db.Text // Deskripsi lengkap tentang sektor
+ value Float? // Nilai kuantitatif sektor (misalnya, kontribusi PDB, jumlah produksi, dll.)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= DEMOGRAFI PEKERJAAN ========================================= //
+model DataDemografiPekerjaan {
+ id String @id @default(cuid())
+ pekerjaan String
+ lakiLaki Int
+ perempuan Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= JUMLAH PENGANGGURAN ========================================= //
+model DetailDataPengangguran {
+ id String @id @default(uuid()) @db.Uuid
+ month String @db.VarChar(20)
+ year Int
+ totalUnemployment Int
+ educatedUnemployment Int
+ uneducatedUnemployment Int
+ percentageChange Float?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ @@unique([month, year])
+}
+
+// ========================================= PADESA PENDAPATAN ASLI DESA ========================================= //
+model ApbDesa {
+ id String @id @default(uuid())
+ tahun Int
+ pembiayaan Pembiayaan[] @relation("ApbDesaPembiayaan")
+ belanja Belanja[] @relation("ApbDesaBelanja")
+ pendapatan Pendapatan[] @relation("ApbDesaPendapatan")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Pendapatan {
+ id String @id @default(uuid())
+ name String
+ value Int
+ ApbDesa ApbDesa[] @relation("ApbDesaPendapatan")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Belanja {
+ id String @id @default(uuid())
+ name String
+ value Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ ApbDesa ApbDesa[] @relation("ApbDesaBelanja")
+}
+
+model Pembiayaan {
+ id String @id @default(uuid())
+ name String
+ value Int
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
+}
+
+// ========================================= MENU INOVASI ========================================= //
+// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
+model DesaDigital {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PROGRAM KREATIF ========================================= //
+model ProgramKreatif {
+ id String @id @default(cuid())
+ name String
+ slug String @db.Text //deskripsi singkat
+ deskripsi String @db.Text //deskripsi panjang
+ icon String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= KOLABORASI INOVASI ========================================= //
+model KolaborasiInovasi {
+ id String @id @default(cuid())
+ name String
+ tahun Int
+ slug String @db.Text //deskripsi singkat
+ deskripsi String @db.Text //deskripsi panjang
+ kolaborator String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model MitraKolaborasi {
+ id String @id @default(cuid())
+ name String
+ image FileStorage? @relation(fields: [imageId], references: [id])
+ imageId String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
+model InfoTekno {
+ id String @id @default(cuid())
+ name String
+ deskripsi String @db.Text
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= AJUKAN IDE INOVATIF ========================================= //
+model AjukanIdeInovatif {
+ id String @id @default(cuid())
+ name String
+ alamat String
+ namaIde String
+ deskripsi String @db.Text
+ masalah String @db.Text
+ benefit String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= LAYANAN ONLINE DESA ========================================= //
+model AdministrasiOnline {
+ id String @id @default(cuid())
+ name String
+ alamat String
+ nomorTelepon String
+ jenisLayanan JenisLayanan @relation(fields: [jenisLayananId], references: [id])
+ jenisLayananId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model JenisLayanan {
+ id String @id @default(uuid())
+ nama String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ AdministrasiOnline AdministrasiOnline[]
+}
+
+model PengaduanMasyarakat {
+ id String @id @default(cuid())
+ name String
+ email String
+ nomorTelepon String
+ nik String
+ judulPengaduan String
+ lokasiKejadian String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ deskripsiPengaduan String @db.Text
+ jenisPengaduan JenisPengaduan @relation(fields: [jenisPengaduanId], references: [id])
+ jenisPengaduanId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model JenisPengaduan {
+ id String @id @default(uuid())
+ nama String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ PengaduanMasyarakat PengaduanMasyarakat[]
+}
+
+// ========================================= LINGKUNGAN ========================================= //
+// ========================================= PENGELOLAAN SAMPAH ========================================= //
+model PengelolaanSampah {
+ id String @id @default(cuid())
+ name String
+ icon String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model KeteranganBankSampahTerdekat {
+ id String @id @default(cuid())
+ name String
+ alamat String
+ namaTempatMaps String
+ linkPetunjukArah String
+ lat Float
+ lng Float
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PORGRAM PENGHIJAUAN ========================================= //
+model ProgramPenghijauan {
+ id String @id @default(cuid())
+ name String
+ judul String @db.Text //deskripsi singkat
+ deskripsi String @db.Text //deskripsi panjang
+ icon String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= DATA LINGKUNGAN DESA ========================================= //
+model DataLingkunganDesa {
+ id String @id @default(cuid())
+ name String
+ jumlah String
+ deskripsi String @db.Text //deskripsi panjang
+ icon String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= GOTONG ROYONG ========================================= //
+model KegiatanDesa {
+ id String @id @default(uuid())
+ judul String
+ deskripsiSingkat String
+ deskripsiLengkap String
+ tanggal DateTime
+ lokasi String
+ partisipan Int
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ kategoriKegiatan KategoriKegiatan @relation(fields: [kategoriKegiatanId], references: [id])
+ kategoriKegiatanId String
+}
+
+model KategoriKegiatan {
+ id String @id @default(cuid())
+ nama String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ KegiatanDesa KegiatanDesa[]
+}
+
+// ========================================= EDUKASI LINGKUNGAN ========================================= //
+model TujuanEdukasiLingkungan {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model MateriEdukasiLingkungan {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model ContohEdukasiLingkungan {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= KONSERVASI ADAT BALI ========================================= //
+model FilosofiTriHita {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model BentukKonservasiBerdasarkanAdat {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model NilaiKonservasiAdat {
+ id String @id @default(cuid())
+ judul String @db.Text
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= MENU PENDIDIKAN ========================================= //
+// ========================================= INFO SEKOLAH & PAUD ========================================= //
+model JenjangPendidikan {
+ id String @id @default(cuid())
+ nama String // TK/PAUD, SD, SMP, SMA/SMK
+ lembagas Lembaga[] // Relasi ke lembaga
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Lembaga {
+ id String @id @default(cuid())
+ nama String
+ jenjangPendidikan JenjangPendidikan @relation(fields: [jenjangId], references: [id])
+ jenjangId String
+ siswa Siswa[]
+ pengajar Pengajar[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Siswa {
+ id String @id @default(cuid())
+ nama String
+ lembaga Lembaga @relation(fields: [lembagaId], references: [id])
+ lembagaId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model Pengajar {
+ id String @id @default(cuid())
+ nama String
+ lembaga Lembaga @relation(fields: [lembagaId], references: [id])
+ lembagaId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= BEASISWA DESA ========================================= //
+model KeunggulanProgram {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model BeasiswaPendaftar {
+ id String @id @default(cuid())
+ namaLengkap String
+ nik String @unique
+ tempatLahir String
+ tanggalLahir DateTime
+ jenisKelamin JenisKelamin
+ kewarganegaraan String
+ agama Agama
+ alamatKTP String
+ alamatDomisili String?
+ noHp String
+ email String @unique
+ statusPernikahan StatusPernikahan
+ ukuranBaju UkuranBaju?
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+enum JenisKelamin {
+ LAKI_LAKI
+ PEREMPUAN
+}
+
+enum Agama {
+ ISLAM
+ KRISTEN_PROTESTAN
+ KRISTEN_KATOLIK
+ HINDU
+ BUDDHA
+ KONGHUCU
+ LAINNYA
+}
+
+enum StatusPernikahan {
+ BELUM_MENIKAH
+ MENIKAH
+ JANDA_DUDA
+}
+
+enum UkuranBaju {
+ S
+ M
+ L
+ XL
+ XXL
+ LAINNYA
+}
+
+// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
+model TujuanProgram {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model ProgramUnggulan {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= BIMBINGAN BELAJAR DESA ========================================= //
+model TujuanBimbinganBelajarDesa {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model LokasiJadwalBimbinganBelajarDesa {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model FasilitasBimbinganBelajarDesa {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PENDIDIKAN NON FORMAL ========================================= //
+model TujuanPendidikanNonFormal {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model TempatKegiatan {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+model JenisProgramYangDiselenggarakan {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+// ========================================= PERPUSTAKAAN ========================================= //
+model DataPerpustakaan {
+ id String @id @default(cuid())
+ judul String
+ deskripsi String @db.Text
+ kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
+ kategoriId String
+ image FileStorage @relation(fields: [imageId], references: [id])
+ imageId String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+
+ // relasi baru ke peminjaman
+ peminjamanBuku PeminjamanBuku[]
+}
+
+model KategoriBuku {
+ id String @id @default(cuid())
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+ DataPerpustakaan DataPerpustakaan[]
+}
+
+model PeminjamanBuku {
+ id String @id @default(cuid())
+ nama String
+ noTelp String
+ alamat String
+ bukuId String
+ tanggalPinjam DateTime @default(now())
+ batasKembali DateTime // tenggat waktu pengembalian
+ tanggalKembali DateTime? // diisi saat dikembalikan
+ status StatusPeminjaman @default(Dipinjam)
+ catatan String? // opsional, misal: kondisi buku
+ buku DataPerpustakaan @relation(fields: [bukuId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+enum StatusPeminjaman {
+ Dipinjam
+ Dikembalikan
+ Terlambat
+ Dibatalkan
+}
+
+// ========================================= USER ========================================= //
+
+model User {
+ id String @id @default(cuid())
+ username String
+ nomor String @unique
+ role Role @relation(fields: [roleId], references: [id])
+ roleId String @default("1")
+ instansi String?
+ UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
+ isActive Boolean @default(true)
+ lastLogin DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+}
+
+model Role {
+ id String @id @default(cuid())
+ name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
+ description String?
+ permissions Json // Menyimpan permission dalam format JSON
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ users User[]
+
+ @@map("roles")
+}
+
+model KodeOtp {
+ id String @id @default(cuid())
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ nomor String
+ otp Int
+}
+
+// Tabel untuk menyimpan permission
+model Permission {
+ id String @id @default(cuid())
+ name String @unique
+ description String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@map("permissions")
+}
+
+model UserSession {
+ id String @id @default(cuid())
+ token String
+ expires DateTime?
+ active Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ User User @relation(fields: [userId], references: [id])
+ userId String @unique
+}
+
+// ========================================= DATA PENDIDIKAN ========================================= //
+model DataPendidikan {
+ id String @id @default(cuid())
+ name String
+ jumlah String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 6fa88d8c..52de4324 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,48 +1,1201 @@
-import layanan from './data/list-layanan.json'
-import potensi from './data/list-potensi.json'
-import prisma from '@/lib/prisma';
-; (async () => {
- for (const l of layanan) {
- await prisma.layanan.upsert({
- where: {
- name: l.name
- },
- update: {
- name: l.name
- },
- create: {
- name: l.name
- }
- })
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import prisma from "@/lib/prisma";
+import profilePejabatDesa from "./data/landing-page/profile/profile.json";
+import programInovasi from "./data/landing-page/profile/programInovasi.json";
+import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
+import desaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/desaantiKorpusi.json";
+import kategoriDesaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json";
+import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json";
+import apbdes from "./data/landing-page/apbdes/apbdes.json";
+import kategoriPrestasiDesa from "./data/landing-page/prestasi-desa/kategori-prestasi.json";
+import prestasiDesa from "./data/landing-page/prestasi-desa/prestasi-desa.json";
+import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
+import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
+import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
+import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
+import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
+import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
+import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
+import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json";
+import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
+import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
+import categoryPengumuman from "./data/category-pengumuman.json";
+import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
+import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
+import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
+import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
+import lambangDesa from "./data/desa/profile/lambang_desa.json";
+import maskotDesa from "./data/desa/profile/maskot_desa.json";
+import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
+import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
+import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
+import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
+import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
+import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
+import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
+import kategoriBerita from "./data/desa/berita/kategori-berita.json";
+import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
+import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
+import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
+import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
+import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json";
+import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
+import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
+import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
+import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json";
+import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
+import potensi from "./data/list-potensi.json";
+import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
+import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
+import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
+import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
+import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
+import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
+import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
+import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
+import roles from "./data/user/roles.json";
+import users from "./data/user/users.json";
+import fileStorage from "./data/file-storage.json";
+import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
+import seedAssets from "./seed_assets";
+import { safeSeedUnique } from "./safeseedUnique";
+
+(async () => {
+ // =========== USER & ROLE ===========
+ // In your seed.ts
+ // =========== ROLES ===========
+ console.log("🔄 Seeding roles...");
+ for (const r of roles) {
+ await safeSeedUnique("role", { id: r.id }, {
+ name: r.name,
+ description: r.description,
+ permissions: r.permissions,
+ isActive: r.isActive,
+ });
+ }
+
+ console.log("✅ Roles seeded");
+
+ // =========== USERS ===========
+ console.log("🔄 Seeding users...");
+ for (const u of users) {
+ // First verify the role exists
+ const roleExists = await prisma.role.findUnique({
+ where: { id: u.roleId },
+ });
+
+ if (!roleExists) {
+ console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
+ continue;
}
- console.log("layanan success ...")
+ await safeSeedUnique("user", { id: u.id }, {
+ username: u.nama,
+ nomor: u.nomor,
+ roleId: u.roleId,
+ isActive: u.isActive,
+ });
+ }
+ console.log("✅ Users seeded");
- for (const p of potensi) {
- await prisma.potensi.upsert({
- where: {
- name: p.name
- },
- update: {
- name: p.name
- },
- create: {
- name: p.name
- }
- })
+ // =========== FILE STORAGE ===========
+ console.log("🔄 Seeding file storage...");
+ for (const f of fileStorage) {
+ await prisma.fileStorage.upsert({
+ where: { id: f.id },
+ update: {
+ name: f.name,
+ realName: f.realName,
+ path: f.path,
+ mimeType: f.mimeType,
+ link: f.link,
+ category: f.category,
+ },
+ create: {
+ id: f.id,
+ name: f.name,
+ realName: f.realName,
+ path: f.path,
+ mimeType: f.mimeType,
+ link: f.link,
+ category: f.category,
+ },
+ });
+ }
+ console.log("✅ File storage seeded");
+ // =========== LANDING PAGE ===========
+ // =========== SUBMENU PROFILE ===========
+ // =========== PROFILE PEJABAT DESA ===========
+ for (const p of profilePejabatDesa) {
+ await prisma.pejabatDesa.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ position: p.position,
+ imageId: p.imageId,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ position: p.position,
+ imageId: p.imageId,
+ },
+ });
+ }
+ console.log(
+ "✅ profilePejabatDesa seeded without imageId (editable later via UI)"
+ );
+
+ // =========== PROGRAM INOVASI ===========
+ for (const p of programInovasi) {
+ let imageId: string | null = null;
+
+ if (p.imageId) {
+ const imageExists = await prisma.fileStorage.findUnique({
+ where: { id: p.imageId },
+ });
+
+ if (imageExists) {
+ imageId = p.imageId;
+ } else {
+ console.warn(
+ `⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}`
+ );
+ }
+ }
+ await prisma.programInovasi.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ description: p.description,
+ link: p.link,
+ imageId: p.imageId,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ description: p.description,
+ link: p.link,
+ imageId: p.imageId,
+ },
+ });
+ }
+ console.log("program inovasi success ...");
+
+ // =========== MEDIA SOSIAL ===========
+ for (const p of mediaSosial) {
+ await prisma.mediaSosial.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ iconUrl: p.iconUrl,
+ imageId: p.imageId,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ iconUrl: p.iconUrl,
+ imageId: p.imageId,
+ },
+ });
+ }
+ console.log("media sosial success ...");
+
+ // =========== SUBMENU DESA ANTI KORUPSI ===========
+ // =========== KATEGORI DESA ANTI KORUPSI ===========
+ for (const k of kategoriDesaAntiKorupsi) {
+ await prisma.kategoriDesaAntiKorupsi.upsert({
+ where: { id: k.id },
+ update: {
+ name: k.name,
+ },
+ create: {
+ id: k.id,
+ name: k.name,
+ },
+ });
+ }
+ console.log("kategori desa anti korupsi success ...");
+
+ // =========== DESA ANTI KORUPSI ===========
+ for (const p of desaAntiKorupsi) {
+ await prisma.desaAntiKorupsi.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ deskripsi: p.deskripsi,
+ kategoriId: p.kategoriId,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ deskripsi: p.deskripsi,
+ kategoriId: p.kategoriId,
+ },
+ });
+ }
+ console.log("desa anti korupsi success ...");
+
+ // =========== KATEGORI DESA ANTI KORUPSI ===========
+ for (const p of kategoriDesaAntiKorupsi) {
+ await prisma.kategoriDesaAntiKorupsi.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ },
+ });
+ }
+ console.log("desa anti korupsi success ...");
+
+ // =========== KATEGORI PRESTASI DESA===========
+ for (const c of kategoriPrestasiDesa) {
+ await prisma.kategoriPrestasiDesa.upsert({
+ where: { id: c.id },
+ update: {
+ name: c.name,
+ },
+ create: {
+ id: c.id,
+ name: c.name,
+ },
+ });
+ }
+ console.log("kategori prestasi desa success ...");
+
+ // =========== PRESTASI DESA===========
+ for (const p of prestasiDesa) {
+ await prisma.prestasiDesa.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ deskripsi: p.deskripsi,
+ kategoriId: p.kategoriId,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ deskripsi: p.deskripsi,
+ kategoriId: p.kategoriId,
+ },
+ });
+ }
+ console.log("prestasi desa success ...");
+
+ // =========== PENGHARGAAN ===========
+ for (const p of penghargaan) {
+ await prisma.penghargaan.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ juara: p.juara,
+ deskripsi: p.deskripsi,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ juara: p.juara,
+ deskripsi: p.deskripsi,
+ },
+ });
+ }
+ console.log("penghargaan success ...");
+
+ // =========== LAYANAN DESA ===========
+ for (const p of pelayananSuratKeterangan) {
+ await prisma.pelayananSuratKeterangan.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ deskripsi: p.deskripsi,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ deskripsi: p.deskripsi,
+ },
+ });
+ }
+ console.log("pelayanan surat keterangan success ...");
+
+ for (const p of pelayananTelunjukSaktiDesa) {
+ await prisma.pelayananTelunjukSaktiDesa.upsert({
+ where: { id: p.id },
+ update: {
+ name: p.name,
+ deskripsi: p.deskripsi,
+ link: p.link,
+ },
+ create: {
+ id: p.id,
+ name: p.name,
+ deskripsi: p.deskripsi,
+ link: p.link,
+ },
+ });
+ }
+ console.log("pelayanan surat keterangan success ...");
+
+ // =========== SDGSDesa ===========
+ for (const l of sdgsDesa) {
+ await prisma.sdgsDesa.upsert({
+ where: { id: l.id },
+ update: {
+ name: l.name,
+ jumlah: l.jumlah,
+ },
+ create: {
+ id: l.id,
+ name: l.name,
+ jumlah: l.jumlah,
+ },
+ });
+ }
+
+ console.log("sdgs desa success ...");
+
+ // =========== APBDes ===========
+ for (const l of apbdes) {
+ await prisma.aPBDes.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ name: l.name,
+ jumlah: l.jumlah,
+ },
+ create: {
+ name: l.name,
+ jumlah: l.jumlah,
+ },
+ });
+ }
+
+ console.log("sdgs desa success ...");
+
+ // =========== MENU DESA ===========
+ // =========== SUBMENU PROFILE ===========
+ // =========== SEJARAH DESA ===========
+ for (const l of sejarahDesa) {
+ await prisma.sejarahDesa.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ judul: l.judul,
+ deskripsi: l.deskripsi,
+ },
+ create: {
+ id: l.id,
+ judul: l.judul,
+ deskripsi: l.deskripsi,
+ },
+ });
+ }
+
+ console.log("sejarah desa success ...");
+
+ // =========== MASKOT DESA ===========
+ for (const l of maskotDesa) {
+ await prisma.maskotDesa.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ judul: l.judul,
+ deskripsi: l.deskripsi,
+ },
+ create: {
+ id: l.id,
+ judul: l.judul,
+ deskripsi: l.deskripsi,
+ },
+ });
+ }
+
+ console.log("maskot desa success ...");
+
+ // =========== LAMBANG DESA ===========
+ for (const l of lambangDesa) {
+ await prisma.lambangDesa.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ judul: l.judul,
+ deskripsi: l.deskripsi,
+ },
+ create: {
+ id: l.id,
+ judul: l.judul,
+ deskripsi: l.deskripsi,
+ },
+ });
+ }
+
+ console.log("lambang desa success ...");
+
+ // =========== PROFIL PERBEKEL ===========
+ for (const c of profilPerbekel) {
+ await prisma.profilPerbekel.upsert({
+ where: { id: c.id },
+ update: {
+ biodata: c.biodata,
+ pengalaman: c.pengalaman,
+ pengalamanOrganisasi: c.pengalamanOrganisasi,
+ programUnggulan: c.programUnggulan,
+ // imageId tidak di-update
+ },
+ create: {
+ id: c.id,
+ biodata: c.biodata,
+ pengalaman: c.pengalaman,
+ pengalamanOrganisasi: c.pengalamanOrganisasi,
+ programUnggulan: c.programUnggulan,
+ // imageId tidak di-create
+ },
+ });
+ }
+ console.log(
+ "✅ profilePerbekel seeded without imageId (editable later via UI)"
+ );
+
+ // =========== VISI MISI DESA ===========
+ for (const l of visiMisiDesa) {
+ await prisma.visiMisiDesa.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ visi: l.visi,
+ misi: l.misi,
+ },
+ create: {
+ id: l.id,
+ visi: l.visi,
+ misi: l.misi,
+ },
+ });
+ }
+
+ console.log("visi misi desa success ...");
+
+ // =========== MENU PPID ===========
+ // =========== SUBMENU PROFILE PPID ===========
+ for (const c of profilePPID) {
+ await prisma.profilePPID.upsert({
+ where: { id: c.id },
+ update: {
+ name: c.name,
+ biodata: c.biodata,
+ riwayat: c.riwayat,
+ pengalaman: c.pengalaman,
+ unggulan: c.unggulan,
+ // imageId tidak di-update
+ },
+ create: {
+ id: c.id,
+ name: c.name,
+ biodata: c.biodata,
+ riwayat: c.riwayat,
+ pengalaman: c.pengalaman,
+ unggulan: c.unggulan,
+ // imageId tidak di-create
+ },
+ });
+ }
+ console.log("✅ profilePPID seeded without imageId (editable later via UI)");
+
+ // =========== SUBMENU STRUKTUR PPID ===========
+ // =========== POSISI ORGANISASI PPID ===========
+
+ const flattenedPosisi = posisiOrganisasiPPID.flat();
+
+ // ✅ Urutkan berdasarkan hierarki
+ const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
+
+ for (const p of sortedPosisi) {
+ console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
+
+ if (p.parentId) {
+ const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
+ if (!parentExists) {
+ console.warn(
+ `⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
+ );
+ continue;
+ }
}
- console.log("potensi success ...")
-})().then(() => prisma.$disconnect()).catch((e) => {
- console.error(e)
- prisma.$disconnect()
+ await prisma.posisiOrganisasiPPID.upsert({
+ where: { id: p.id },
+ update: p,
+ create: p,
+ });
+ }
+ console.log("posisi organisasi berhasil");
+
+ // =========== PEGAWAI PPID ===========
+ const flattenedPegawai = pegawaiPPID.flat();
+ for (const p of flattenedPegawai) {
+ await prisma.pegawaiPPID.upsert({
+ where: { id: p.id },
+ update: p,
+ create: p,
+ });
+ }
+ console.log("pegawai berhasil");
+
+ // =========== SUBMENU VISI MISI PPID ===========
+
+ for (const v of visiMisiPPID) {
+ await prisma.visiMisiPPID.upsert({
+ where: {
+ id: v.id,
+ },
+ update: {
+ misi: v.misi,
+ visi: v.visi,
+ },
+ create: {
+ id: v.id,
+ misi: v.misi,
+ visi: v.visi,
+ },
+ });
+ }
+ console.log("visi misi PPID success ...");
+
+ // =========== SUBMENU DASAR HUKUM PPID ===========
+ for (const v of dasarHukumPPID) {
+ await prisma.dasarHukumPPID.upsert({
+ where: {
+ id: v.id,
+ },
+ update: {
+ judul: v.judul,
+ content: v.content,
+ },
+ create: {
+ id: v.id,
+ judul: v.judul,
+ content: v.content,
+ },
+ });
+ }
+ console.log("dasar hukum PPID success ...");
+
+ // =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
+ for (const v of daftarInformasiPublik) {
+ // Convert string date to Date object
+ const tanggal = new Date(v.tanggal);
+
+ await prisma.daftarInformasiPublik.upsert({
+ where: {
+ id: v.id,
+ },
+ update: {
+ jenisInformasi: v.jenisInformasi,
+ deskripsi: v.deskripsi,
+ tanggal: tanggal,
+ },
+ create: {
+ id: v.id,
+ jenisInformasi: v.jenisInformasi,
+ deskripsi: v.deskripsi,
+ tanggal: tanggal,
+ },
+ });
+ }
+ console.log("daftar informasi publik PPID success ...");
+
+ for (const l of pelayananPerizinanBerusaha) {
+ await prisma.pelayananPerizinanBerusaha.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ name: l.name,
+ deskripsi: l.deskripsi,
+ link: l.link,
+ },
+ create: {
+ id: l.id,
+ name: l.name,
+ deskripsi: l.deskripsi,
+ link: l.link,
+ },
+ });
+ }
+
+ console.log("pelayanan perizinan berusaha success ...");
+
+ for (const l of pelayananPendudukNonPermanen) {
+ await prisma.pelayananPendudukNonPermanen.upsert({
+ where: {
+ id: l.id,
+ },
+ update: {
+ name: l.name,
+ deskripsi: l.deskripsi,
+ },
+ create: {
+ id: l.id,
+ name: l.name,
+ deskripsi: l.deskripsi,
+ },
+ });
+ }
+ console.log("pelayanan penduduk non permanen success ...");
+
+ for (const p of potensi) {
+ await prisma.potensi.upsert({
+ where: {
+ name: p.name,
+ },
+ update: {
+ name: p.name,
+ },
+ create: {
+ name: p.name,
+ },
+ });
+ }
+
+ console.log("potensi success ...");
+
+ for (const k of kategoriBerita) {
+ await prisma.kategoriBerita.upsert({
+ where: {
+ name: k.name,
+ },
+ update: {
+ name: k.name,
+ },
+ create: {
+ name: k.name,
+ },
+ });
+ }
+
+ console.log("kategori berita success ...");
+
+ for (const c of categoryPengumuman) {
+ await prisma.categoryPengumuman.upsert({
+ where: {
+ name: c.name,
+ },
+ update: {
+ name: c.name,
+ },
+ create: {
+ name: c.name,
+ },
+ });
+ }
+
+ console.log("category pengumuman success ...");
+
+ for (const j of jenisInformasiDiminta) {
+ await prisma.jenisInformasiDiminta.upsert({
+ where: {
+ name: j.name,
+ },
+ update: {
+ name: j.name,
+ },
+ create: {
+ name: j.name,
+ },
+ });
+ }
+ console.log("jenis informasi diminta success ...");
+
+ for (const c of caraMemperolehInformasi) {
+ await prisma.caraMemperolehInformasi.upsert({
+ where: {
+ name: c.name,
+ },
+ update: {
+ name: c.name,
+ },
+ create: {
+ name: c.name,
+ },
+ });
+ }
+ console.log("cara memperoleh informasi success ...");
+
+ for (const c of caraMemperolehSalinanInformasi) {
+ await prisma.caraMemperolehSalinanInformasi.upsert({
+ where: {
+ name: c.name,
+ },
+ update: {
+ name: c.name,
+ },
+ create: {
+ name: c.name,
+ },
+ });
+ }
+ console.log("cara memperoleh salinan informasi success ...");
+
+ for (const j of jenisKelamin) {
+ await prisma.jenisKelaminResponden.upsert({
+ where: {
+ id: j.id,
+ },
+ update: {
+ name: j.name,
+ },
+ create: {
+ id: j.id,
+ name: j.name,
+ },
+ });
+ }
+ console.log("jenis kelamin responden success ...");
+
+ for (const r of pilihanRatingResponden) {
+ await prisma.pilihanRatingResponden.upsert({
+ where: {
+ id: r.id,
+ },
+ update: {
+ name: r.name,
+ },
+ create: {
+ id: r.id,
+ name: r.name,
+ },
+ });
+ }
+ console.log("pilihan rating responden success ...");
+
+ for (const u of umurResponden) {
+ await prisma.umurResponden.upsert({
+ where: {
+ id: u.id,
+ },
+ update: {
+ name: u.name,
+ },
+ create: {
+ id: u.id,
+ name: u.name,
+ },
+ });
+ }
+ console.log("umur responden success ...");
+
+ for (const k of kategoriProduk) {
+ await prisma.kategoriProduk.upsert({
+ where: {
+ id: k.id,
+ },
+ update: {
+ nama: k.nama,
+ },
+ create: {
+ id: k.id,
+ nama: k.nama,
+ },
+ });
+ }
+ console.log("kategori produk success ...");
+
+ const flattenedPosisiBumdes = posisiOrganisasi.flat();
+
+ // ✅ Urutkan berdasarkan hierarki
+ const sortedPosisiBumdes = flattenedPosisiBumdes.sort((a, b) => a.hierarki - b.hierarki);
+
+ for (const p of sortedPosisiBumdes) {
+ console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
+
+ if (p.parentId) {
+ const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
+ if (!parentExists) {
+ console.warn(
+ `⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
+ );
+ continue;
+ }
+ }
+
+ await prisma.posisiOrganisasiBumDes.upsert({
+ where: { id: p.id },
+ update: p,
+ create: p,
+ });
+ }
+ console.log("posisi organisasi berhasil");
+
+ for (const p of pegawai) {
+ await prisma.pegawaiBumDes.upsert({
+ where: {
+ id: p.id,
+ },
+ update: {
+ namaLengkap: p.namaLengkap,
+ gelarAkademik: p.gelarAkademik,
+ tanggalMasuk: new Date(p.tanggalMasuk),
+ email: p.email,
+ telepon: p.telepon,
+ alamat: p.alamat,
+ posisiId: p.posisiId,
+ isActive: p.isActive,
+ },
+ create: {
+ id: p.id,
+ namaLengkap: p.namaLengkap,
+ gelarAkademik: p.gelarAkademik,
+ tanggalMasuk: new Date(p.tanggalMasuk),
+ email: p.email,
+ telepon: p.telepon,
+ alamat: p.alamat,
+ posisiId: p.posisiId,
+ isActive: p.isActive,
+ },
+ });
+ }
+ console.log("pegawai success ...");
+
+ for (const d of detailDataPengangguran) {
+ await prisma.detailDataPengangguran.upsert({
+ where: {
+ month_year: { month: d.month, year: d.year },
+ },
+ update: {
+ totalUnemployment: d.totalUnemployment,
+ educatedUnemployment: d.educatedUnemployment,
+ uneducatedUnemployment: d.uneducatedUnemployment,
+ percentageChange: d.percentageChange,
+ },
+ create: {
+ month: d.month,
+ year: d.year,
+ totalUnemployment: d.totalUnemployment,
+ educatedUnemployment: d.educatedUnemployment,
+ uneducatedUnemployment: d.uneducatedUnemployment,
+ percentageChange: d.percentageChange,
+ },
+ });
+ }
+ console.log("📊 detailDataPengangguran success ...");
+
+ // =========== KATEGORI GOTONG ROYONG ===========
+ // Add IDs to the kategoriKegiatan data
+ const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
+ ...k,
+ id: `kategori-${index + 1}`
+ }));
+
+ for (const k of kategoriKegiatan) {
+ await prisma.kategoriKegiatan.upsert({
+ where: {
+ id: k.id,
+ },
+ update: {
+ nama: k.nama,
+ },
+ create: {
+ id: k.id,
+ nama: k.nama,
+ },
+ });
+ }
+
+ console.log("kategori kegiatan success ...");
+
+ for (const e of tujuanEdukasiLingkungan) {
+ await prisma.tujuanEdukasiLingkungan.upsert({
+ where: {
+ id: e.id,
+ },
+ update: {
+ judul: e.judul,
+ deskripsi: e.deskripsi,
+ },
+ create: {
+ id: e.id,
+ judul: e.judul,
+ deskripsi: e.deskripsi,
+ },
+ });
+ }
+
+ console.log("tujuan edukasi lingkungan success ...");
+
+ for (const m of materiEdukasiLingkungan) {
+ await prisma.materiEdukasiLingkungan.upsert({
+ where: {
+ id: m.id,
+ },
+ update: {
+ judul: m.judul,
+ deskripsi: m.deskripsi,
+ },
+ create: {
+ id: m.id,
+ judul: m.judul,
+ deskripsi: m.deskripsi,
+ },
+ });
+ }
+
+ console.log("materi edukasi lingkungan success ...");
+
+ for (const c of contohEdukasiLingkungan) {
+ await prisma.contohEdukasiLingkungan.upsert({
+ where: {
+ id: c.id,
+ },
+ update: {
+ judul: c.judul,
+ deskripsi: c.deskripsi,
+ },
+ create: {
+ id: c.id,
+ judul: c.judul,
+ deskripsi: c.deskripsi,
+ },
+ });
+ }
+
+ console.log("contoh edukasi lingkungan success ...");
+
+ for (const f of filosofiTriHita) {
+ await prisma.filosofiTriHita.upsert({
+ where: {
+ id: f.id,
+ },
+ update: {
+ judul: f.judul,
+ deskripsi: f.deskripsi,
+ },
+ create: {
+ id: f.id,
+ judul: f.judul,
+ deskripsi: f.deskripsi,
+ },
+ });
+ }
+
+ console.log("filosofi tri hita success ...");
+
+ for (const b of bentukKonservasiBerdasarkanAdat) {
+ await prisma.bentukKonservasiBerdasarkanAdat.upsert({
+ where: {
+ id: b.id,
+ },
+ update: {
+ judul: b.judul,
+ deskripsi: b.deskripsi,
+ },
+ create: {
+ id: b.id,
+ judul: b.judul,
+ deskripsi: b.deskripsi,
+ },
+ });
+ }
+
+ console.log("bentuk konservasi berdasarkan adat success ...");
+
+ for (const n of nilaiKonservasiAdat) {
+ await prisma.nilaiKonservasiAdat.upsert({
+ where: {
+ id: n.id,
+ },
+ update: {
+ judul: n.judul,
+ deskripsi: n.deskripsi,
+ },
+ create: {
+ id: n.id,
+ judul: n.judul,
+ deskripsi: n.deskripsi,
+ },
+ });
+ }
+
+ console.log("nilai konservasi adat success ...");
+
+ for (const t of tujuanProgram) {
+ await prisma.tujuanProgram.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log("✅ tujuan program seeded (editable later via UI)");
+
+ for (const t of programUnggulan) {
+ await prisma.programUnggulan.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log("✅ program unggulan seeded (editable later via UI)");
+
+ for (const t of tujuanBimbinganBelajarDesa) {
+ await prisma.tujuanBimbinganBelajarDesa.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log(
+ "✅ tujuan bimbingan belajar desa seeded (editable later via UI)"
+ );
+
+ for (const t of lokasiJadwalBimbinganBelajarDesa) {
+ await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log(
+ "✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)"
+ );
+
+ for (const t of fasilitasBimbinganBelajarDesa) {
+ await prisma.fasilitasBimbinganBelajarDesa.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log(
+ "✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
+ );
+
+ for (const t of tujuanProgram2) {
+ await prisma.tujuanPendidikanNonFormal.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log(
+ "✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
+ );
+
+ for (const t of tempatKegiatan) {
+ await prisma.tempatKegiatan.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log(
+ "✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
+ );
+
+ for (const t of jenisProgramYangDiselenggarakan) {
+ await prisma.jenisProgramYangDiselenggarakan.upsert({
+ where: { id: t.id },
+ update: {
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ create: {
+ id: t.id,
+ judul: t.judul,
+ deskripsi: t.deskripsi,
+ },
+ });
+ }
+ console.log(
+ "✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
+ );
+
+ for (const j of jenjangPendidikan) {
+ await prisma.jenjangPendidikan.upsert({
+ where: {
+ id: j.id || undefined,
+ },
+ update: {
+ nama: j.nama,
+ },
+ create: {
+ nama: j.nama,
+ },
+ });
+ }
+
+ console.log("✅ Jenjang Pendidikan seeded successfully");
+
+ // seed assets
+ await seedAssets();
+
+})()
+ .then(() => prisma.$disconnect())
+ .catch((e) => {
+ console.error(e);
+ prisma.$disconnect();
+ });
+
+process.on("exit", () => {
+ prisma.$disconnect();
});
-process.on('exit', () => {
- prisma.$disconnect()
-})
-
-process.on('SIGINT', () => {
- prisma.$disconnect()
- process.exit(0)
-})
\ No newline at end of file
+process.on("SIGINT", () => {
+ prisma.$disconnect();
+ process.exit(0);
+});
diff --git a/prisma/seed_assets.ts b/prisma/seed_assets.ts
new file mode 100644
index 00000000..f92c0d36
--- /dev/null
+++ b/prisma/seed_assets.ts
@@ -0,0 +1,118 @@
+// prisma/seedAssets.ts
+import fs from "fs/promises";
+import path from "path";
+import sharp from "sharp";
+import fetch from "node-fetch";
+import AdmZip from "adm-zip";
+import prisma from "@/lib/prisma";
+
+const UPLOADS_DIR =
+ process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads");
+
+// --- Helper: deteksi kategori file ---
+function detectCategory(filename: string): "image" | "document" | "other" {
+ const ext = path.extname(filename).toLowerCase();
+ if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image";
+ if ([".pdf", ".doc", ".docx"].includes(ext)) return "document";
+ return "other";
+}
+
+// --- Helper: recursive walk dir ---
+async function walkDir(dir: string, fileList: string[] = []): Promise {
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ if (entry.name === "__MACOSX") continue; // skip folder sampah
+ await walkDir(fullPath, fileList);
+ } else {
+ if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah
+ fileList.push(fullPath);
+ }
+ }
+
+ return fileList;
+}
+
+export default async function seedAssets() {
+ console.log("🚀 Seeding assets...");
+
+ // 1. Download zip
+ const url =
+ "https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1";
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`);
+ const buffer = Buffer.from(await res.arrayBuffer());
+
+ // 2. Extract zip ke folder tmp
+ const extractDir = path.join(process.cwd(), "tmp_assets");
+ await fs.rm(extractDir, { recursive: true, force: true });
+ await fs.mkdir(extractDir, { recursive: true });
+
+ const zip = new AdmZip(buffer);
+ zip.extractAllTo(extractDir, true);
+
+ // 3. Cari semua file valid (recursive)
+ const files = await walkDir(extractDir);
+
+ // 4. Loop tiap file & simpan
+ for (const filePath of files) {
+ const entryName = path.basename(filePath);
+ const category = detectCategory(entryName);
+
+ let finalName = entryName;
+ let mimeType = "application/octet-stream";
+ let targetPath = "";
+
+ if (category === "image") {
+ const fileBaseName = path.parse(entryName).name;
+ finalName = `${fileBaseName}.webp`;
+ targetPath = path.join(UPLOADS_DIR, "images", finalName);
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await sharp(filePath).webp({ quality: 80 }).toFile(targetPath);
+ mimeType = "image/webp";
+ } else if (category === "document") {
+ targetPath = path.join(UPLOADS_DIR, "documents", entryName);
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.copyFile(filePath, targetPath);
+ mimeType = "application/pdf";
+ } else {
+ targetPath = path.join(UPLOADS_DIR, "other", entryName);
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.copyFile(filePath, targetPath);
+ }
+
+ // 5. Simpan ke DB
+ await prisma.fileStorage.create({
+ data: {
+ name: finalName,
+ realName: entryName,
+ path: targetPath,
+ mimeType,
+ link: `/uploads/${category}/${finalName}`,
+ category,
+ },
+ });
+
+ console.log(`📂 saved: ${category}/${finalName}`);
+ }
+
+ // 6. Cleanup
+ await fs.rm(extractDir, { recursive: true, force: true });
+
+ console.log("✅ Selesai seed assets!");
+}
+
+// --- Auto run kalau dipanggil langsung ---
+if (import.meta.main) {
+ seedAssets()
+ .catch((err) => {
+ console.error("❌ Error seeding assets:", err);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
+}
diff --git a/public/Share.png b/public/Share.png
new file mode 100644
index 00000000..82ffa19d
Binary files /dev/null and b/public/Share.png differ
diff --git a/public/assets/images/layanan/kelahiran.jpeg b/public/assets/images/layanan/kelahiran.jpeg
new file mode 100644
index 00000000..b4869119
Binary files /dev/null and b/public/assets/images/layanan/kelahiran.jpeg differ
diff --git a/public/assets/images/layanan/layanan1.jpeg b/public/assets/images/layanan/layanan1.jpeg
new file mode 100644
index 00000000..ee998c4e
Binary files /dev/null and b/public/assets/images/layanan/layanan1.jpeg differ
diff --git a/public/assets/images/layanan/test.png b/public/assets/images/layanan/test.png
new file mode 100644
index 00000000..c36b4a65
Binary files /dev/null and b/public/assets/images/layanan/test.png differ
diff --git a/public/assets/images/layanan/test2.jpeg b/public/assets/images/layanan/test2.jpeg
new file mode 100644
index 00000000..58d15344
Binary files /dev/null and b/public/assets/images/layanan/test2.jpeg differ
diff --git a/public/assets/images/layanan/test3.jpeg b/public/assets/images/layanan/test3.jpeg
new file mode 100644
index 00000000..2eede0de
Binary files /dev/null and b/public/assets/images/layanan/test3.jpeg differ
diff --git a/public/assets/images/perbekel.png b/public/assets/images/perbekel.png
deleted file mode 100644
index ed1cbd10..00000000
Binary files a/public/assets/images/perbekel.png and /dev/null differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747623028993_berita-pemerintahan.jpg b/public/assets/images/ppid/profile-ppid/1_1747623028993_berita-pemerintahan.jpg
new file mode 100644
index 00000000..6b0e5be3
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747623028993_berita-pemerintahan.jpg differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747820179116_berita-pemerintahan.jpg b/public/assets/images/ppid/profile-ppid/1_1747820179116_berita-pemerintahan.jpg
new file mode 100644
index 00000000..6b0e5be3
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747820179116_berita-pemerintahan.jpg differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747835389645_capybara.png b/public/assets/images/ppid/profile-ppid/1_1747835389645_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747835389645_capybara.png differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747836129969_bgDesktop.jpg b/public/assets/images/ppid/profile-ppid/1_1747836129969_bgDesktop.jpg
new file mode 100644
index 00000000..9a7f9d24
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747836129969_bgDesktop.jpg differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747836263333_capybara.png b/public/assets/images/ppid/profile-ppid/1_1747836263333_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747836263333_capybara.png differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747836558027_capybara.png b/public/assets/images/ppid/profile-ppid/1_1747836558027_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747836558027_capybara.png differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747836664305_capybara.png b/public/assets/images/ppid/profile-ppid/1_1747836664305_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747836664305_capybara.png differ
diff --git a/public/assets/images/ppid/profile-ppid/1_1747894445049_capybara.png b/public/assets/images/ppid/profile-ppid/1_1747894445049_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/assets/images/ppid/profile-ppid/1_1747894445049_capybara.png differ
diff --git a/public/assets/images/pudak-icon.png b/public/assets/images/pudak-icon.png
deleted file mode 100644
index 50051447..00000000
Binary files a/public/assets/images/pudak-icon.png and /dev/null differ
diff --git a/public/assets/images/sosmed/telephone-call.png b/public/assets/images/sosmed/telephone-call.png
new file mode 100644
index 00000000..effdf779
Binary files /dev/null and b/public/assets/images/sosmed/telephone-call.png differ
diff --git a/public/bagikanPostingan.png b/public/bagikanPostingan.png
new file mode 100644
index 00000000..72826cbf
Binary files /dev/null and b/public/bagikanPostingan.png differ
diff --git a/public/beasiswa-siswa.png b/public/beasiswa-siswa.png
new file mode 100644
index 00000000..ed40216d
Binary files /dev/null and b/public/beasiswa-siswa.png differ
diff --git a/public/bungapudak.png b/public/bungapudak.png
new file mode 100644
index 00000000..e35e2875
Binary files /dev/null and b/public/bungapudak.png differ
diff --git a/public/chatbot-removebg-preview.png b/public/chatbot-removebg-preview.png
new file mode 100644
index 00000000..47d1ad63
Binary files /dev/null and b/public/chatbot-removebg-preview.png differ
diff --git a/uploads/image/darmasaba-icon.png b/public/darmasaba-icon.png
similarity index 100%
rename from uploads/image/darmasaba-icon.png
rename to public/darmasaba-icon.png
diff --git a/public/klimakstari.png b/public/klimakstari.png
new file mode 100644
index 00000000..e668ffdf
Binary files /dev/null and b/public/klimakstari.png differ
diff --git a/public/pa-desa.png b/public/pa-desa.png
new file mode 100644
index 00000000..dd674822
Binary files /dev/null and b/public/pa-desa.png differ
diff --git a/public/perbekel.png b/public/perbekel.png
new file mode 100644
index 00000000..a940365b
Binary files /dev/null and b/public/perbekel.png differ
diff --git a/public/pohonpudak.png b/public/pohonpudak.png
new file mode 100644
index 00000000..0782bbc7
Binary files /dev/null and b/public/pohonpudak.png differ
diff --git a/public/pudak-icon.png b/public/pudak-icon.png
new file mode 100644
index 00000000..c54c6062
Binary files /dev/null and b/public/pudak-icon.png differ
diff --git a/public/sematkan.png b/public/sematkan.png
new file mode 100644
index 00000000..9ce93c24
Binary files /dev/null and b/public/sematkan.png differ
diff --git a/public/struktur_ppid.png b/public/struktur_ppid.png
new file mode 100644
index 00000000..5124ac1e
Binary files /dev/null and b/public/struktur_ppid.png differ
diff --git a/public/tarisekar.png b/public/tarisekar.png
new file mode 100644
index 00000000..77dcc5d3
Binary files /dev/null and b/public/tarisekar.png differ
diff --git a/public/uploads/profile-ppid/1_1747885424609_budaya-1.jpg b/public/uploads/profile-ppid/1_1747885424609_budaya-1.jpg
new file mode 100644
index 00000000..9cd84d04
Binary files /dev/null and b/public/uploads/profile-ppid/1_1747885424609_budaya-1.jpg differ
diff --git a/public/uploads/seeded-images/ppid/profile-ppid/1_1747836703445_capybara.png b/public/uploads/seeded-images/ppid/profile-ppid/1_1747836703445_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/uploads/seeded-images/ppid/profile-ppid/1_1747836703445_capybara.png differ
diff --git a/public/uploads/seeded-images/ppid/profile-ppid/1_1747836821689_capybara.png b/public/uploads/seeded-images/ppid/profile-ppid/1_1747836821689_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/uploads/seeded-images/ppid/profile-ppid/1_1747836821689_capybara.png differ
diff --git a/public/uploads/seeded-images/ppid/profile-ppid/1_1747839042145_capybara.png b/public/uploads/seeded-images/ppid/profile-ppid/1_1747839042145_capybara.png
new file mode 100644
index 00000000..16a01d4c
Binary files /dev/null and b/public/uploads/seeded-images/ppid/profile-ppid/1_1747839042145_capybara.png differ
diff --git a/public/video.png b/public/video.png
new file mode 100644
index 00000000..3c144bc0
Binary files /dev/null and b/public/video.png differ
diff --git a/src/app/_com/SpashScreen.tsx b/src/app/_com/SpashScreen.tsx
index 69d22304..a2f35a43 100644
--- a/src/app/_com/SpashScreen.tsx
+++ b/src/app/_com/SpashScreen.tsx
@@ -23,6 +23,7 @@ export default function SpashScreen() {
{
+ delete (L.Icon.Default.prototype as any)._getIconUrl;
+ L.Icon.Default.mergeOptions({
+ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+ iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+ });
+ }, []);
+
+ return (
+
+
+
+ {markers.map((marker, index) => (
+
+ {marker.popup}
+
+ ))}
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/_com/createEditor.tsx b/src/app/admin/(dashboard)/_com/createEditor.tsx
new file mode 100644
index 00000000..7878e59a
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/createEditor.tsx
@@ -0,0 +1,85 @@
+// TestEditor.tsx
+import { RichTextEditor, Link } from '@mantine/tiptap';
+import { useEditor } from '@tiptap/react';
+import Highlight from '@tiptap/extension-highlight';
+import StarterKit from '@tiptap/starter-kit';
+import Underline from '@tiptap/extension-underline';
+import TextAlign from '@tiptap/extension-text-align';
+import Superscript from '@tiptap/extension-superscript';
+import SubScript from '@tiptap/extension-subscript';
+
+type CreateEditorProps = {
+ value: string;
+ onChange: (content: string) => void;
+};
+
+export default function CreateEditor({ value, onChange }: CreateEditorProps) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ content: value,
+ onUpdate: () => {
+ if (editor) {
+ onChange(editor.getHTML());
+ }
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_com/editEditor.tsx b/src/app/admin/(dashboard)/_com/editEditor.tsx
new file mode 100644
index 00000000..66317cc2
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/editEditor.tsx
@@ -0,0 +1,102 @@
+'use client'
+import { RichTextEditor, Link } from '@mantine/tiptap';
+import { useEditor } from '@tiptap/react';
+import Highlight from '@tiptap/extension-highlight';
+import StarterKit from '@tiptap/starter-kit';
+import Underline from '@tiptap/extension-underline';
+import TextAlign from '@tiptap/extension-text-align';
+import Superscript from '@tiptap/extension-superscript';
+import SubScript from '@tiptap/extension-subscript';
+import { useEffect } from 'react';
+
+type EditEditorProps = {
+ value: string;
+ onChange: (content: string) => void;
+};
+
+export default function EditEditor({ value, onChange }: EditEditorProps) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ content: value,
+ // Hapus `immediatelyRender` dan `onMount`
+ });
+
+ // Sinkronisasi konten saat `value` berubah
+ useEffect(() => {
+ if (editor && value !== editor.getHTML()) {
+ editor.commands.setContent(value);
+ }
+ }, [value, editor]);
+
+ // Sinkronisasi konten ke parent saat diubah
+ useEffect(() => {
+ if (!editor) return;
+
+ const updateHandler = () => onChange(editor.getHTML());
+ editor.on('update', updateHandler);
+
+ return () => {
+ editor.off('update', updateHandler);
+ };
+ }, [editor, onChange]);
+
+ return (
+
+
+ {/* Toolbar seperti sebelumnya */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_com/header.tsx b/src/app/admin/(dashboard)/_com/header.tsx
new file mode 100644
index 00000000..39735f4d
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/header.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core';
+import { IconSearch } from '@tabler/icons-react';
+import colors from '@/con/colors';
+
+type HeaderSearchProps = {
+ title: string;
+ placeholder?: string;
+ searchIcon?: React.ReactNode;
+ value?: string;
+ onChange?: (event: React.ChangeEvent) => void;
+};
+
+const HeaderSearch = ({
+ title = "",
+ placeholder = "pencarian",
+ searchIcon = ,
+ value,
+ onChange,
+}: HeaderSearchProps) => {
+ return (
+
+
+ {title}
+
+
+
+
+
+
+
+ );
+};
+
+export default HeaderSearch;
diff --git a/src/app/admin/(dashboard)/_com/iconMap.tsx b/src/app/admin/(dashboard)/_com/iconMap.tsx
new file mode 100644
index 00000000..de88a3dc
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/iconMap.tsx
@@ -0,0 +1,100 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+
+import React from 'react'
+import {
+ IconLeaf,
+ IconTrophy,
+ IconTent,
+ IconChartLine,
+ IconRecycle,
+ IconTruck,
+ IconScale,
+ IconClipboard,
+ IconTrash,
+ IconHomeEco,
+ IconChristmasTreeFilled,
+ IconTrendingUp,
+ IconShieldFilled,
+ IconHome,
+ IconTree,
+ IconDroplet,
+ IconCash,
+ IconSchool,
+ IconShoppingCart,
+ IconHospital,
+ IconAmbulance,
+ IconFiretruck,
+ IconBuilding,
+ IconAlertTriangle,
+} from '@tabler/icons-react'
+
+export type IconKey =
+ | 'ekowisata'
+ | 'kompetisi'
+ | 'wisata'
+ | 'ekonomi'
+ | 'sampah'
+ | 'truck'
+ | 'scale'
+ | 'clipboard'
+ | 'trash'
+ | 'lingkunganSehat'
+ | 'sumberOksigen'
+ | 'ekonomiBerkelanjutan'
+ | 'mencegahBencana'
+ | 'rumah'
+ | 'pohon'
+ | 'air'
+ | 'bantuan'
+ | 'pelatihan'
+ | 'subsidi'
+ | 'layananKesehatan'
+ | 'polisi'
+ | 'ambulans'
+ | 'pemadam'
+ | 'rumahSakit'
+ | 'bangunan'
+ | 'darurat'
+
+
+const iconMap: Record> = {
+ ekowisata: IconLeaf,
+ kompetisi: IconTrophy,
+ wisata: IconTent,
+ ekonomi: IconChartLine,
+ sampah: IconRecycle,
+ truck: IconTruck,
+ scale: IconScale,
+ clipboard: IconClipboard,
+ trash: IconTrash,
+ lingkunganSehat: IconHomeEco,
+ sumberOksigen: IconChristmasTreeFilled,
+ ekonomiBerkelanjutan: IconTrendingUp,
+ mencegahBencana: IconShieldFilled,
+ rumah: IconHome,
+ pohon: IconTree,
+ air: IconDroplet,
+ bantuan: IconCash,
+ pelatihan: IconSchool,
+ subsidi: IconShoppingCart,
+ layananKesehatan: IconHospital,
+ polisi: IconShieldFilled,
+ ambulans: IconAmbulance,
+ pemadam: IconFiretruck,
+ rumahSakit: IconHospital,
+ bangunan: IconBuilding,
+ darurat: IconAlertTriangle
+}
+
+type Props = {
+ name: IconKey
+ size?: number
+ color?: string
+}
+
+export const IconMapper: React.FC = ({ name, size = 24, color }) => {
+ const IconComponent = iconMap[name]
+ if (!IconComponent) return null
+ return
+}
diff --git a/src/app/admin/(dashboard)/_com/judulList.tsx b/src/app/admin/(dashboard)/_com/judulList.tsx
new file mode 100644
index 00000000..4eaa5731
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/judulList.tsx
@@ -0,0 +1,30 @@
+
+'use client'
+import colors from '@/con/colors';
+import { Grid, GridCol, Button, Text } from '@mantine/core';
+import { IconCircleDashedPlus } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import React from 'react';
+
+const JudulList = ({ title = "", href = "#" }) => {
+ const router = useRouter();
+
+ const handleNavigate = () => {
+ router.push(href);
+ };
+
+ return (
+
+
+ {title}
+
+
+
+
+
+
+
+ );
+};
+
+export default JudulList;
diff --git a/src/app/admin/(dashboard)/_com/judulListTab.tsx b/src/app/admin/(dashboard)/_com/judulListTab.tsx
new file mode 100644
index 00000000..21037671
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/judulListTab.tsx
@@ -0,0 +1,60 @@
+'use client'
+import colors from '@/con/colors';
+import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core';
+import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import React from 'react';
+
+type JudulListTabProps = {
+ title: string;
+ href: string;
+ placeholder: string;
+ searchIcon: React.ReactNode;
+ value?: string;
+ onChange?: (e: React.ChangeEvent) => void;
+}
+
+
+
+
+const JudulListTab = ({
+ title = "",
+ href = "#",
+ placeholder = "pencarian",
+ searchIcon = ,
+ value,
+ onChange
+}: JudulListTabProps) => {
+ const router = useRouter();
+
+ const handleNavigate = () => {
+ router.push(href);
+ };
+
+ return (
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default JudulListTab;
diff --git a/src/app/admin/(dashboard)/_com/leafletMapCreate.tsx b/src/app/admin/(dashboard)/_com/leafletMapCreate.tsx
new file mode 100644
index 00000000..4b4282ab
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/leafletMapCreate.tsx
@@ -0,0 +1,59 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
+import { useEffect, useState } from 'react';
+import 'leaflet/dist/leaflet.css';
+import L, { LeafletMouseEvent } from 'leaflet';
+
+type Props = {
+ defaultCenter: { lat: number; lng: number };
+ onSelect?: (pos: { lat: number; lng: number }) => void;
+ readOnly?: boolean;
+};
+
+export default function LeafletMap({ defaultCenter, onSelect, readOnly = false }: Props) {
+ const [markerPos, setMarkerPos] = useState(defaultCenter);
+
+ useEffect(() => {
+ // Aman di sini, karena ini hanya jalan di client
+ delete (L.Icon.Default.prototype as any)._getIconUrl;
+ L.Icon.Default.mergeOptions({
+ iconRetinaUrl:
+ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+ iconUrl:
+ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+ shadowUrl:
+ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+ });
+ }, []);
+
+ function LocationMarker() {
+ useMapEvents({
+ click(e: LeafletMouseEvent) {
+ if (readOnly) return;
+
+ const { lat, lng } = e.latlng;
+ setMarkerPos({ lat, lng });
+ onSelect?.({ lat, lng });
+ },
+ });
+
+ return ;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/_com/leafletMapEdit.tsx b/src/app/admin/(dashboard)/_com/leafletMapEdit.tsx
new file mode 100644
index 00000000..e28c2e8e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/leafletMapEdit.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
+import { useState, useEffect } from 'react';
+import 'leaflet/dist/leaflet.css';
+import L, { LeafletMouseEvent } from 'leaflet';
+
+type Props = {
+ initialPosition: { lat: number; lng: number };
+ onChange: (pos: { lat: number; lng: number }) => void;
+};
+
+export default function LeafletMapEdit({ initialPosition, onChange }: Props) {
+ const [markerPos, setMarkerPos] = useState(initialPosition);
+
+ // ✅ Pastikan icon config cuma jalan di client
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ delete (L.Icon.Default.prototype as any)._getIconUrl;
+ L.Icon.Default.mergeOptions({
+ iconRetinaUrl:
+ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+ iconUrl:
+ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+ shadowUrl:
+ 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+ });
+ }
+ }, []);
+
+ useEffect(() => {
+ setMarkerPos(initialPosition);
+ }, [initialPosition]);
+
+ function LocationMarker() {
+ useMapEvents({
+ click(e: LeafletMouseEvent) {
+ const { lat, lng } = e.latlng;
+ setMarkerPos({ lat, lng });
+ onChange({ lat, lng });
+ },
+ });
+
+ return ;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/_com/modalKonfirmasiHapus.tsx b/src/app/admin/(dashboard)/_com/modalKonfirmasiHapus.tsx
new file mode 100644
index 00000000..db1e64af
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/modalKonfirmasiHapus.tsx
@@ -0,0 +1,36 @@
+// components/modal/ModalKonfirmasiHapus.tsx
+import colors from "@/con/colors"
+import { Modal, Text, Button, Flex } from "@mantine/core"
+
+interface ModalKonfirmasiHapusProps {
+ opened: boolean
+ loading?: boolean
+ onClose: () => void
+ onConfirm: () => void
+ text: string
+}
+
+export function ModalKonfirmasiHapus({
+ opened,
+ loading = false,
+ onClose,
+ onConfirm,
+ text,
+}: ModalKonfirmasiHapusProps) {
+ return (
+ Konfirmasi Hapus}
+ centered
+ >
+ {text}
+
+ Batal
+
+ Yakin Hapus
+
+
+
+ )
+}
diff --git a/src/app/admin/(dashboard)/_com/selectIcon.tsx b/src/app/admin/(dashboard)/_com/selectIcon.tsx
new file mode 100644
index 00000000..88e17af8
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/selectIcon.tsx
@@ -0,0 +1,116 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import { Box, rem, Select } from '@mantine/core';
+import {
+ IconAlertTriangle,
+ IconAmbulance,
+ IconBuilding,
+ IconCash,
+ IconChartLine,
+ IconChristmasTreeFilled,
+ IconClipboardTextFilled,
+ IconDroplet,
+ IconFiretruck,
+ IconHome,
+ IconHomeEco,
+ IconHospital,
+ IconLeaf,
+ IconRecycle,
+ IconScale,
+ IconSchool,
+ IconShieldFilled,
+ IconShoppingCart,
+ IconTent,
+ IconTrashFilled,
+ IconTree,
+ IconTrendingUp,
+ IconTrophy,
+ IconTruckFilled,
+} from '@tabler/icons-react';
+import { useEffect, useState } from 'react';
+
+const iconMap = {
+ ekowisata: { label: 'Ekowisata', icon: IconLeaf },
+ kompetisi: { label: 'Kompetisi', icon: IconTrophy },
+ wisata: { label: 'Wisata', icon: IconTent },
+ ekonomi: { label: 'Ekonomi', icon: IconChartLine },
+ sampah: { label: 'Sampah', icon: IconRecycle },
+ truck: { label: 'Truck', icon: IconTruckFilled },
+ scale: { label: 'Scale', icon: IconScale },
+ clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
+ trash: { label: 'Trash', icon: IconTrashFilled },
+ lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
+ sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
+ ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
+ mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
+ rumah: { label: 'Rumah', icon: IconHome },
+ pohon: { label: 'Pohon', icon: IconTree },
+ air: { label: 'Air', icon: IconDroplet },
+ bantuan: { label: 'Bantuan', icon: IconCash },
+ pelatihan: { label: 'Pelatihan', icon: IconSchool },
+ subsidi: { label: 'Subsidi', icon: IconShoppingCart },
+ layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
+ polisi: { label: 'Polisi', icon: IconShieldFilled },
+ ambulans: { label: 'Ambulans', icon: IconAmbulance },
+ pemadam: { label: 'Pemadam', icon: IconFiretruck },
+ rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
+ bangunan: { label: 'Bangunan', icon: IconBuilding },
+ darurat: { label: 'Darurat', icon: IconAlertTriangle },
+
+};
+
+type IconKey = keyof typeof iconMap;
+
+const iconList = Object.entries(iconMap).map(([value, data]) => ({
+ value,
+ label: data.label,
+}));
+
+export default function SelectIconProgram(
+ { onChange }: { onChange: (value: IconKey) => void }) {
+ const [selectedIcon, setSelectedIcon] = useState('ekowisata');
+ const IconComponent = iconMap[selectedIcon]?.icon || null;
+
+ // Push default icon ke state saat render awal
+ useEffect(() => {
+ onChange(selectedIcon);
+ }, []);
+
+ return (
+
+ {
+ if (value) {
+ setSelectedIcon(value as IconKey);
+ onChange(value as IconKey);
+ }
+ }}
+ data={iconList}
+ leftSection={
+ IconComponent && (
+
+
+
+ )
+ }
+ withCheckIcon={false}
+ searchable={false}
+ rightSectionWidth={0}
+ styles={{
+ input: {
+ textAlign: 'left',
+ fontSize: rem(16),
+ paddingLeft: 40,
+ },
+ section: {
+ left: 10,
+ right: 'auto',
+ },
+ }}
+ />
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx
new file mode 100644
index 00000000..093a41df
--- /dev/null
+++ b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx
@@ -0,0 +1,110 @@
+'use client'
+
+import { Box, rem, Select } from '@mantine/core';
+import {
+ IconAmbulance,
+ IconCash,
+ IconChartLine,
+ IconChristmasTreeFilled,
+ IconClipboardTextFilled,
+ IconDroplet,
+ IconFiretruck,
+ IconHome,
+ IconHomeEco,
+ IconHospital,
+ IconLeaf,
+ IconRecycle,
+ IconScale,
+ IconSchool,
+ IconShieldFilled,
+ IconShoppingCart,
+ IconTent,
+ IconTrashFilled,
+ IconTree,
+ IconTrendingUp,
+ IconTrophy,
+ IconTruckFilled,
+ IconBuilding,
+ IconAlertTriangle
+} from '@tabler/icons-react';
+
+const iconMap = {
+ ekowisata: { label: 'Ekowisata', icon: IconLeaf },
+ kompetisi: { label: 'Kompetisi', icon: IconTrophy },
+ wisata: { label: 'Wisata', icon: IconTent },
+ ekonomi: { label: 'Ekonomi', icon: IconChartLine },
+ sampah: { label: 'Sampah', icon: IconRecycle },
+ truck: { label: 'Truck', icon: IconTruckFilled },
+ scale: { label: 'Scale', icon: IconScale },
+ clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
+ trash: { label: 'Trash', icon: IconTrashFilled },
+ lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
+ sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
+ ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
+ mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
+ rumah: {label: 'Rumah', icon: IconHome},
+ pohon: {label: 'Pohon', icon: IconTree},
+ air: {label: 'Air', icon: IconDroplet},
+ bantuan: {label: 'Bantuan', icon: IconCash},
+ pelatihan: {label: 'Pelatihan', icon: IconSchool},
+ subsidi: {label: 'Subsidi', icon: IconShoppingCart},
+ layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
+ polisi: {label: 'Polisi', icon: IconShieldFilled},
+ ambulans: {label: 'Ambulans', icon: IconAmbulance},
+ pemadam: {label: 'Pemadam', icon: IconFiretruck},
+ rumahSakit: {label: 'Rumah Sakit', icon: IconHospital},
+ bangunan: {label: 'Bangunan', icon: IconBuilding},
+ darurat: {label: 'Darurat', icon: IconAlertTriangle},
+};
+
+type IconKey = keyof typeof iconMap;
+
+const iconList = Object.entries(iconMap).map(([value, data]) => ({
+ value,
+ label: data.label,
+}));
+
+export default function SelectIconProgramEdit({
+ onChange,
+ value,
+}: {
+ onChange: (value: IconKey) => void;
+ value: IconKey;
+}) {
+ const IconComponent = iconMap[value]?.icon || null;
+
+ return (
+
+ {
+ if (value) onChange(value as IconKey);
+ }}
+ data={iconList}
+ leftSection={
+ IconComponent && (
+
+
+
+ )
+ }
+ withCheckIcon={false}
+ searchable={false}
+ rightSectionWidth={0}
+ styles={{
+ input: {
+ textAlign: 'left',
+ fontSize: rem(16),
+ paddingLeft: 40,
+ },
+ section: {
+ left: 10,
+ right: 'auto',
+ },
+ }}
+ />
+
+ );
+}
+
diff --git a/src/app/admin/(dashboard)/_state/desa/berita.ts b/src/app/admin/(dashboard)/_state/desa/berita.ts
new file mode 100644
index 00000000..0a7dc17e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/berita.ts
@@ -0,0 +1,578 @@
+/* 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";
+
+// 1. Schema validasi dengan Zod
+const templateForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ content: z.string().min(3, "Content minimal 3 karakter"),
+ kategoriBeritaId: z.string().nonempty(),
+ imageId: z.string().nonempty(),
+});
+
+// 2. Default value form berita (hindari uncontrolled input)
+const defaultForm = {
+ judul: "",
+ deskripsi: "",
+ imageId: "",
+ content: "",
+ kategoriBeritaId: "",
+};
+
+// 4. Berita proxy
+const berita = proxy({
+ create: {
+ form: { ...defaultForm }, // ✅ ini kunci fix-nya
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(berita.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ berita.create.loading = true;
+ const res = await ApiFetch.api.desa.berita["create"].post(
+ berita.create.form
+ );
+ if (res.status === 200) {
+ berita.findMany.load();
+ return toast.success("Berita berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan berita");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ berita.create.loading = false;
+ }
+ },
+ resetForm() {
+ berita.create.form = { ...defaultForm };
+ },
+ },
+
+ // State untuk berita utama (hanya 1)
+
+ findMany: {
+ data: null as
+ | Prisma.BeritaGetPayload<{
+ include: {
+ image: true;
+ kategoriBerita: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", kategori = "") => {
+ const startTime = Date.now();
+ berita.findMany.loading = true;
+ berita.findMany.page = page;
+ berita.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+
+ const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ berita.findMany.data = res.data.data ?? [];
+ berita.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ berita.findMany.data = [];
+ berita.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita paginated:", err);
+ berita.findMany.data = [];
+ berita.findMany.totalPages = 1;
+ } finally {
+ // pastikan minimal 300ms sebelum loading = false (biar UX smooth)
+ const elapsed = Date.now() - startTime;
+ const minDelay = 300;
+ const delay = elapsed < minDelay ? minDelay - elapsed : 0;
+
+ setTimeout(() => {
+ berita.findMany.loading = false;
+ }, delay);
+ }
+ },
+ },
+
+ findUnique: {
+ data: null as Prisma.BeritaGetPayload<{
+ include: {
+ image: true;
+ kategoriBerita: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/berita/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ berita.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch berita:", res.statusText);
+ berita.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching berita:", error);
+ berita.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ berita.delete.loading = true;
+
+ const response = await fetch(`/api/desa/berita/delete/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Berita berhasil dihapus");
+ await berita.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus berita");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus berita");
+ } finally {
+ berita.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/berita/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ deskripsi: data.deskripsi,
+ content: data.content,
+ kategoriBeritaId: data.kategoriBeritaId || "",
+ imageId: data.imageId || "",
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading berita:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(berita.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ berita.edit.loading = true;
+
+ const response = await fetch(`/api/desa/berita/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ deskripsi: this.form.deskripsi,
+ content: this.form.content,
+ kategoriBeritaId: this.form.kategoriBeritaId || null,
+ imageId: this.form.imageId,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update berita");
+ await berita.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update berita");
+ }
+ } catch (error) {
+ console.error("Error updating berita:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update berita"
+ );
+ return false;
+ } finally {
+ berita.edit.loading = false;
+ }
+ },
+
+ reset() {
+ berita.edit.id = "";
+ berita.edit.form = { ...defaultForm };
+ },
+ },
+ findFirst: {
+ data: null as Prisma.BeritaGetPayload<{
+ include: {
+ image: true;
+ kategoriBerita: true;
+ };
+ }> | null,
+ loading: false,
+ // findFirst.load()
+ async load(kategori?: string) {
+ this.loading = true;
+ try {
+ const res = await ApiFetch.api.desa.berita["find-first"].get({
+ query: kategori ? { kategori } : {},
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data || null;
+ } else {
+ this.data = null;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita terbaru:", err);
+ this.data = null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findRecent: {
+ data: [] as Prisma.BeritaGetPayload<{
+ include: {
+ image: true;
+ kategoriBerita: true;
+ };
+ }>[],
+ loading: false,
+
+ async load() {
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.desa.berita["find-recent"].get();
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data ?? [];
+ }
+ } catch (error) {
+ console.error("Gagal fetch berita recent:", error);
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+});
+
+//=============== Kategori Berita ===============
+
+const templateKategoriBerita = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultKategoriBerita = {
+ name: "",
+};
+
+const kategoriBerita = proxy({
+ create: {
+ form: { ...defaultKategoriBerita },
+ loading: false,
+ async create() {
+ const cek = templateKategoriBerita.safeParse(kategoriBerita.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kategoriBerita.create.loading = true;
+ const res = await ApiFetch.api.desa.kategoriberita["create"].post(
+ kategoriBerita.create.form
+ );
+ if (res.status === 200) {
+ kategoriBerita.findMany.load();
+ return toast.success("Data Kategori Berita Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ kategoriBerita.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.KategoriBeritaGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
+ kategoriBerita.findMany.page = page;
+ kategoriBerita.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.desa.kategoriberita[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriBerita.findMany.data = res.data.data ?? [];
+ kategoriBerita.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ kategoriBerita.findMany.data = [];
+ kategoriBerita.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori berita paginated:", err);
+ kategoriBerita.findMany.data = [];
+ kategoriBerita.findMany.totalPages = 1;
+ } finally {
+ kategoriBerita.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriBeritaGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/kategoriberita/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kategoriBerita.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriBerita.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriBerita.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriBerita.delete.loading = true;
+
+ const response = await fetch(`/api/desa/kategoriberita/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Data Kategori Berita berhasil dihapus"
+ );
+ await kategoriBerita.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus Data Kategori Berita"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data Kategori Berita");
+ } finally {
+ kategoriBerita.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultKategoriBerita },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/kategoriberita/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori berita:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateKategoriBerita.safeParse(kategoriBerita.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriBerita.update.loading = true;
+
+ const response = await fetch(`/api/desa/kategoriberita/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data kategori berita");
+ await kategoriBerita.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update data kategori berita"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating data kategori berita:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data kategori berita"
+ );
+ return false;
+ } finally {
+ kategoriBerita.update.loading = false;
+ }
+ },
+ reset() {
+ kategoriBerita.update.id = "";
+ kategoriBerita.update.form = { ...defaultKategoriBerita };
+ },
+ },
+});
+
+// 5. State global
+const stateDashboardBerita = proxy({
+ kategoriBerita,
+ berita,
+});
+
+export default stateDashboardBerita;
diff --git a/src/app/admin/(dashboard)/_state/desa/gallery.ts b/src/app/admin/(dashboard)/_state/desa/gallery.ts
new file mode 100644
index 00000000..61faa3fd
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/gallery.ts
@@ -0,0 +1,486 @@
+/* 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 fotoForm = z.object({
+ name: z.string().min(1, { message: "Name is required" }),
+ deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
+ imagesId: z.string().nonempty(),
+});
+
+const videoForm = z.object({
+ name: z.string().min(1, { message: "Name is required" }),
+ deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
+ linkVideo: z.string().min(1, { message: "Link video is required" }),
+});
+
+const defaultFormFoto = {
+ name: "",
+ deskripsi: "",
+ imagesId: "",
+};
+
+const defaultFormVideo = {
+ name: "",
+ deskripsi: "",
+ linkVideo: "",
+};
+
+const foto = proxy({
+ create: {
+ form: { ...defaultFormFoto },
+ loading: false,
+ async create() {
+ const cek = fotoForm.safeParse(foto.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ foto.create.loading = true;
+ const res = await ApiFetch.api.desa.gallery.foto["create"].post(
+ foto.create.form
+ );
+ if (res.status === 200) {
+ foto.findMany.load();
+ return toast.success("Foto berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan foto");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ foto.create.loading = false;
+ }
+ },
+ resetForm() {
+ foto.create.form = { ...defaultFormFoto };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.GalleryFotoGetPayload<{
+ include: {
+ imageGalleryFoto: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ foto.findMany.loading = true; // ✅ Akses langsung via nama path
+ foto.findMany.page = page;
+ foto.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.desa.gallery.foto["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ foto.findMany.data = res.data.data ?? [];
+ foto.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ foto.findMany.data = [];
+ foto.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch foto paginated:", err);
+ foto.findMany.data = [];
+ foto.findMany.totalPages = 1;
+ } finally {
+ foto.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GalleryFotoGetPayload<{
+ include: {
+ imageGalleryFoto: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/gallery/foto/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ foto.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch foto:", res.statusText);
+ foto.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching foto:", error);
+ foto.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ foto.delete.loading = true;
+ const response = await fetch(`/api/desa/gallery/foto/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(result.message || "Foto berhasil dihapus");
+ await foto.findMany.load(); // refresh list
+ } else {
+ toast.error(result.message || "Gagal menghapus foto");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus foto");
+ } finally {
+ foto.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormFoto },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/desa/gallery/foto/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imagesId: data.imagesId || "",
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading foto:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = fotoForm.safeParse(foto.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+ try {
+ foto.update.loading = true;
+ const response = await fetch(`/api/desa/gallery/foto/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imagesId: this.form.imagesId,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Foto berhasil diupdate");
+ await foto.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate foto");
+ }
+ } catch (error) {
+ console.error("Error updating foto:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate foto"
+ );
+ return false;
+ } finally {
+ foto.update.loading = false;
+ }
+ },
+ reset() {
+ foto.update.id = "";
+ foto.update.form = { ...defaultFormFoto };
+ },
+ },
+ findRecent: {
+ data: [] as Prisma.GalleryFotoGetPayload<{
+ include: {
+ imageGalleryFoto: true;
+ };
+ }>[],
+ loading: false,
+
+ async load() {
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.desa.gallery.foto["find-recent"].get();
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data ?? [];
+ }
+ } catch (error) {
+ console.error("Gagal fetch foto recent:", error);
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+});
+
+const video = proxy({
+ create: {
+ form: { ...defaultFormVideo },
+ loading: false,
+ async create() {
+ const cek = videoForm.safeParse(video.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ video.create.loading = true;
+ const res = await ApiFetch.api.desa.gallery.video["create"].post(
+ video.create.form
+ );
+ if (res.status === 200) {
+ video.findMany.load();
+ return toast.success("Video berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan video");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ video.create.loading = false;
+ }
+ },
+ resetForm() {
+ video.create.form = { ...defaultFormVideo };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.GalleryVideoGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ video.findMany.loading = true; // ✅ Akses langsung via nama path
+ video.findMany.page = page;
+ video.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.desa.gallery.video["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ video.findMany.data = res.data.data ?? [];
+ video.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ video.findMany.data = [];
+ video.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch video paginated:", err);
+ video.findMany.data = [];
+ video.findMany.totalPages = 1;
+ } finally {
+ video.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GalleryVideoGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/gallery/video/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ video.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch video:", res.statusText);
+ video.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching video:", error);
+ video.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ video.delete.loading = true;
+ const response = await fetch(`/api/desa/gallery/video/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(result.message || "Video berhasil dihapus");
+ await video.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus video");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus video");
+ } finally {
+ video.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormVideo },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/desa/gallery/video/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ linkVideo: data.linkVideo,
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading video:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = videoForm.safeParse(video.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+ try {
+ video.update.loading = true;
+ const response = await fetch(`/api/desa/gallery/video/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ linkVideo: this.form.linkVideo,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Video berhasil diupdate");
+ await video.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate video");
+ }
+ } catch (error) {
+ console.error("Error updating video:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate video"
+ );
+ return false;
+ } finally {
+ video.update.loading = false;
+ }
+ },
+ reset() {
+ video.update.id = "";
+ video.update.form = { ...defaultFormVideo };
+ },
+ },
+});
+
+const stateGallery = proxy({
+ foto,
+ video,
+});
+
+export default stateGallery;
diff --git a/src/app/admin/(dashboard)/_state/desa/layananDesa.ts b/src/app/admin/(dashboard)/_state/desa/layananDesa.ts
new file mode 100644
index 00000000..a0a39410
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/layananDesa.ts
@@ -0,0 +1,1049 @@
+/* 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 templateSuratKeteranganForm = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+ image2Id: z.string().nonempty(),
+});
+
+const suratKeteranganForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+ image2Id: "",
+};
+
+const telunjukSaktiDesaForm = {
+ name: "",
+ deskripsi: "",
+ link: "",
+};
+
+const templateTelunjukSaktiDesaForm = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+const templatePelayananPerizinanBerusaha = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ link: z.string().min(3, "Link minimal 3 karakter"),
+});
+
+type pelayananPerizinanBerusahaForm =
+ Prisma.PelayananPerizinanBerusahaGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ deskripsi: true;
+ link: true;
+ };
+ }>;
+
+const pelayananPerizinanBerusahaForm = {
+ name: "",
+ deskripsi: "",
+ link: "",
+};
+
+const templatePelayananPendudukNonPermanen = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type pelayananPendudukNonPermanenForm =
+ Prisma.PelayananPendudukNonPermanenGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ deskripsi: true;
+ };
+ }>;
+
+const pelayananPendudukNonPermanenForm = {
+ name: "",
+ deskripsi: "",
+};
+
+const templateAjukanForm = z.object({
+ nama: z.string().min(1).max(5000),
+ nik: z.string().min(1).max(5000),
+ alamat: z.string().min(1).max(5000),
+ nomorKk: z.string().min(1).max(5000),
+ kategoriId: z.string().min(1).max(5000),
+});
+
+const defaultAjukanForm = {
+ nama: "",
+ nik: "",
+ alamat: "",
+ nomorKk: "",
+ kategoriId: "",
+};
+
+const suratKeterangan = proxy({
+ create: {
+ form: { ...suratKeteranganForm },
+ loading: false,
+ async create() {
+ const cek = templateSuratKeteranganForm.safeParse(
+ suratKeterangan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ suratKeterangan.create.loading = true;
+ const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
+ "create"
+ ].post(suratKeterangan.create.form);
+ if (res.status === 200) {
+ suratKeterangan.findMany.load();
+ return toast.success("Surat Keterangan berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan surat keterangan");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ suratKeterangan.create.loading = false;
+ }
+ },
+ resetForm() {
+ suratKeterangan.create.form = { ...suratKeteranganForm };
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ suratKeterangan.findMany.loading = true; // Use the full path to access the property
+ suratKeterangan.findMany.page = page;
+ suratKeterangan.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ suratKeterangan.findMany.data = res.data.data || [];
+ suratKeterangan.findMany.total = res.data.total || 0;
+ suratKeterangan.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load surat keterangan:", res.data?.message);
+ suratKeterangan.findMany.data = [];
+ suratKeterangan.findMany.total = 0;
+ suratKeterangan.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading surat keterangan:", error);
+ suratKeterangan.findMany.data = [];
+ suratKeterangan.findMany.total = 0;
+ suratKeterangan.findMany.totalPages = 1;
+ } finally {
+ suratKeterangan.findMany.loading = false;
+ }
+ },
+ },
+ findManyAll: {
+ data: null as Prisma.PelayananSuratKeteranganGetPayload<{
+ omit: { isActive: true };
+ }>[] | null,
+ loading: false,
+ load: async () => {
+ suratKeterangan.findManyAll.loading = true;
+ try {
+ const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan["findManyAll"].get();
+
+ if (res.status === 200 && res.data?.success) {
+ suratKeterangan.findManyAll.data = res.data.data || [];
+ } else {
+ suratKeterangan.findManyAll.data = [];
+ console.error("Failed to load surat keterangan all:", res.data?.message);
+ }
+ } catch (error) {
+ console.error("Error loading surat keterangan all:", error);
+ suratKeterangan.findManyAll.data = [];
+ } finally {
+ suratKeterangan.findManyAll.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PelayananSuratKeteranganGetPayload<{
+ include: {
+ image: true;
+ image2: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/desa/layanan/pelayanansuratketerangan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ suratKeterangan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch surat keterangan:", res.statusText);
+ suratKeterangan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching surat keterangan:", error);
+ suratKeterangan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ suratKeterangan.delete.loading = true;
+ const response = await fetch(
+ `/api/desa/layanan/pelayanansuratketerangan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(result.message || "Surat Keterangan berhasil dihapus");
+ await suratKeterangan.findMany.load(); // refresh list
+ } else {
+ toast.error(result.message || "Gagal menghapus surat keterangan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus surat keterangan");
+ } finally {
+ suratKeterangan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...suratKeteranganForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/desa/layanan/pelayanansuratketerangan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId || "",
+ image2Id: data.image2Id || "",
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching surat keterangan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateSuratKeteranganForm.safeParse(
+ suratKeterangan.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ suratKeterangan.edit.loading = true;
+ const response = await fetch(
+ `/api/desa/layanan/pelayanansuratketerangan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ image2Id: this.form.image2Id,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Surat Keterangan berhasil diupdate");
+ await suratKeterangan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate surat keterangan"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating surat keterangan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update surat keterangan"
+ );
+ return false;
+ } finally {
+ suratKeterangan.edit.loading = false;
+ }
+ },
+ },
+});
+
+const pelayananTelunjukSaktiDesa = proxy({
+ create: {
+ form: { ...telunjukSaktiDesaForm },
+ loading: false,
+ async create() {
+ const cek = templateTelunjukSaktiDesaForm.safeParse(
+ pelayananTelunjukSaktiDesa.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pelayananTelunjukSaktiDesa.create.loading = true;
+ const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
+ "create"
+ ].post(pelayananTelunjukSaktiDesa.create.form);
+ if (res.status === 200) {
+ pelayananTelunjukSaktiDesa.findMany.load();
+ return toast.success("Telunjuk Sakti Desa berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan telunjuk sakti desa");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ pelayananTelunjukSaktiDesa.create.loading = false;
+ }
+ },
+ resetForm() {
+ pelayananTelunjukSaktiDesa.create.form = { ...telunjukSaktiDesaForm };
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
+ pelayananTelunjukSaktiDesa.findMany.page = page;
+ pelayananTelunjukSaktiDesa.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
+ pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
+ pelayananTelunjukSaktiDesa.findMany.totalPages =
+ res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load surat keterangan:", res.data?.message);
+ pelayananTelunjukSaktiDesa.findMany.data = [];
+ suratKeterangan.findMany.total = 0;
+ suratKeterangan.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading surat keterangan:", error);
+ pelayananTelunjukSaktiDesa.findMany.data = [];
+ pelayananTelunjukSaktiDesa.findMany.total = 0;
+ pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
+ } finally {
+ pelayananTelunjukSaktiDesa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PelayananTelunjukSaktiDesaGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/desa/layanan/pelayanantelunjuksaktidesa/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ pelayananTelunjukSaktiDesa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch telunjuk sakti desa:", res.statusText);
+ pelayananTelunjukSaktiDesa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching telunjuk sakti desa:", error);
+ pelayananTelunjukSaktiDesa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pelayananTelunjukSaktiDesa.delete.loading = true;
+ const response = await fetch(
+ `/api/desa/layanan/pelayanantelunjuksaktidesa/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(
+ result.message || "Telunjuk Sakti Desa berhasil dihapus"
+ );
+ await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
+ } else {
+ toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus telunjuk sakti desa");
+ } finally {
+ pelayananTelunjukSaktiDesa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...telunjukSaktiDesaForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/desa/layanan/pelayanantelunjuksaktidesa/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ link: data.link,
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching telunjuk sakti desa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateTelunjukSaktiDesaForm.safeParse(
+ pelayananTelunjukSaktiDesa.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pelayananTelunjukSaktiDesa.edit.loading = true;
+ const response = await fetch(
+ `/api/desa/layanan/pelayanantelunjuksaktidesa/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ link: this.form.link,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(
+ result.message || "Telunjuk Sakti Desa berhasil diupdate"
+ );
+ await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate telunjuk sakti desa"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating telunjuk sakti desa:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update telunjuk sakti desa"
+ );
+ return false;
+ } finally {
+ pelayananTelunjukSaktiDesa.edit.loading = false;
+ }
+ },
+ },
+});
+
+const pelayananPerizinanBerusaha = proxy({
+ findById: {
+ data: null as pelayananPerizinanBerusahaForm | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ this.loading = true;
+ const response = await fetch(`/api/desa/layanan/pelayananperizinanberusaha/${id}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ this.data = result.data; // Make sure this matches your API response structure
+ }
+ return result?.data || null;
+ } catch (error) {
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data');
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...pelayananPerizinanBerusahaForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak boleh kosong");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/desa/layanan/pelayananperizinanberusaha/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ pelayananPerizinanBerusaha.update.id = data.id;
+ pelayananPerizinanBerusaha.update.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ link: data.link,
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching pelayanan perizinan berusaha:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update(data: pelayananPerizinanBerusahaForm) {
+ const cek = templatePelayananPerizinanBerusaha.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ pelayananPerizinanBerusaha.update.loading = true;
+ const res = await fetch(
+ `/api/desa/layanan/pelayananperizinanberusaha/${data.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ }
+ );
+ if (res.ok) {
+ toast.success("Pelayanan perizinan berusaha berhasil diupdate");
+ await pelayananPerizinanBerusaha.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengupdate pelayanan perizinan berusaha");
+ }
+ } catch (error) {
+ console.error("Error updating pelayanan perizinan berusaha:", error);
+ toast.error(
+ "Terjadi kesalahan saat mengupdate pelayanan perizinan berusaha"
+ );
+ } finally {
+ pelayananPerizinanBerusaha.update.loading = false;
+ }
+ },
+ },
+});
+
+const pelayananPendudukNonPermanen = proxy({
+ findById: {
+ data: null as pelayananPendudukNonPermanenForm | null,
+ loading: false,
+ initialize() {
+ pelayananPendudukNonPermanen.findById.data = {
+ id: "",
+ name: "",
+ deskripsi: "",
+ } as pelayananPendudukNonPermanenForm;
+ },
+ async load(id: string) {
+ try {
+ pelayananPendudukNonPermanen.findById.loading = true;
+ const res = await fetch(
+ `/api/desa/layanan/pelayananpenduduknonpermanen/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ pelayananPendudukNonPermanen.findById.data = data.data ?? null;
+ } else {
+ console.error(
+ "Failed to fetch pelayanan penduduk non permanen:",
+ res.statusText
+ );
+ pelayananPendudukNonPermanen.findById.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching pelayanan penduduk non permanen:", error);
+ pelayananPendudukNonPermanen.findById.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...pelayananPendudukNonPermanenForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak boleh kosong");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/desa/layanan/pelayananpenduduknonpermanen/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ pelayananPendudukNonPermanen.update.id = data.id;
+ pelayananPendudukNonPermanen.update.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching pelayanan penduduk non permanen:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update(data: pelayananPendudukNonPermanenForm) {
+ const cek = templatePelayananPendudukNonPermanen.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ pelayananPendudukNonPermanen.update.loading = true;
+ const res = await fetch(
+ `/api/desa/layanan/pelayananpenduduknonpermanen/${data.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ }
+ );
+ if (res.ok) {
+ toast.success("Pelayanan penduduk non permanen berhasil diupdate");
+ await pelayananPendudukNonPermanen.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengupdate pelayanan penduduk non permanen");
+ }
+ } catch (error) {
+ console.error("Error updating pelayanan penduduk non permanen:", error);
+ toast.error(
+ "Terjadi kesalahan saat mengupdate pelayanan penduduk non permanen"
+ );
+ } finally {
+ pelayananPendudukNonPermanen.update.loading = false;
+ }
+ },
+ },
+});
+
+const ajukanPermohonan = proxy({
+ create: {
+ form: { ...defaultAjukanForm },
+ loading: false,
+ async create() {
+ const cek = templateAjukanForm.safeParse(
+ ajukanPermohonan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ ajukanPermohonan.create.loading = true;
+ const res = await ApiFetch.api.desa.ajukanpermohonan[
+ "create"
+ ].post(ajukanPermohonan.create.form);
+ if (res.status === 200) {
+ ajukanPermohonan.findMany.load();
+ return toast.success("Ajukan permohonan berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan ajukan permohonan");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ ajukanPermohonan.create.loading = false;
+ }
+ },
+ resetForm() {
+ ajukanPermohonan.create.form = { ...defaultAjukanForm };
+ },
+ },
+ findMany: {
+ data: null as Prisma.AjukanPermohonanGetPayload<{
+ include: {
+ kategori: true;
+ };
+ }>[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ ajukanPermohonan.findMany.loading = true; // Use the full path to access the property
+ ajukanPermohonan.findMany.page = page;
+ ajukanPermohonan.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.desa.ajukanpermohonan[
+ "findMany"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ ajukanPermohonan.findMany.data = res.data.data || [];
+ ajukanPermohonan.findMany.total = res.data.total || 0;
+ ajukanPermohonan.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load ajukan permohonan:", res.data?.message);
+ ajukanPermohonan.findMany.data = [];
+ ajukanPermohonan.findMany.total = 0;
+ ajukanPermohonan.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading ajukan permohonan:", error);
+ ajukanPermohonan.findMany.data = [];
+ ajukanPermohonan.findMany.total = 0;
+ ajukanPermohonan.findMany.totalPages = 1;
+ } finally {
+ ajukanPermohonan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.AjukanPermohonanGetPayload<{
+ include: {
+ kategori: true;
+ }
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/desa/ajukanpermohonan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ ajukanPermohonan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch ajukan permohonan:", res.statusText);
+ ajukanPermohonan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching ajukan permohonan:", error);
+ ajukanPermohonan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ ajukanPermohonan.delete.loading = true;
+ const response = await fetch(
+ `/api/desa/ajukanpermohonan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(result.message || "Ajukan permohonan berhasil dihapus");
+ await ajukanPermohonan.findMany.load(); // refresh list
+ } else {
+ toast.error(result.message || "Gagal menghapus ajukan permohonan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus ajukan permohonan");
+ } finally {
+ ajukanPermohonan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultAjukanForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/desa/ajukanpermohonan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ nik: data.nik,
+ alamat: data.alamat,
+ nomorKk: data.nomorKk,
+ kategoriId: data.kategoriId,
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching ajukan permohonan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateAjukanForm.safeParse(
+ ajukanPermohonan.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ ajukanPermohonan.edit.loading = true;
+ const response = await fetch(
+ `/api/desa/ajukanpermohonan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ nik: this.form.nik,
+ alamat: this.form.alamat,
+ nomorKk: this.form.nomorKk,
+ kategoriId: this.form.kategoriId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Ajukan permohonan berhasil diupdate");
+ await ajukanPermohonan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate ajukan permohonan"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating ajukan permohonan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update ajukan permohonan"
+ );
+ return false;
+ } finally {
+ ajukanPermohonan.edit.loading = false;
+ }
+ },
+ },
+});
+
+const stateLayananDesa = proxy({
+ suratKeterangan,
+ pelayananPerizinanBerusaha,
+ pelayananTelunjukSaktiDesa,
+ pelayananPendudukNonPermanen,
+ ajukanPermohonan,
+});
+
+export default stateLayananDesa;
diff --git a/src/app/admin/(dashboard)/_state/desa/penghargaan.ts b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts
new file mode 100644
index 00000000..68be0ba7
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts
@@ -0,0 +1,248 @@
+/* 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({
+ name: z.string().min(1).max(5000),
+ juara: z.string().min(1).max(5000),
+ deskripsi: z.string().min(1).max(5000),
+ imageId: z.string().min(1).max(5000),
+});
+
+const defaultForm = {
+ name: "",
+ juara: "",
+ deskripsi: "",
+ imageId: "",
+};
+
+const penghargaanState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(penghargaanState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ penghargaanState.create.loading = true;
+ const res = await ApiFetch.api.desa.penghargaan["create"].post(
+ penghargaanState.create.form
+ );
+ if (res.status === 200) {
+ penghargaanState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ penghargaanState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ penghargaanState.findMany.loading = true; // Use the full path to access the property
+ penghargaanState.findMany.page = page;
+ penghargaanState.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.desa.penghargaan[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ penghargaanState.findMany.data = res.data.data || [];
+ penghargaanState.findMany.total = res.data.total || 0;
+ penghargaanState.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load penghargaan:", res.data?.message);
+ penghargaanState.findMany.data = [];
+ penghargaanState.findMany.total = 0;
+ penghargaanState.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading penghargaan:", error);
+ penghargaanState.findMany.data = [];
+ penghargaanState.findMany.total = 0;
+ penghargaanState.findMany.totalPages = 1;
+ } finally {
+ penghargaanState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PenghargaanGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/penghargaan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ penghargaanState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ penghargaanState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading penghargaan:", error);
+ penghargaanState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ penghargaanState.delete.loading = true;
+ const response = await fetch(`/api/desa/penghargaan/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+
+ if (response.ok) {
+ toast.success(result.message || "Penghargaan berhasil dihapus");
+ await penghargaanState.findMany.load();
+ } else {
+ toast.error(result?.message || "Gagal menghapus penghargaan");
+ }
+ } catch (error) {
+ console.log((error as Error).message);
+ toast.error("Terjadi kesalahan saat menghapus penghargaan");
+ } finally {
+ penghargaanState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/penghargaan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ juara: data.juara,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading penghargaan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(penghargaanState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ penghargaanState.edit.loading = true;
+ const response = await fetch(
+ `/api/desa/penghargaan/${penghargaanState.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ juara: this.form.juara,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update penghargaan");
+ await penghargaanState.findMany.load();
+ return true;
+ } else {
+ throw new Error(result?.message || "Gagal update penghargaan");
+ }
+ } catch (error) {
+ console.error("Error updating penghargaan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update penghargaan"
+ );
+ return false;
+ } finally {
+ penghargaanState.edit.loading = false;
+ }
+ },
+ reset() {
+ penghargaanState.edit.id = "";
+ penghargaanState.edit.form = { ...defaultForm };
+ },
+ },
+});
+export default penghargaanState;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts
new file mode 100644
index 00000000..09320003
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts
@@ -0,0 +1,556 @@
+/* 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 templateKategoriPengumuman = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultKategoriPengumuman = {
+ name: "",
+};
+
+const category = proxy({
+ create: {
+ form: { ...defaultKategoriPengumuman },
+ loading: false,
+ async create() {
+ const cek = templateKategoriPengumuman.safeParse(category.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ category.create.loading = true;
+ const res = await ApiFetch.api.desa.kategoripengumuman["create"].post(
+ category.create.form
+ );
+ if (res.status === 200) {
+ category.findMany.load();
+ return toast.success("Data Kategori Pengumuman Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ category.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as (Prisma.CategoryPengumumanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> & {
+ _count: {
+ pengumumans: number;
+ };
+ })[],
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
+ category.findMany.loading = true; // Use the full path to access the property
+ category.findMany.page = page;
+ category.findMany.search = search;
+ try {
+ const res = await ApiFetch.api.desa.kategoripengumuman[
+ "findMany"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ category.findMany.data = res.data.data || [];
+ category.findMany.total = res.data.total || 0;
+ category.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load potensi desa:", res.data?.message);
+ category.findMany.data = [];
+ category.findMany.total = 0;
+ category.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading potensi desa:", error);
+ category.findMany.data = [];
+ category.findMany.total = 0;
+ category.findMany.totalPages = 1;
+ } finally {
+ category.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.CategoryPengumumanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/kategoripengumuman/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ category.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ category.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ category.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ category.delete.loading = true;
+
+ const response = await fetch(`/api/desa/kategoripengumuman/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Data Kategori Pengumuman berhasil dihapus"
+ );
+ await category.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus Data Kategori Pengumuman"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error(
+ "Terjadi kesalahan saat menghapus Data Kategori Pengumuman"
+ );
+ } finally {
+ category.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultKategoriPengumuman },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/kategoripengumuman/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori pengumuman:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateKategoriPengumuman.safeParse(category.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ category.update.loading = true;
+
+ const response = await fetch(
+ `/api/desa/kategoripengumuman/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data kategori pengumuman");
+ await category.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update data kategori pengumuman"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating data kategori pengumuman:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data kategori pengumuman"
+ );
+ return false;
+ } finally {
+ category.update.loading = false;
+ }
+ },
+ reset() {
+ category.update.id = "";
+ category.update.form = { ...defaultKategoriPengumuman };
+ },
+ },
+});
+
+const templateFormPengumuman = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ content: z.string().min(3, "Content minimal 3 karakter"),
+ categoryPengumumanId: z.string().nonempty(),
+});
+
+const defaultForm = {
+ judul: "",
+ deskripsi: "",
+ content: "",
+ categoryPengumumanId: "",
+};
+const pengumuman = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateFormPengumuman.safeParse(pengumuman.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pengumuman.create.loading = true;
+ const res = await ApiFetch.api.desa.pengumuman["create"].post(
+ pengumuman.create.form
+ );
+ if (res.status === 200) {
+ pengumuman.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ pengumuman.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.PengumumanGetPayload<{
+ include: {
+ CategoryPengumuman: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", kategori = "") => {
+ pengumuman.findMany.loading = true; // ✅ Akses langsung via nama path
+ pengumuman.findMany.page = page;
+ pengumuman.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+
+ const res = await ApiFetch.api.desa.pengumuman["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pengumuman.findMany.data = res.data.data ?? [];
+ pengumuman.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ pengumuman.findMany.data = [];
+ pengumuman.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch pengumuman paginated:", err);
+ pengumuman.findMany.data = [];
+ pengumuman.findMany.totalPages = 1;
+ } finally {
+ pengumuman.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PengumumanGetPayload<{
+ include: {
+ CategoryPengumuman: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/pengumuman/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ pengumuman.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch pengumuman:", res.statusText);
+ pengumuman.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching pengumuman:", error);
+ pengumuman.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pengumuman.delete.loading = true;
+
+ const response = await fetch(`/api/desa/pengumuman/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Pengumuman berhasil dihapus");
+ await pengumuman.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus pengumuman");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pengumuman");
+ } finally {
+ pengumuman.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/pengumuman/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ deskripsi: data.deskripsi,
+ content: data.content,
+ categoryPengumumanId: data.categoryPengumumanId || "",
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading pengumuman:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateFormPengumuman.safeParse(pengumuman.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ pengumuman.edit.loading = true;
+
+ const response = await fetch(`/api/desa/pengumuman/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ deskripsi: this.form.deskripsi,
+ content: this.form.content,
+ categoryPengumumanId: this.form.categoryPengumumanId || null,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update pengumuman");
+ await pengumuman.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update pengumuman");
+ }
+ } catch (error) {
+ console.error("Error updating pengumuman:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update pengumuman"
+ );
+ return false;
+ } finally {
+ pengumuman.edit.loading = false;
+ }
+ },
+
+ reset() {
+ pengumuman.edit.id = "";
+ pengumuman.edit.form = { ...defaultForm };
+ },
+ },
+ findFirst: {
+ data: null as Prisma.PengumumanGetPayload<{
+ include: {
+ CategoryPengumuman: true;
+ };
+ }> | null,
+ loading: false,
+ async load() {
+ this.loading = true;
+ try {
+ const res = await ApiFetch.api.desa.pengumuman["find-first"].get();
+ if (res.status === 200 && res.data?.success) {
+ // Add type assertion to ensure type safety
+ pengumuman.findFirst.data = res.data
+ .data as Prisma.PengumumanGetPayload<{
+ include: {
+ CategoryPengumuman: true;
+ };
+ }> | null;
+ }
+ } catch (err) {
+ console.error("Gagal fetch pengumuman terbaru:", err);
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findRecent: {
+ data: [] as Prisma.PengumumanGetPayload<{
+ include: {
+ CategoryPengumuman: true;
+ };
+ }>[],
+ loading: false,
+
+ async load() {
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.desa.pengumuman["find-recent"].get();
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data ?? [];
+ }
+ } catch (error) {
+ console.error("Gagal fetch pengumuman recent:", error);
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+});
+
+const stateDesaPengumuman = proxy({
+ category,
+ pengumuman,
+});
+export default stateDesaPengumuman;
diff --git a/src/app/admin/(dashboard)/_state/desa/potensi.ts b/src/app/admin/(dashboard)/_state/desa/potensi.ts
new file mode 100644
index 00000000..0c158b38
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/potensi.ts
@@ -0,0 +1,500 @@
+/* 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({
+ name: z.string().min(1).max(5000),
+ deskripsi: z.string().min(1).max(5000),
+ kategoriId: z.string().min(1).max(50),
+ imageId: z.string().min(1).max(50),
+ content: z.string().min(1).max(5000),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ kategoriId: "",
+ imageId: "",
+ content: "",
+};
+
+const potensiDesa = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(potensiDesa.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ potensiDesa.create.loading = true;
+ const res = await ApiFetch.api.desa.potensi["create"].post(
+ potensiDesa.create.form
+ );
+ if (res.status === 200) {
+ potensiDesa.findMany.load();
+ return toast.success("Potensi berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan potensi");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ potensiDesa.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
+ potensiDesa.findMany.loading = true; // Use the full path to access the property
+ potensiDesa.findMany.page = page;
+ potensiDesa.findMany.search = search;
+ try {
+ const res = await ApiFetch.api.desa.potensi[
+ "find-many"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ potensiDesa.findMany.data = res.data.data || [];
+ potensiDesa.findMany.total = res.data.total || 0;
+ potensiDesa.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load potensi desa:", res.data?.message);
+ potensiDesa.findMany.data = [];
+ potensiDesa.findMany.total = 0;
+ potensiDesa.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading potensi desa:", error);
+ potensiDesa.findMany.data = [];
+ potensiDesa.findMany.total = 0;
+ potensiDesa.findMany.totalPages = 1;
+ } finally {
+ potensiDesa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PotensiDesaGetPayload<{
+ include: {
+ image: true;
+ kategori: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/potensi/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ potensiDesa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch potensi:", res.statusText);
+ potensiDesa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching potensi:", error);
+ potensiDesa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ potensiDesa.delete.loading = true;
+
+ const response = await fetch(`/api/desa/potensi/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Potensi berhasil dihapus");
+ await potensiDesa.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus potensi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus potensi");
+ } finally {
+ potensiDesa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/potensi/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ kategoriId: data.kategoriId,
+ imageId: data.imageId || "",
+ content: data.content,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading potensi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(potensiDesa.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ potensiDesa.edit.loading = true;
+
+ const response = await fetch(`/api/desa/potensi/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ kategoriId: this.form.kategoriId,
+ imageId: this.form.imageId,
+ content: this.form.content,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update potensi");
+ await potensiDesa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update potensi");
+ }
+ } catch (error) {
+ console.error("Error updating potensi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update potensi"
+ );
+ return false;
+ } finally {
+ potensiDesa.edit.loading = false;
+ }
+ },
+ reset() {
+ potensiDesa.edit.id = "";
+ potensiDesa.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+const templateKategoriPotensi = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultKategoriPotensi = {
+ nama: "",
+};
+
+const kategoriPotensi = proxy({
+ create: {
+ form: { ...defaultKategoriPotensi },
+ loading: false,
+ async create() {
+ const cek = templateKategoriPotensi.safeParse(
+ kategoriPotensi.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kategoriPotensi.create.loading = true;
+ const res = await ApiFetch.api.desa.kategoripotensi["create"].post(
+ kategoriPotensi.create.form
+ );
+ if (res.status === 200) {
+ kategoriPotensi.findMany.load();
+ return toast.success("Data Kategori Potensi Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ kategoriPotensi.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.KategoriPotensiGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kategoriPotensi.findMany.loading = true; // ✅ Akses langsung via nama path
+ kategoriPotensi.findMany.page = page;
+ kategoriPotensi.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriPotensi.findMany.data = res.data.data ?? [];
+ kategoriPotensi.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kategoriPotensi.findMany.data = [];
+ kategoriPotensi.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori potensi paginated:", err);
+ kategoriPotensi.findMany.data = [];
+ kategoriPotensi.findMany.totalPages = 1;
+ } finally {
+ kategoriPotensi.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriPotensiGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/kategoripotensi/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kategoriPotensi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriPotensi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriPotensi.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriPotensi.delete.loading = true;
+
+ const response = await fetch(`/api/desa/kategoripotensi/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Data Kategori Potensi berhasil dihapus"
+ );
+ await kategoriPotensi.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus Data Kategori Potensi"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data Kategori Potensi");
+ } finally {
+ kategoriPotensi.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultKategoriPotensi },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/desa/kategoripotensi/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori potensi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateKategoriPotensi.safeParse(
+ kategoriPotensi.update.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriPotensi.update.loading = true;
+
+ const response = await fetch(`/api/desa/kategoripotensi/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data kategori potensi");
+ await kategoriPotensi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update data kategori potensi"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating data kategori potensi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data kategori potensi"
+ );
+ return false;
+ } finally {
+ kategoriPotensi.update.loading = false;
+ }
+ },
+ reset() {
+ kategoriPotensi.update.id = "";
+ kategoriPotensi.update.form = { ...defaultKategoriPotensi };
+ },
+ },
+});
+
+const potensiDesaState = proxy({
+ potensiDesa,
+ kategoriPotensi,
+});
+
+export default potensiDesaState;
diff --git a/src/app/admin/(dashboard)/_state/desa/profile.ts b/src/app/admin/(dashboard)/_state/desa/profile.ts
new file mode 100644
index 00000000..b8bb16d8
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/desa/profile.ts
@@ -0,0 +1,1057 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { toast } from "react-toastify";
+import { proxy } from "valtio";
+import { z } from "zod";
+import { Prisma } from "@prisma/client";
+import ApiFetch from "@/lib/api-fetch";
+
+// ========================================= SEJARAH DESA ========================================= //
+const sejarahDesaForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+const sejarahDesaDefaultForm = {
+ judul: "",
+ deskripsi: "",
+};
+
+type SejarahDesaForm = Prisma.SejarahDesaGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const sejarahDesa = proxy({
+ findUnique: {
+ data: null as SejarahDesaForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/sejarah/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengambil data sejarah desa"
+ );
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ console.error("Load sejarah desa error:", msg);
+ toast.error("Terjadi kesalahan saat mengambil data sejarah desa");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+ update: {
+ id: "",
+ form: { ...sejarahDesaDefaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(sejarahData: SejarahDesaForm) {
+ this.id = sejarahData.id;
+ this.isReadOnly = false;
+ this.form = {
+ judul: sejarahData.judul || "",
+ deskripsi: sejarahData.deskripsi || "",
+ };
+ },
+
+ updateField(field: keyof typeof sejarahDesaDefaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ async submit() {
+ // Validate form
+ const validation = sejarahDesaForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/sejarah/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update profile");
+ // Refresh profile data
+ await sejarahDesa.findUnique.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update profile");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update profile error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update profile");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ // Reset form
+ reset() {
+ this.id = "";
+ this.form = { ...sejarahDesaDefaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+});
+
+// ========================================= VISI MISI DESA ========================================= //
+const visiMisiDesaForm = z.object({
+ visi: z.string().min(3, "Visi minimal 3 karakter"),
+ misi: z.string().min(3, "Misi minimal 3 karakter"),
+});
+
+const visiMisiDesaDefaultForm = {
+ visi: "",
+ misi: "",
+};
+
+type VisiMisiDesaForm = Prisma.VisiMisiDesaGetPayload<{
+ select: {
+ id: true;
+ visi: true;
+ misi: true;
+ };
+}>;
+
+const visiMisiDesa = proxy({
+ findUnique: {
+ data: null as VisiMisiDesaForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/visi-misi/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengambil data visi misi desa"
+ );
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ console.error("Load visi misi desa error:", msg);
+ toast.error("Terjadi kesalahan saat mengambil data visi misi desa");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+ update: {
+ id: "",
+ form: { ...visiMisiDesaDefaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(visiMisiData: VisiMisiDesaForm) {
+ this.id = visiMisiData.id;
+ this.isReadOnly = false;
+ this.form = {
+ visi: visiMisiData.visi || "",
+ misi: visiMisiData.misi || "",
+ };
+ },
+
+ updateField(field: keyof typeof visiMisiDesaDefaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ async submit() {
+ // Validate form
+ const validation = visiMisiDesaForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/visi-misi/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update visi misi desa");
+ // Refresh profile data
+ await visiMisiDesa.findUnique.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update visi misi desa");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update visi misi desa error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update visi misi desa");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ // Reset form
+ reset() {
+ this.id = "";
+ this.form = { ...visiMisiDesaDefaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+});
+
+// ========================================= LAMBANG DESA ========================================= //
+const lambangDesaForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+const lambangDesaDefaultForm = {
+ judul: "",
+ deskripsi: "",
+};
+
+type LambangDesaForm = Prisma.LambangDesaGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const lambangDesa = proxy({
+ findUnique: {
+ data: null as LambangDesaForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/lambang/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengambil data lambang desa"
+ );
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ console.error("Load lambang desa error:", msg);
+ toast.error("Terjadi kesalahan saat mengambil data lambang desa");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+ update: {
+ id: "",
+ form: { ...lambangDesaDefaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(lambangDesaData: LambangDesaForm) {
+ this.id = lambangDesaData.id;
+ this.isReadOnly = false;
+ this.form = {
+ judul: lambangDesaData.judul || "",
+ deskripsi: lambangDesaData.deskripsi || "",
+ };
+ },
+
+ updateField(field: keyof typeof lambangDesaDefaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ async submit() {
+ // Validate form
+ const validation = lambangDesaForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/lambang/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update lambang desa");
+ // Refresh profile data
+ await lambangDesa.findUnique.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update lambang desa");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update lambang desa error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update lambang desa");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ // Reset form
+ reset() {
+ this.id = "";
+ this.form = { ...lambangDesaDefaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+});
+
+// ========================================= MASKOT DESA ========================================= //
+const maskotForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ images: z
+ .array(
+ z.object({
+ label: z.string().min(1, "Label wajib"),
+ imageId: z.string().min(1, "Image ID wajib"),
+ })
+ )
+ .min(1, "Minimal 1 gambar harus diisi"),
+});
+
+const maskotDefaultForm = {
+ judul: "",
+ deskripsi: "",
+ images: [] as { label: string; imageId: string }[],
+};
+
+type FormData = typeof maskotDefaultForm;
+
+type MaskotDesaForm = Prisma.MaskotDesaGetPayload<{
+ include: {
+ images: {
+ include: {
+ image: {
+ select: {
+ id: true;
+ name: true;
+ path: true;
+ link: true;
+ };
+ };
+ };
+ };
+ };
+}>;
+
+const maskotDesa = proxy({
+ findUnique: {
+ data: null as MaskotDesaForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/maskot/${id}`);
+ const result = await response.json();
+
+ if (response.ok && result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(result.message || "Gagal mengambil data profile");
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ console.error("Load profile error:", msg);
+ toast.error("Terjadi kesalahan saat mengambil data profile");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+
+ update: {
+ id: "",
+ form: { ...maskotDefaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(profileData: MaskotDesaForm) {
+ this.id = profileData.id;
+ this.isReadOnly = false;
+ this.form = {
+ judul: profileData.judul || "",
+ deskripsi: profileData.deskripsi || "",
+ images: (profileData.images || []).map((img) => ({
+ label: img.label,
+ imageId: img.image.id,
+ })),
+ };
+ },
+
+ updateField(field: K, value: FormData[K]) {
+ this.form[field] = value;
+ },
+
+ addImage() {
+ this.form.images.push({ label: "", imageId: "" });
+ },
+
+ removeImage(index: number) {
+ this.form.images.splice(index, 1);
+ },
+
+ async submit() {
+ const validation = maskotForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/maskot/${this.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(this.form),
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result.success) {
+ toast.success("Berhasil update profile");
+ await maskotDesa.findUnique.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update profile");
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ toast.error("Terjadi kesalahan saat update profile");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.id = "";
+ this.form = { ...maskotDefaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+
+ async loadForEdit(id: string) {
+ const data = await this.findUnique.load(id);
+ if (data) {
+ this.update.initialize(data);
+ }
+ return data;
+ },
+
+ reset() {
+ this.findUnique.reset();
+ this.update.reset();
+ },
+});
+
+// ========================================= PROFIL PERBEKEL ========================================= //
+const profilPerbekelForm = z.object({
+ biodata: z.string().min(3, "Biodata minimal 3 karakter"),
+ pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"),
+ pengalamanOrganisasi: z
+ .string()
+ .min(3, "Pengalaman Organisasi minimal 3 karakter"),
+ programUnggulan: z.string().min(3, "Program Unggulan minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+});
+
+const profilPerbekelDefaultForm = {
+ biodata: "",
+ pengalaman: "",
+ pengalamanOrganisasi: "",
+ programUnggulan: "",
+ imageId: "",
+};
+
+type ProfilPerbekelForm = Prisma.ProfilPerbekelGetPayload<{
+ select: {
+ id: true;
+ biodata: true;
+ pengalaman: true;
+ pengalamanOrganisasi: true;
+ programUnggulan: true;
+ imageId: true;
+ image?: {
+ select: {
+ link: true;
+ };
+ };
+ };
+}>;
+
+const profilPerbekel = proxy({
+ findUnique: {
+ data: null as ProfilPerbekelForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/desa/profile/profileperbekel/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengambil data profil perbekel"
+ );
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ toast.error("Terjadi kesalahan saat mengambil data profil perbekel");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+
+ edit: {
+ id: "",
+ form: { ...profilPerbekelDefaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(profilData: ProfilPerbekelForm) {
+ this.id = profilData.id;
+ this.isReadOnly = false;
+ this.form = {
+ biodata: profilData.biodata || "",
+ pengalaman: profilData.pengalaman || "",
+ pengalamanOrganisasi: profilData.pengalamanOrganisasi || "",
+ programUnggulan: profilData.programUnggulan || "",
+ imageId: profilData.imageId || "",
+ };
+ },
+
+ updateField(field: keyof typeof profilPerbekelDefaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ async submit() {
+ const validation = profilPerbekelForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(
+ `/api/desa/profile/profileperbekel/${this.id}`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(this.form),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update profil perbekel");
+ await profilPerbekel.findUnique.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update profil perbekel");
+ }
+ } catch (error) {
+ const msg = (error as Error).message;
+ this.error = msg;
+ toast.error("Terjadi kesalahan saat update profil perbekel");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.id = "";
+ this.form = { ...profilPerbekelDefaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+
+ async loadForEdit(id: string) {
+ const profileData = await this.findUnique.load(id);
+ if (profileData) {
+ this.edit.initialize(profileData);
+ }
+ return profileData;
+ },
+
+ reset() {
+ this.findUnique.reset();
+ this.edit.reset();
+ },
+});
+
+//========================================= MANTAN PERBEKEL ========================================= //
+const mantanPerbekelForm = z.object({
+ nama: z.string().min(3, "Nama minimal 3 karakter"),
+ daerah: z.string().min(3, "Daerah minimal 3 karakter"),
+ periode: z.string().min(3, "Periode minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+});
+
+const mantanPerbekelDefaultForm = {
+ nama: "",
+ daerah: "",
+ periode: "",
+ imageId: "",
+};
+
+const mantanPerbekel = proxy({
+ create: {
+ form: { ...mantanPerbekelDefaultForm },
+ loading: false,
+ async create() {
+ const cek = mantanPerbekelForm.safeParse(mantanPerbekel.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ mantanPerbekel.create.loading = true;
+ const res = await ApiFetch.api.desa.mantanperbekel["create"].post(
+ mantanPerbekel.create.form
+ );
+ if (res.status === 200) {
+ mantanPerbekel.findMany.load();
+ return toast.success("Foto berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan foto");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ mantanPerbekel.create.loading = false;
+ }
+ },
+ resetForm() {
+ mantanPerbekel.create.form = { ...mantanPerbekelDefaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.PerbekelDariMasaKeMasaGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ mantanPerbekel.findMany.loading = true; // ✅ Akses langsung via nama path
+ mantanPerbekel.findMany.page = page;
+ mantanPerbekel.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.desa.mantanperbekel["findMany"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ mantanPerbekel.findMany.data = res.data.data ?? [];
+ mantanPerbekel.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ mantanPerbekel.findMany.data = [];
+ mantanPerbekel.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch mantan perbekel paginated:", err);
+ mantanPerbekel.findMany.data = [];
+ mantanPerbekel.findMany.totalPages = 1;
+ } finally {
+ mantanPerbekel.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PerbekelDariMasaKeMasaGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/desa/mantanperbekel/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ mantanPerbekel.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch mantan perbekel:", res.statusText);
+ mantanPerbekel.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching mantan perbekel:", error);
+ mantanPerbekel.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ mantanPerbekel.delete.loading = true;
+ const response = await fetch(`/api/desa/mantanperbekel/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(result.message || "Mantan perbekel berhasil dihapus");
+ await mantanPerbekel.findMany.load(); // refresh list
+ } else {
+ toast.error(result.message || "Gagal menghapus mantan perbekel");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus mantan perbekel");
+ } finally {
+ mantanPerbekel.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...mantanPerbekelDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/desa/mantanperbekel/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ daerah: data.daerah,
+ periode: data.periode,
+ imageId: data.imageId || "",
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading foto:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = mantanPerbekelForm.safeParse(mantanPerbekel.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+ try {
+ mantanPerbekel.update.loading = true;
+ const response = await fetch(`/api/desa/mantanperbekel/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ daerah: this.form.daerah,
+ periode: this.form.periode,
+ imageId: this.form.imageId,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Mantan perbekel berhasil diupdate");
+ await mantanPerbekel.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate mantan perbekel");
+ }
+ } catch (error) {
+ console.error("Error updating mantan perbekel:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate mantan perbekel"
+ );
+ return false;
+ } finally {
+ mantanPerbekel.update.loading = false;
+ }
+ },
+ reset() {
+ mantanPerbekel.update.id = "";
+ mantanPerbekel.update.form = { ...mantanPerbekelDefaultForm };
+ },
+ },
+});
+
+const stateProfileDesa = proxy({
+ lambangDesa,
+ maskotDesa,
+ profilPerbekel,
+ visiMisiDesa,
+ sejarahDesa,
+ mantanPerbekel,
+});
+
+export default stateProfileDesa;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts
new file mode 100644
index 00000000..fd3dd897
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts
@@ -0,0 +1,949 @@
+/* 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 templateApbDesa = z.object({
+ tahun: z.number().min(4, "Tahun minimal 4 karakter"),
+ pembiayaanIds: z
+ .array(z.string().uuid())
+ .nonempty("Pilih minimal 1 pembiayaan"),
+ belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"),
+ pendapatanIds: z
+ .array(z.string().uuid())
+ .nonempty("Pilih minimal 1 pendapatan"),
+});
+
+const ApbDesaDefaultForm = {
+ tahun: 0,
+ pendapatanIds: [] as string[],
+ belanjaIds: [] as string[],
+ pembiayaanIds: [] as string[],
+};
+
+const ApbDesa = proxy({
+ create: {
+ form: { ...ApbDesaDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateApbDesa.safeParse(this.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((v) => v.message).join("\n");
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
+ "create"
+ ].post(this.form);
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan APB Desa");
+ ApbDesa.findMany.load();
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal menambahkan APB Desa");
+ }
+ } catch (error) {
+ console.error("Create error:", error);
+ toast.error("Gagal menambahkan APB Desa");
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.form = { ...ApbDesaDefaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.ApbDesaGetPayload<{
+ include: {
+ pendapatan: true;
+ belanja: true;
+ pembiayaan: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ ApbDesa.findMany.loading = true; // ✅ Akses langsung via nama path
+ ApbDesa.findMany.page = page;
+ ApbDesa.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ ApbDesa.findMany.data = res.data.data ?? [];
+ ApbDesa.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ ApbDesa.findMany.data = [];
+ ApbDesa.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch APB Desa paginated:", err);
+ ApbDesa.findMany.data = [];
+ ApbDesa.findMany.totalPages = 1;
+ } finally {
+ ApbDesa.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...ApbDesaDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/apbdesa/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error("Gagal mengambil APB Desa");
+ }
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.message || "Gagal memuat APB Desa");
+ }
+
+ const data = result.data;
+
+ this.id = id;
+ this.form = {
+ tahun: data.tahun || 0,
+ pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
+ belanjaIds: data.belanja?.map((b: any) => b.id) || [],
+ pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
+ };
+
+ return data;
+ } catch (error) {
+ console.error("Error loading APB Desa:", error);
+ toast.error("Gagal memuat data APB Desa");
+ return null;
+ }
+ },
+ async update() {
+ try {
+ this.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/apbdesa/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ }
+ );
+ if (!response.ok) {
+ throw new Error("Gagal memperbarui APB Desa");
+ }
+ const data = await response.json();
+ toast.success("APB Desa berhasil diperbarui");
+ return data;
+ } catch (error) {
+ console.error("Error updating APB Desa:", error);
+ toast.error("Gagal memperbarui APB Desa");
+ throw error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.id = "";
+ this.form = { ...ApbDesaDefaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ this.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/apbdesa/del/${id}`,
+ {
+ method: "DELETE",
+ }
+ );
+ if (!response.ok) {
+ throw new Error("Gagal menghapus APB Desa");
+ }
+ toast.success("APB Desa berhasil dihapus");
+ return true;
+ } catch (error) {
+ console.error("Error deleting APB Desa:", error);
+ toast.error("Gagal menghapus APB Desa");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ApbDesaGetPayload<{
+ include: { pendapatan: true; belanja: true; pembiayaan: true };
+ }> | null,
+
+ async load(id: string) {
+ try {
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/apbdesa/${id}`
+ );
+ if (!response.ok) {
+ throw new Error("Gagal mengambil detail APB Desa");
+ }
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.message || "Gagal mengambil data");
+ }
+
+ this.data = result.data; // ✅ fix utama di sini
+ return result.data;
+ } catch (error) {
+ console.error("Error loading APB Desa detail:", error);
+ toast.error("Gagal memuat detail APB Desa");
+ return null;
+ }
+ },
+ },
+});
+
+const templatePendapatan = z.object({
+ name: z.string().min(2, "Nama harus diisi"),
+ value: z.number().int().positive("Nilai harus angka positif"),
+});
+
+const PendapatanDefaultForm = {
+ name: "",
+ value: 0,
+};
+
+const pendapatan = proxy({
+ create: {
+ form: { ...PendapatanDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templatePendapatan.safeParse(this.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((v) => v.message).join("\n");
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const res =
+ await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
+ "create"
+ ].post(this.form);
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan Pendapatan Asli");
+ pendapatan.findMany.load();
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal menambahkan Pendapatan Asli");
+ }
+ } catch (error) {
+ console.error("Create error:", error);
+ toast.error("Gagal menambahkan Pendapatan Asli");
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.form = { ...PendapatanDefaultForm };
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ pendapatan.findMany.loading = true; // ✅ Akses langsung via nama path
+ pendapatan.findMany.page = page;
+ pendapatan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pendapatan.findMany.data = res.data.data ?? [];
+ pendapatan.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ pendapatan.findMany.data = [];
+ pendapatan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch pendapatan asli desa paginated:", err);
+ pendapatan.findMany.data = [];
+ pendapatan.findMany.totalPages = 1;
+ } finally {
+ pendapatan.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...PendapatanDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ value: data.value,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading pendapatan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templatePendapatan.safeParse(pendapatan.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ pendapatan.update.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ value: this.form.value,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update pendapatan");
+ await pendapatan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate pendapatan");
+ }
+ } catch (error) {
+ console.error("Error updating pendapatan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate pendapatan"
+ );
+ return false;
+ } finally {
+ pendapatan.update.loading = false;
+ }
+ },
+ reset() {
+ pendapatan.update.id = "";
+ pendapatan.update.form = { ...PendapatanDefaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pendapatan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pendapatanasli/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Pendapatan Asli berhasil dihapus");
+ await pendapatan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus Pendapatan Asli");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Pendapatan Asli");
+ } finally {
+ pendapatan.delete.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PendapatanGetPayload<{
+ select: { isActive: boolean };
+ }> | null,
+ async load(id: string) {
+ const res = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`
+ );
+ if (res.ok) {
+ const json = await res.json();
+ pendapatan.findUnique.data = json.data
+ ? {
+ ...json.data,
+ isActive: json.data.isActive ?? true, // Fallback ke aktif:true jika tidak ada data
+ }
+ : null;
+ } else {
+ pendapatan.findUnique.data = null;
+ }
+ },
+ },
+});
+
+const templateBelanja = z.object({
+ name: z.string().min(2, "Nama harus diisi"),
+ value: z.number().int().positive("Nilai harus angka positif"),
+});
+
+const BelanjaDefaultForm = {
+ name: "",
+ value: 0,
+};
+
+const belanja = proxy({
+ create: {
+ form: { ...BelanjaDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateBelanja.safeParse(this.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((v) => v.message).join("\n");
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
+ "create"
+ ].post(this.form);
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan Belanja");
+ belanja.findMany.load();
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal menambahkan Belanja");
+ }
+ } catch (error) {
+ console.error("Create error:", error);
+ toast.error("Gagal menambahkan Belanja");
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.form = { ...BelanjaDefaultForm };
+ },
+ },
+ findMany: {
+ data: [] as Array<{
+ id: string;
+ name: string;
+ value: number;
+ }>,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ belanja.findMany.loading = true; // ✅ Akses langsung via nama path
+ belanja.findMany.page = page;
+ belanja.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ belanja.findMany.data = res.data.data ?? [];
+ belanja.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ belanja.findMany.data = [];
+ belanja.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch Belanja paginated:", err);
+ belanja.findMany.data = [];
+ belanja.findMany.totalPages = 1;
+ } finally {
+ belanja.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...BelanjaDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/belanja/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ value: data.value,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading belanja:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateBelanja.safeParse(belanja.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ belanja.update.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ value: this.form.value,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update belanja");
+ await belanja.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate belanja");
+ }
+ } catch (error) {
+ console.error("Error updating belanja:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate belanja"
+ );
+ return false;
+ } finally {
+ belanja.update.loading = false;
+ }
+ },
+ reset() {
+ belanja.update.id = "";
+ belanja.update.form = { ...BelanjaDefaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ belanja.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/belanja/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Belanja berhasil dihapus");
+ await belanja.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus Belanja");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Belanja");
+ } finally {
+ belanja.delete.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.BelanjaGetPayload<{
+ select: { isActive: boolean };
+ }> | null,
+ async load(id: string) {
+ const res = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`);
+ if (res.ok) {
+ const json = await res.json();
+ belanja.findUnique.data = json.data
+ ? {
+ ...json.data,
+ isActive: json.data.isActive ?? true, // Fallback ke aktif:true jika tidak ada data
+ }
+ : null;
+ } else {
+ belanja.findUnique.data = null;
+ }
+ },
+ },
+});
+
+const templatePembiayaan = z.object({
+ name: z.string().min(2, "Nama harus diisi"),
+ value: z.number().int().positive("Nilai harus angka positif"),
+});
+
+const PembiayaanDefaultForm = {
+ name: "",
+ value: 0,
+};
+
+const pembiayaan = proxy({
+ create: {
+ form: { ...PembiayaanDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templatePembiayaan.safeParse(this.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((v) => v.message).join("\n");
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
+ "create"
+ ].post(this.form);
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan Pembiayaan");
+ pembiayaan.findMany.load();
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal menambahkan Pembiayaan");
+ }
+ } catch (error) {
+ console.error("Create error:", error);
+ toast.error("Gagal menambahkan Pembiayaan");
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.form = { ...PembiayaanDefaultForm };
+ },
+ },
+ findMany: {
+ data: [] as Array<{
+ id: string;
+ name: string;
+ value: number;
+ }>,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ pembiayaan.findMany.loading = true; // ✅ Akses langsung via nama path
+ pembiayaan.findMany.page = page;
+ pembiayaan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pembiayaan.findMany.data = res.data.data ?? [];
+ pembiayaan.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ pembiayaan.findMany.data = [];
+ pembiayaan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch Pembiayaan paginated:", err);
+ pembiayaan.findMany.data = [];
+ pembiayaan.findMany.totalPages = 1;
+ } finally {
+ pembiayaan.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...PembiayaanDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ value: data.value,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading pembiayaan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templatePembiayaan.safeParse(pembiayaan.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ pembiayaan.update.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ value: this.form.value,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update pembiayaan");
+ await pembiayaan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate pembiayaan");
+ }
+ } catch (error) {
+ console.error("Error updating pembiayaan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate pembiayaan"
+ );
+ return false;
+ } finally {
+ pembiayaan.update.loading = false;
+ }
+ },
+ reset() {
+ pembiayaan.update.id = "";
+ pembiayaan.update.form = { ...PembiayaanDefaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pembiayaan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pembiayaan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Pembiayaan berhasil dihapus");
+ await pembiayaan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus Pembiayaan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Pembiayaan");
+ } finally {
+ pembiayaan.delete.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PembiayaanGetPayload<{
+ select: { isActive: boolean };
+ }> | null,
+ async load(id: string) {
+ const res = await fetch(
+ `/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`
+ );
+ if (res.ok) {
+ const json = await res.json();
+ pembiayaan.findUnique.data = json.data
+ ? {
+ ...json.data,
+ isActive: json.data.isActive ?? true, // Fallback ke aktif:true jika tidak ada data
+ }
+ : null;
+ } else {
+ pembiayaan.findUnique.data = null;
+ }
+ },
+ },
+});
+
+const PendapatanAsliDesa = proxy({
+ ApbDesa,
+ belanja,
+ pembiayaan,
+ pendapatan,
+});
+
+export default PendapatanAsliDesa;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts b/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts
new file mode 100644
index 00000000..bf29247b
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts
@@ -0,0 +1,223 @@
+/* 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 templateDemografiPekerjaan = z.object({
+ pekerjaan: z.string().min(1, "Pekerjaan harus diisi"),
+ lakiLaki: z.number().min(1, "Laki - Laki harus diisi"),
+ perempuan: z.number().min(1, "Perempuan harus diisi"),
+});
+
+type DemografiPekerjaan = Prisma.DataDemografiPekerjaanGetPayload<{
+ select: {
+ pekerjaan: true;
+ lakiLaki: true;
+ perempuan: true;
+ };
+}>;
+
+const defaultForm: DemografiPekerjaan = {
+ pekerjaan: "",
+ lakiLaki: 0,
+ perempuan: 0,
+};
+
+const demografiPekerjaan = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templateDemografiPekerjaan.safeParse(
+ demografiPekerjaan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ try {
+ demografiPekerjaan.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.demografipekerjaan[
+ "create"
+ ].post(demografiPekerjaan.create.form);
+
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ demografiPekerjaan.create.form = { ...defaultForm };
+ demografiPekerjaan.findMany.load();
+ return id;
+ }
+ }
+ toast.error("failed create");
+ return null;
+ } catch (error) {
+ console.log((error as Error).message);
+ return null;
+ } finally {
+ demografiPekerjaan.create.loading = false;
+ }
+ },
+ },
+
+ findMany: {
+ data: null as
+ | Prisma.DataDemografiPekerjaanGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ demografiPekerjaan.findMany.loading = true; // ✅ Akses langsung via nama path
+ demografiPekerjaan.findMany.page = page;
+ demografiPekerjaan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.demografipekerjaan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ demografiPekerjaan.findMany.data = res.data.data ?? [];
+ demografiPekerjaan.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ demografiPekerjaan.findMany.data = [];
+ demografiPekerjaan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch demografi pekerjaan paginated:", err);
+ demografiPekerjaan.findMany.data = [];
+ demografiPekerjaan.findMany.totalPages = 1;
+ } finally {
+ demografiPekerjaan.findMany.loading = false;
+ }
+ },
+ },
+
+ findUnique: {
+ data: null as Prisma.DataDemografiPekerjaanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/demografipekerjaan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ demografiPekerjaan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch demografiPekerjaan:", res.statusText);
+ demografiPekerjaan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching demografiPekerjaan:", error);
+ demografiPekerjaan.findUnique.data = null;
+ }
+ },
+ },
+
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ const formData = {
+ pekerjaan: this.form.pekerjaan,
+ lakiLaki: this.form.lakiLaki,
+ perempuan: this.form.perempuan,
+ };
+
+ const cek = templateDemografiPekerjaan.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const res = await fetch(`/api/ekonomi/demografipekerjaan/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const result = await res.json();
+
+ if (!res.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+ await demografiPekerjaan.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Update error:", error);
+ toast.error("Gagal update data demografi pekerjaan");
+ throw error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ demografiPekerjaan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/demografipekerjaan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Demografi pekerjaan berhasil dihapus"
+ );
+ await demografiPekerjaan.findMany.load();
+ } else {
+ toast.error(result?.message || "Gagal menghapus demografi pekerjaan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus persentase kelahiran");
+ } finally {
+ demografiPekerjaan.delete.loading = false;
+ }
+ },
+ },
+});
+export default demografiPekerjaan;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts
new file mode 100644
index 00000000..2eb11a03
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts
@@ -0,0 +1,216 @@
+/* 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 templateJumlahPendudukMiskin = z.object({
+ year: z.number().min(1, "Data tahun harus diisi"),
+ totalPoorPopulation: z
+ .number()
+ .min(1, "Data total penduduk miskin harus diisi"),
+});
+
+type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{
+ select: {
+ id: true;
+ year: true;
+ totalPoorPopulation: true;
+ };
+}>;
+
+const defaultForm: Omit & { id?: string } = {
+ year: 0,
+ totalPoorPopulation: 0,
+};
+
+const jumlahPendudukMiskin = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templateJumlahPendudukMiskin.safeParse(
+ jumlahPendudukMiskin.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ jumlahPendudukMiskin.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
+ "create"
+ ].post(jumlahPendudukMiskin.create.form);
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ jumlahPendudukMiskin.create.form = {
+ year: 0,
+ totalPoorPopulation: 0,
+ };
+ jumlahPendudukMiskin.findMany.load();
+ return id;
+ }
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ jumlahPendudukMiskin.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.GrafikJumlahPendudukMiskinGetPayload<{
+ select: { id: true; year: true; totalPoorPopulation: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ jumlahPendudukMiskin.findMany.loading = true; // ✅ Akses langsung via nama path
+ jumlahPendudukMiskin.findMany.page = page;
+ jumlahPendudukMiskin.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ jumlahPendudukMiskin.findMany.data = res.data.data ?? [];
+ jumlahPendudukMiskin.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ jumlahPendudukMiskin.findMany.data = [];
+ jumlahPendudukMiskin.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch jumlah penduduk miskin paginated:", err);
+ jumlahPendudukMiskin.findMany.data = [];
+ jumlahPendudukMiskin.findMany.totalPages = 1;
+ } finally {
+ jumlahPendudukMiskin.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
+ select: { id: true; year: true; totalPoorPopulation: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/jumlahpendudukmiskin/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ jumlahPendudukMiskin.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ jumlahPendudukMiskin.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik jumlah penduduk miskin:", error);
+ jumlahPendudukMiskin.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateJumlahPendudukMiskin.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => (v.path as string[]).join("."))
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/ekonomi/jumlahpendudukmiskin/${id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ }
+ );
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await jumlahPendudukMiskin.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error(
+ "Error update data grafik jumlah penduduk miskin:",
+ error
+ );
+ toast.error("Gagal update data grafik jumlah penduduk miskin");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ jumlahPendudukMiskin.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/jumlahpendudukmiskin/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Grafik jumlah penduduk miskin berhasil dihapus"
+ );
+ await jumlahPendudukMiskin.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus grafik jumlah penduduk miskin"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete grafik jumlah penduduk miskin:", error);
+ toast.error(
+ "Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin"
+ );
+ } finally {
+ jumlahPendudukMiskin.delete.loading = false;
+ }
+ },
+ },
+});
+export default jumlahPendudukMiskin;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts
new file mode 100644
index 00000000..c45ac24a
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts
@@ -0,0 +1,276 @@
+/* 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 templateJumlahPengngguran = z.object({
+ month: z.string().min(1, "Bulan harus diisi"),
+ year: z.number().min(1, "Tahun harus diisi"),
+ totalUnemployment: z.number().min(1, "Total pengangguran harus diisi"),
+ educatedUnemployment: z
+ .number()
+ .min(1, "Pengangguran pendidikan harus diisi"),
+ uneducatedUnemployment: z
+ .number()
+ .min(1, "Pengangguran tidak pendidikan harus diisi"),
+ percentageChange: z.number({ invalid_type_error: "Persentase perubahan harus angka" }),
+
+});
+
+type JumlahPengangguran = {
+ month: string;
+ year: number;
+ totalUnemployment: number;
+ educatedUnemployment: number;
+ uneducatedUnemployment: number;
+ percentageChange: number;
+};
+
+const jumlahPengangguranForm: JumlahPengangguran = {
+ month: "",
+ year: new Date().getFullYear(), // Default to current year
+ totalUnemployment: 0,
+ educatedUnemployment: 0,
+ uneducatedUnemployment: 0,
+ percentageChange: 0,
+};
+
+const jumlahPengangguran = proxy({
+ findByMonthYear: {
+ data: null as any,
+ loading: false,
+ load: async ({ month, year }: { month: string; year: number }) => {
+ jumlahPengangguran.findByMonthYear.loading = true;
+ try {
+ const res = await fetch(
+ `/api/ekonomi/jumlahpengangguran/detaildatapengangguran/month/${month}/year/${year}`
+ );
+ const json = await res.json();
+ jumlahPengangguran.findByMonthYear.data = json.data;
+ return json.data;
+ } catch (err) {
+ console.error("Gagal ambil data bulan/tahun:", err);
+ } finally {
+ jumlahPengangguran.findByMonthYear.loading = false;
+ }
+ },
+ },
+ create: {
+ form: jumlahPengangguranForm,
+ loading: false,
+ async create() {
+ // Ensure all number fields are actual numbers
+ const formData = {
+ ...jumlahPengangguran.create.form,
+ year: Number(jumlahPengangguran.create.form.year) || new Date().getFullYear(),
+ totalUnemployment: Number(jumlahPengangguran.create.form.totalUnemployment) || 0,
+ educatedUnemployment: Number(jumlahPengangguran.create.form.educatedUnemployment) || 0,
+ uneducatedUnemployment: Number(jumlahPengangguran.create.form.uneducatedUnemployment) || 0,
+ percentageChange: Number(jumlahPengangguran.create.form.percentageChange) || 0,
+ };
+
+ const cek = templateJumlahPengngguran.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")} (${v.message})`)
+ .join("\n")}]`;
+ toast.error(err);
+ return null;
+ }
+ try {
+ jumlahPengangguran.create.loading = true;
+ const res =
+ await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
+ "create"
+ ].post(jumlahPengangguran.create.form);
+
+ if (res.status === 200) {
+ const id = res.data?.id;
+ if (id) {
+ toast.success("Success create");
+ jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
+ jumlahPengangguran.findMany.load();
+ return id;
+ }
+ }
+ toast.error("failed create");
+ return null;
+ } catch (error) {
+ console.log((error as Error).message);
+ return null;
+ } finally {
+ jumlahPengangguran.create.loading = false;
+ }
+ },
+ },
+
+ findMany: {
+ data: null as
+ | Prisma.DetailDataPengangguranGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ jumlahPengangguran.findMany.loading = true; // ✅ Akses langsung via nama path
+ jumlahPengangguran.findMany.page = page;
+ jumlahPengangguran.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ jumlahPengangguran.findMany.data = res.data.data ?? [];
+ jumlahPengangguran.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ jumlahPengangguran.findMany.data = [];
+ jumlahPengangguran.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch jumlah pengangguran paginated:", err);
+ jumlahPengangguran.findMany.data = [];
+ jumlahPengangguran.findMany.totalPages = 1;
+ } finally {
+ jumlahPengangguran.findMany.loading = false;
+ }
+ },
+ },
+
+ findUnique: {
+ data: null as Prisma.DetailDataPengangguranGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ekonomi/jumlahpengangguran/detaildatapengangguran/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ jumlahPengangguran.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch jumlahPengangguran:", res.statusText);
+ jumlahPengangguran.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching jumlahPengangguran:", error);
+ jumlahPengangguran.findUnique.data = null;
+ }
+ },
+ },
+
+ update: {
+ id: "",
+ form: { ...jumlahPengangguranForm },
+ loading: false,
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ const formData = {
+ month: this.form.month,
+ year: this.form.year,
+ totalUnemployment: this.form.totalUnemployment,
+ educatedUnemployment: this.form.educatedUnemployment,
+ uneducatedUnemployment: this.form.uneducatedUnemployment,
+ percentageChange: this.form.percentageChange,
+ };
+
+ const cek = templateJumlahPengngguran.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const res = await fetch(
+ `/api/ekonomi/jumlahpengangguran/detaildatapengangguran/${id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ }
+ );
+
+ const result = await res.json();
+
+ if (!res.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+ await jumlahPengangguran.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Update error:", error);
+ toast.error("Gagal update data jumlah pengangguran");
+ throw error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ jumlahPengangguran.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/jumlahpengangguran/detaildatapengangguran/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Jumlah pengangguran berhasil dihapus"
+ );
+ await jumlahPengangguran.findMany.load();
+ } else {
+ toast.error(result?.message || "Gagal menghapus jumlah pengangguran");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus jumlah pengangguran");
+ } finally {
+ jumlahPengangguran.delete.loading = false;
+ }
+ },
+ },
+});
+
+const jumlahPengangguranState = proxy({
+ jumlahPengangguran,
+});
+
+export default jumlahPengangguranState;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts
new file mode 100644
index 00000000..227c6b8c
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts
@@ -0,0 +1,256 @@
+/* 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({
+ posisi: z.string(),
+ namaPerusahaan: z.string(),
+ lokasi: z.string(),
+ tipePekerjaan: z.string(),
+ gaji: z.string(),
+ deskripsi: z.string(),
+ kualifikasi: z.string(),
+ notelp: z.string(),
+});
+
+const defaultForm = {
+ posisi: "",
+ namaPerusahaan: "",
+ lokasi: "",
+ tipePekerjaan: "",
+ gaji: "",
+ deskripsi: "",
+ kualifikasi: "",
+ notelp: "",
+};
+
+const lowonganKerjaState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(lowonganKerjaState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ lowonganKerjaState.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.lowongankerja["create"].post(
+ lowonganKerjaState.create.form
+ );
+ if (res.status === 200) {
+ lowonganKerjaState.create.loading = false;
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ lowonganKerjaState.create.loading = false;
+ }
+ },
+ resetForm() {
+ lowonganKerjaState.create.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.LowonganPekerjaanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ lowonganKerjaState.findMany.loading = true; // ✅ Akses langsung via nama path
+ lowonganKerjaState.findMany.page = page;
+ lowonganKerjaState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ lowonganKerjaState.findMany.data = res.data.data ?? [];
+ lowonganKerjaState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ lowonganKerjaState.findMany.data = [];
+ lowonganKerjaState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch lowongan kerja paginated:", err);
+ lowonganKerjaState.findMany.data = [];
+ lowonganKerjaState.findMany.totalPages = 1;
+ } finally {
+ lowonganKerjaState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.LowonganPekerjaanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/lowongankerja/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ lowonganKerjaState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ lowonganKerjaState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ lowonganKerjaState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ lowonganKerjaState.delete.loading = true;
+ const response = await fetch(`/api/ekonomi/lowongankerja/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Lowongan kerja berhasil dihapus");
+ await lowonganKerjaState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus lowongan kerja");
+ }
+ } catch (error) {
+ console.error("Error deleting data:", error);
+ toast.error("Terjadi kesalahan saat menghapus lowongan kerja");
+ } finally {
+ lowonganKerjaState.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/ekonomi/lowongankerja/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ posisi: data.posisi,
+ namaPerusahaan: data.namaPerusahaan,
+ lokasi: data.lokasi,
+ tipePekerjaan: data.tipePekerjaan,
+ gaji: data.gaji,
+ deskripsi: data.deskripsi,
+ kualifikasi: data.kualifikasi,
+ notelp: data.notelp,
+ };
+ return data;
+ } else {
+ throw new Error(
+ result?.message || "Gagal memuat data lowongan kerja"
+ );
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(lowonganKerjaState.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ lowonganKerjaState.update.loading = true;
+ const response = await fetch(`/api/ekonomi/lowongankerja/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ posisi: this.form.posisi,
+ namaPerusahaan: this.form.namaPerusahaan,
+ lokasi: this.form.lokasi,
+ tipePekerjaan: this.form.tipePekerjaan,
+ gaji: this.form.gaji,
+ deskripsi: this.form.deskripsi,
+ kualifikasi: this.form.kualifikasi,
+ notelp: this.form.notelp,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update lowongan kerja");
+ await lowonganKerjaState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update lowongan kerja");
+ }
+ } catch (error) {
+ console.error("Error updating data:", error);
+ toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate lowongan kerja");
+ return false;
+ } finally {
+ lowonganKerjaState.update.loading = false;
+ }
+ },
+ reset() {
+ lowonganKerjaState.update.id = "";
+ lowonganKerjaState.update.form = { ...defaultForm };
+ },
+ },
+});
+
+export default lowonganKerjaState;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts b/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts
new file mode 100644
index 00000000..21d8e7b4
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts
@@ -0,0 +1,559 @@
+/* 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 templatePasarDesaForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+ harga: z.number().min(1, "Harga minimal 1"),
+ alamatUsaha: z.string().min(1, "Alamat minimal 1 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+ rating: z.number().min(1, "Rating minimal 1"),
+ kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
+ kontak: z.string().min(1, "Kontak wajib diisi"),
+});
+
+const defaultPasarDesaForm = {
+ nama: "",
+ harga: 0,
+ alamatUsaha: "",
+ imageId: "",
+ rating: 0,
+ kategoriId: [] as string[],
+ kontak: "",
+};
+
+const pasarDesa = proxy({
+ create: {
+ form: { ...defaultPasarDesaForm },
+ loading: false,
+ async create() {
+ const cek = templatePasarDesaForm.safeParse(pasarDesa.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pasarDesa.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.pasardesa["create"].post(
+ pasarDesa.create.form
+ );
+ if (res.status === 200) {
+ pasarDesa.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ pasarDesa.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.PasarDesaGetPayload<{
+ include: {
+ image: true;
+ KategoriToPasar: {
+ include: {
+ kategori: true;
+ };
+ };
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", categoryId?: string) => {
+ pasarDesa.findMany.loading = true;
+ pasarDesa.findMany.page = page;
+ pasarDesa.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (categoryId) query.categoryId = categoryId;
+
+ const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pasarDesa.findMany.data = res.data.data ?? [];
+ pasarDesa.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ pasarDesa.findMany.data = [];
+ pasarDesa.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch keamanan lingkungan paginated:", err);
+ pasarDesa.findMany.data = [];
+ pasarDesa.findMany.totalPages = 1;
+ } finally {
+ pasarDesa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PasarDesaGetPayload<{
+ include: {
+ image: true;
+ KategoriToPasar: {
+ include: {
+ kategori: true;
+ };
+ };
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/pasardesa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ pasarDesa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ pasarDesa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ pasarDesa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pasarDesa.delete.loading = true;
+
+ const response = await fetch(`/api/ekonomi/pasardesa/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Pasar desa berhasil dihapus");
+ await pasarDesa.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus pasar desa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pasar desa");
+ } finally {
+ pasarDesa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultPasarDesaForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/ekonomi/pasardesa/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ harga: data.harga,
+ alamatUsaha: data.alamatUsaha,
+ imageId: data.imageId,
+ rating: data.rating,
+ kategoriId: data.kategoriId,
+ kontak: data.kontak,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading pasar desa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templatePasarDesaForm.safeParse(pasarDesa.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ pasarDesa.edit.loading = true;
+ const response = await fetch(`/api/ekonomi/pasardesa/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ harga: this.form.harga,
+ alamatUsaha: this.form.alamatUsaha,
+ imageId: this.form.imageId,
+ rating: this.form.rating,
+ kategoriId: this.form.kategoriId,
+ kontak: this.form.kontak,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update pasar desa");
+ await pasarDesa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate pasar desa");
+ }
+ } catch (error) {
+ console.error("Error updating pasar desa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate pasar desa"
+ );
+ return false;
+ } finally {
+ pasarDesa.edit.loading = false;
+ }
+ },
+ reset() {
+ pasarDesa.edit.id = "";
+ pasarDesa.edit.form = { ...defaultPasarDesaForm };
+ },
+ },
+});
+
+// ========================================= KATEGORI PRODUK ========================================= //
+const kategoriProdukForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+});
+
+const kategoriProdukDefaultForm = {
+ nama: "",
+};
+
+const kategoriProduk = proxy({
+ create: {
+ form: { ...kategoriProdukDefaultForm },
+ loading: false,
+ async create() {
+ const cek = kategoriProdukForm.safeParse(kategoriProduk.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kategoriProduk.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.kategoriproduk["create"].post(
+ kategoriProduk.create.form
+ );
+ if (res.status === 200) {
+ kategoriProduk.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ kategoriProduk.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.KategoriProdukGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search2: "",
+ load: async (page = 1, limit = 10, search2 = "") => {
+ kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path
+ kategoriProduk.findMany.page = page;
+ kategoriProduk.findMany.search2 = search2;
+
+ try {
+ const query: any = { page, limit };
+ if (search2) query.search2 = search2;
+
+ const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriProduk.findMany.data = res.data.data ?? [];
+ kategoriProduk.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kategoriProduk.findMany.data = [];
+ kategoriProduk.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori produk paginated:", err);
+ kategoriProduk.findMany.data = [];
+ kategoriProduk.findMany.totalPages = 1;
+ } finally {
+ kategoriProduk.findMany.loading = false;
+ }
+ },
+ },
+ // ✅ Versi findManyAll (ambil semua tanpa pagination)
+ findManyAll: {
+ data: null as
+ | Prisma.KategoriProdukGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ kategoriProduk.findManyAll.loading = true;
+ kategoriProduk.findManyAll.search = search;
+
+ try {
+ const query: any = {};
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many-all"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriProduk.findManyAll.data = res.data.data ?? [];
+ } else {
+ kategoriProduk.findManyAll.data = [];
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori produk (all):", err);
+ kategoriProduk.findManyAll.data = [];
+ } finally {
+ kategoriProduk.findManyAll.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriProdukGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/kategoriproduk/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kategoriProduk.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriProduk.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriProduk.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriProduk.delete.loading = true;
+
+ const response = await fetch(`/api/ekonomi/kategoriproduk/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kategori produk berhasil dihapus");
+ await kategoriProduk.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kategori produk");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kategori produk");
+ } finally {
+ kategoriProduk.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...kategoriProdukDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/ekonomi/kategoriproduk/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori produk:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = kategoriProdukForm.safeParse(kategoriProduk.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriProduk.edit.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/kategoriproduk/${kategoriProduk.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: kategoriProduk.edit.form.nama,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate kategori produk (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui kategori produk"
+ );
+ await kategoriProduk.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate kategori produk"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating kategori produk:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kategori produk"
+ );
+ return false;
+ } finally {
+ kategoriProduk.edit.loading = false;
+ }
+ },
+ reset() {
+ kategoriProduk.edit.id = "";
+ kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
+ },
+ },
+});
+
+const pasarDesaState = proxy({
+ pasarDesa,
+ kategoriProduk,
+});
+export default pasarDesaState;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts b/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts
new file mode 100644
index 00000000..1332bbea
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts
@@ -0,0 +1,272 @@
+/* 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 minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ icon: z.string().min(1, "Icon minimal 1 karakter"),
+ statistik: z.object({
+ tahun: z.string().min(1, "Tahun minimal 1 karakter"),
+ jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
+ }),
+});
+
+const defaultForm = {
+ nama: "",
+ deskripsi: "",
+ icon: "",
+ statistik: {
+ tahun: "",
+ jumlah: "",
+ },
+};
+
+const programKemiskinanState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(programKemiskinanState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ programKemiskinanState.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.programkemiskinan["create"].post(
+ programKemiskinanState.create.form
+ );
+ if (res.status === 200) {
+ programKemiskinanState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ programKemiskinanState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.ProgramKemiskinanGetPayload<{
+ include: {
+ statistik: true;
+ };
+ }>[],
+ loading: false,
+ page: 1,
+ totalPages: 1,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ programKemiskinanState.findMany.loading = true; // ✅ Akses langsung via nama path
+ programKemiskinanState.findMany.page = page;
+ programKemiskinanState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.programkemiskinan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ programKemiskinanState.findMany.data = res.data.data ?? [];
+ programKemiskinanState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ programKemiskinanState.findMany.data = [];
+ programKemiskinanState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch program kemiskinan paginated:", err);
+ programKemiskinanState.findMany.data = [];
+ programKemiskinanState.findMany.totalPages = 1;
+ } finally {
+ programKemiskinanState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ProgramKemiskinanGetPayload<{
+ include: {
+ statistik: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/programkemiskinan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ programKemiskinanState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ programKemiskinanState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ programKemiskinanState.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/ekonomi/programkemiskinan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ deskripsi: data.deskripsi,
+ icon: data.icon,
+ statistik: {
+ tahun: data.statistik.tahun,
+ jumlah: data.statistik.jumlah,
+ },
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading program kemiskinan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(programKemiskinanState.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ programKemiskinanState.update.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/programkemiskinan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ deskripsi: this.form.deskripsi,
+ icon: this.form.icon,
+ statistik: {
+ tahun: this.form.statistik.tahun,
+ jumlah: this.form.statistik.jumlah,
+ },
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update program kemiskinan");
+ await programKemiskinanState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update program kemiskinan");
+ }
+ } catch (error) {
+ console.error("Error updating program kemiskinan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update program kemiskinan"
+ );
+ return false;
+ } finally {
+ programKemiskinanState.update.loading = false;
+ }
+ },
+ reset() {
+ programKemiskinanState.update.id = "";
+ programKemiskinanState.update.form = { ...defaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ programKemiskinanState.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/programkemiskinan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Program kemiskinan berhasil dihapus"
+ );
+ await programKemiskinanState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus program kemiskinan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus program kemiskinan");
+ } finally {
+ programKemiskinanState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default programKemiskinanState;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts b/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts
new file mode 100644
index 00000000..341398f0
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts
@@ -0,0 +1,227 @@
+/* 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 templateGrafikSektorUnggulan = z.object({
+ name: z.string().min(2, "Nama harus diisi"),
+ description: z.string().min(2, "Deskripsi harus diisi"),
+ value: z.number().min(1, "Nilai harus diisi"),
+});
+
+interface SektorUnggulanForm {
+ id?: string;
+ name: string;
+ description: string;
+ value: number;
+}
+
+const defaultForm: SektorUnggulanForm = {
+ name: "",
+ description: "",
+ value: 0,
+};
+
+const grafikSektorUnggulan = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templateGrafikSektorUnggulan.safeParse(
+ grafikSektorUnggulan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ grafikSektorUnggulan.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
+ "create"
+ ].post(grafikSektorUnggulan.create.form);
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ grafikSektorUnggulan.create.form = {
+ name: "",
+ description: "",
+ value: 0,
+ };
+ grafikSektorUnggulan.findMany.load();
+ return id;
+ }
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ grafikSektorUnggulan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.SektorUnggulanDesaGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ description: true;
+ value: true;
+ createdAt: true;
+ updatedAt: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ grafikSektorUnggulan.findMany.loading = true; // ✅ Akses langsung via nama path
+ grafikSektorUnggulan.findMany.page = page;
+ grafikSektorUnggulan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikSektorUnggulan.findMany.data = res.data.data ?? [];
+ grafikSektorUnggulan.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ grafikSektorUnggulan.findMany.data = [];
+ grafikSektorUnggulan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch sektor unggulan desa paginated:", err);
+ grafikSektorUnggulan.findMany.data = [];
+ grafikSektorUnggulan.findMany.totalPages = 1;
+ } finally {
+ grafikSektorUnggulan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.SektorUnggulanDesaGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ description: true;
+ value: true;
+ createdAt: true;
+ updatedAt: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ekonomi/sektourunggulandesa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ grafikSektorUnggulan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikSektorUnggulan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik sektor unggulan desa:", error);
+ grafikSektorUnggulan.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateGrafikSektorUnggulan.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/ekonomi/sektourunggulandesa/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await grafikSektorUnggulan.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik sektor unggulan desa");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikSektorUnggulan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/sektourunggulandesa/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Grafik sektor unggulan desa berhasil dihapus"
+ );
+ await grafikSektorUnggulan.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus grafik sektor unggulan desa"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error(
+ "Terjadi kesalahan saat menghapus grafik sektor unggulan desa"
+ );
+ } finally {
+ grafikSektorUnggulan.delete.loading = false;
+ }
+ },
+ },
+});
+export default grafikSektorUnggulan;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts b/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts
new file mode 100644
index 00000000..cef5c3fa
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts
@@ -0,0 +1,762 @@
+/* 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({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+});
+
+const defaultForm = {
+ name: "",
+ imageId: "",
+};
+
+type StrukturBumDesForm = Prisma.StrukturBumDesGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ imageId: true;
+ image?: {
+ select: {
+ link: true;
+ };
+ };
+ };
+}>;
+
+const stateStruktur = proxy({
+ struktur: {
+ data: null as StrukturBumDesForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ekonomi/struktur-organisasi/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(result.message || "Gagal mengambil data struktur");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Load struktur error:", errorMessage);
+ toast.error("Terjadi kesalahan saat mengambil data struktur");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+
+ editStruktur: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(strukturData: StrukturBumDesForm) {
+ this.id = strukturData.id;
+ this.isReadOnly = false;
+ this.form = {
+ name: strukturData.name || "",
+ imageId: strukturData.imageId || "",
+ };
+ },
+
+ updateField(field: keyof typeof defaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ async submit() {
+ const validation = templateForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ekonomi/struktur-organisasi/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update struktur");
+ await stateStruktur.struktur.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update struktur");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update struktur error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update struktur");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.id = "";
+ this.form = { ...defaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+
+ async loadForEdit(id: string) {
+ const strukturData = await this.struktur.load(id);
+ if (strukturData) {
+ this.editStruktur.initialize(strukturData);
+ }
+ return strukturData;
+ },
+
+ reset() {
+ this.struktur.reset();
+ this.editStruktur.reset();
+ },
+});
+
+const templatePosisiOrganisasi = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+ deskripsi: z.string().optional(),
+ hierarki: z.number().int().positive("Hierarki harus angka positif"),
+});
+
+const posisiOrganisasiDefaultForm = {
+ nama: "",
+ deskripsi: "",
+ hierarki: 0,
+};
+
+const posisiOrganisasi = proxy({
+ create: {
+ form: { ...posisiOrganisasiDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templatePosisiOrganisasi.safeParse(this.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((v) => v.message).join("\n");
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan posisi organisasi");
+ posisiOrganisasi.findMany.load();
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal menambahkan posisi");
+ }
+ } catch (error) {
+ console.error("Create error:", error);
+ toast.error("Terjadi kesalahan saat menambahkan posisi");
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.form = { ...posisiOrganisasiDefaultForm };
+ },
+ },
+
+ findUnique: {
+ data: null as Prisma.StrukturOrganisasiBumDesGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ posisiOrganisasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch posisiOrganisasi:", res.statusText);
+ posisiOrganisasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching posisiOrganisasi:", error);
+ posisiOrganisasi.findUnique.data = null;
+ }
+ },
+ },
+
+ edit: {
+ id: "",
+ form: { ...posisiOrganisasiDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ deskripsi: data.deskripsi,
+ hierarki: data.hierarki,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading posisi organisasi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templatePosisiOrganisasi.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const response = await fetch(
+ `/api/ekonomi/struktur-organisasi/posisi-organisasi/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ deskripsi: this.form.deskripsi,
+ hierarki: this.form.hierarki,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update posisi organisasi");
+ await posisiOrganisasi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate posisi organisasi"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating posisi organisasi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate posisi organisasi"
+ );
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.id = "";
+ this.form = { ...posisiOrganisasiDefaultForm };
+ },
+ },
+
+ findMany: {
+ data: [] as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string | null;
+ hierarki: number;
+ }>,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit?: number, search = "") => {
+ const appliedLimit = limit ?? 10;
+ posisiOrganisasi.findMany.page = page;
+ posisiOrganisasi.findMany.search = search;
+
+ try {
+ const query: any = { page, limit: appliedLimit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many'].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ posisiOrganisasi.findMany.data = res.data.data ?? [];
+ posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ posisiOrganisasi.findMany.data = [];
+ posisiOrganisasi.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch posisi organisasi paginated:", err);
+ posisiOrganisasi.findMany.data = [];
+ posisiOrganisasi.findMany.totalPages = 1;
+ } finally {
+ posisiOrganisasi.findMany.loading = false;
+ }
+ },
+ },
+ findManyAll: {
+ data: [] as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string | null;
+ hierarki: number;
+ }>,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ // Change to arrow function
+ posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
+ posisiOrganisasi.findManyAll.search = search;
+ try {
+ const query: any = { search };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many-all'].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ posisiOrganisasi.findManyAll.data = res.data.data || [];
+
+ } else {
+ console.error("Failed to load posisiOrganisasi:", res.data?.message);
+ posisiOrganisasi.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading posisiOrganisasi:", error);
+ posisiOrganisasi.findManyAll.data = [];
+ } finally {
+ posisiOrganisasi.findManyAll.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ posisiOrganisasi.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ekonomi/struktur-organisasi/posisi-organisasi/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Posisi organisasi berhasil dihapus");
+ await posisiOrganisasi.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus posisi organisasi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
+ } finally {
+ posisiOrganisasi.delete.loading = false;
+ }
+ },
+ },
+});
+
+const templatePegawai = z.object({
+ namaLengkap: z.string().min(1, "Nama wajib diisi"),
+ gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+ tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
+ email: z.string().email("Email tidak valid").optional(),
+ telepon: z.string().min(1, "Telepom wajib diisi"),
+ alamat: z.string().min(1, "Alamat wajib diisi"),
+ posisiId: z.string().min(1, "Posisi wajib diisi"),
+ isActive: z.boolean().default(true),
+});
+
+const pegawaiDefaultForm = {
+ namaLengkap: "",
+ gelarAkademik: "",
+ imageId: "",
+ tanggalMasuk: "",
+ email: "",
+ telepon: "",
+ alamat: "",
+ posisiId: "",
+ isActive: true,
+};
+
+const pegawai = proxy({
+ create: {
+ form: { ...pegawaiDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templatePegawai.safeParse(pegawai.create.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+
+ try {
+ pegawai.create.loading = true;
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['create'].post(
+ pegawai.create.form
+ );
+ if (res.status === 200) {
+ toast.success("Pegawai berhasil ditambahkan");
+ await pegawai.findMany.load();
+ } else {
+ toast.error(res.data?.message ?? "Gagal tambah pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error("Terjadi kesalahan saat menambahkan pegawai");
+ } finally {
+ pegawai.create.loading = false;
+ }
+ },
+ },
+
+ // In struktur-organisasi.ts
+ findMany: {
+ data: null as
+ | Prisma.PegawaiBumDesGetPayload<{
+ include: {
+ image: true;
+ posisi: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ pegawai.findMany.loading = true; // Use the full path to access the property
+ pegawai.findMany.page = page;
+ pegawai.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['find-many'].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pegawai.findMany.data = res.data.data || [];
+ pegawai.findMany.total = res.data.total || 0;
+ pegawai.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ pegawai.findMany.data = [];
+ pegawai.findMany.total = 0;
+ pegawai.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
+ pegawai.findMany.data = [];
+ pegawai.findMany.total = 0;
+ pegawai.findMany.totalPages = 1;
+ } finally {
+ pegawai.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as
+ | (Prisma.PegawaiBumDesGetPayload<{
+ include: { posisi: true; image: true };
+ }> & { isActive: boolean })
+ | null,
+ async load(id: string) {
+ const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`);
+ if (res.ok) {
+ const json = await res.json();
+ pegawai.findUnique.data = json.data
+ ? {
+ ...json.data,
+ isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada data
+ }
+ : null;
+ } else {
+ pegawai.findUnique.data = null;
+ }
+ },
+ },
+
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pegawai.delete.loading = true;
+ const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
+ method: "DELETE",
+ });
+ const json = await res.json();
+ if (res.ok) {
+ toast.success(json.message ?? "Berhasil hapus pegawai");
+ await pegawai.findMany.load();
+ } else {
+ toast.error(json.message ?? "Gagal hapus pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus");
+ } finally {
+ pegawai.delete.loading = false;
+ }
+ },
+ },
+
+ nonActive: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pegawai.nonActive.loading = true;
+ const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/non-active/${id}`, {
+ method: "DELETE", // biasanya nonActive pakai PATCH
+ });
+ const json = await res.json();
+ if (res.ok) {
+ toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
+ await pegawai.findMany.load(); // refresh data
+ } else {
+ toast.error(json.message ?? "Gagal menonaktifkan pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal nonActive:", error);
+ toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
+ } finally {
+ pegawai.nonActive.loading = false;
+ }
+ },
+ },
+
+ edit: {
+ id: "",
+ form: { ...pegawaiDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ namaLengkap: data.namaLengkap,
+ gelarAkademik: data.gelarAkademik,
+ imageId: data.imageId,
+ tanggalMasuk: data.tanggalMasuk,
+ email: data.email,
+ telepon: data.telepon,
+ alamat: data.alamat,
+ posisiId: data.posisiId,
+ isActive: data.isActive,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading berita:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const cek = templatePegawai.safeParse(pegawai.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ pegawai.edit.loading = true;
+
+ // Format tanggalMasuk to ISO string if it exists
+ const formattedTanggalMasuk = this.form.tanggalMasuk
+ ? new Date(this.form.tanggalMasuk).toISOString()
+ : undefined;
+
+ const response = await fetch(
+ `/api/ekonomi/struktur-organisasi/pegawai/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ id: this.id,
+ namaLengkap: this.form.namaLengkap,
+ gelarAkademik: this.form.gelarAkademik,
+ imageId: this.form.imageId || null,
+ tanggalMasuk: formattedTanggalMasuk,
+ email: this.form.email,
+ telepon: this.form.telepon,
+ alamat: this.form.alamat,
+ posisiId: this.form.posisiId,
+ isActive: this.form.isActive,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update pegawai");
+ await pegawai.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update pegawai");
+ }
+ } catch (error) {
+ console.error("Error updating pegawai:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update pegawai"
+ );
+ return false;
+ } finally {
+ pegawai.edit.loading = false;
+ }
+ },
+
+ reset() {
+ pegawai.edit.id = "";
+ pegawai.edit.form = { ...pegawaiDefaultForm };
+ },
+ },
+});
+
+const stateStrukturBumDes = proxy({
+ stateStruktur,
+ posisiOrganisasi,
+ pegawai,
+});
+
+export default stateStrukturBumDes;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts b/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts
new file mode 100644
index 00000000..52cd2f92
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts
@@ -0,0 +1,419 @@
+/* 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 templateGrafikUsiaKerjaYangMenganggur = z.object({
+ usia18_25: z.string().min(1, "Data usia 18-25 harus diisi"),
+ usia26_35: z.string().min(1, "Data usia 26-35 harus diisi"),
+ usia36_45: z.string().min(1, "Data usia 36-45 harus diisi"),
+ usia46_keatas: z.string().min(1, "Data usia 46 keatas harus diisi"),
+});
+
+type GrafikUsiaKerjaYangMenganggur = Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
+ select: {
+ id: true;
+ usia18_25: true;
+ usia26_35: true;
+ usia36_45: true;
+ usia46_keatas: true;
+ };
+}>;
+
+const defaultForm: Omit & { id?: string } = {
+ usia18_25: "",
+ usia26_35: "",
+ usia36_45: "",
+ usia46_keatas: "",
+};
+
+const grafikBerdasarkanUsiaKerjaNganggur = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templateGrafikUsiaKerjaYangMenganggur.safeParse(
+ grafikBerdasarkanUsiaKerjaNganggur.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ grafikBerdasarkanUsiaKerjaNganggur.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[
+ "create"
+ ].post(grafikBerdasarkanUsiaKerjaNganggur.create.form);
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ grafikBerdasarkanUsiaKerjaNganggur.create.form = {
+ usia18_25: "",
+ usia26_35: "",
+ usia36_45: "",
+ usia46_keatas: "",
+ };
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.load();
+ return id;
+ }
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ grafikBerdasarkanUsiaKerjaNganggur.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = true; // ✅ Akses langsung via nama path
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.page = page;
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data.data ?? [];
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch grafik berdasarkan usia kerja yang menganggur paginated:", err);
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
+ } finally {
+ grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ekonomi/grafikusiakerjayangmenganggur/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ grafikBerdasarkanUsiaKerjaNganggur.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikBerdasarkanUsiaKerjaNganggur.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan usia kerja yang menganggur:", error);
+ grafikBerdasarkanUsiaKerjaNganggur.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: {...defaultForm},
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateGrafikUsiaKerjaYangMenganggur.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/ekonomi/grafikusiakerjayangmenganggur/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await grafikBerdasarkanUsiaKerjaNganggur.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik berdasarkan usia kerja yang menganggur");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikBerdasarkanUsiaKerjaNganggur.delete.loading = true;
+
+ const response = await fetch(`/api/ekonomi/grafikusiakerjayangmenganggur/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Grafik berdasarkan usia kerja yang menganggur berhasil dihapus");
+ await grafikBerdasarkanUsiaKerjaNganggur.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus grafik berdasarkan usia kerja yang menganggur");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan usia kerja yang menganggur");
+ } finally {
+ grafikBerdasarkanUsiaKerjaNganggur.delete.loading = false;
+ }
+ },
+ }
+});
+
+const templateGrafikBerpendidikanYangMenganggur = z.object({
+ SD: z.string().min(1, "Data SD harus diisi"),
+ SMP: z.string().min(1, "Data SMP harus diisi"),
+ SMA: z.string().min(1, "Data SMA harus diisi"),
+ D3: z.string().min(1, "Data D3 harus diisi"),
+ S1: z.string().min(1, "Data S1 harus diisi"),
+});
+
+type GrafikBerpendidikanYangMenganggur = Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{
+ select: {
+ id: true;
+ SD: true;
+ SMP: true;
+ SMA: true;
+ D3: true;
+ S1: true;
+ };
+}>;
+
+const defaultFormBerpendidikan: Omit & { id?: string } = {
+ SD: "",
+ SMP: "",
+ SMA: "",
+ D3: "",
+ S1: "",
+};
+
+const grafikBerdasarkanPendidikan = proxy({
+ create: {
+ form: defaultFormBerpendidikan,
+ loading: false,
+ async create() {
+ const cek = templateGrafikBerpendidikanYangMenganggur.safeParse(
+ grafikBerdasarkanPendidikan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ grafikBerdasarkanPendidikan.create.loading = true;
+ const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[
+ "create"
+ ].post(grafikBerdasarkanPendidikan.create.form);
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ grafikBerdasarkanPendidikan.create.form = {
+ SD: "",
+ SMP: "",
+ SMA: "",
+ D3: "",
+ S1: "",
+ };
+ grafikBerdasarkanPendidikan.findMany.load();
+ return id;
+ }
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ grafikBerdasarkanPendidikan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ grafikBerdasarkanPendidikan.findMany.loading = true; // ✅ Akses langsung via nama path
+ grafikBerdasarkanPendidikan.findMany.page = page;
+ grafikBerdasarkanPendidikan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikBerdasarkanPendidikan.findMany.data = res.data.data ?? [];
+ grafikBerdasarkanPendidikan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ grafikBerdasarkanPendidikan.findMany.data = [];
+ grafikBerdasarkanPendidikan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch grafik berdasarkan pendidikan paginated:", err);
+ grafikBerdasarkanPendidikan.findMany.data = [];
+ grafikBerdasarkanPendidikan.findMany.totalPages = 1;
+ } finally {
+ grafikBerdasarkanPendidikan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ekonomi/grafikmenganggurberdasarkanpendidikan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ grafikBerdasarkanPendidikan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikBerdasarkanPendidikan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan usia kerja yang menganggur:", error);
+ grafikBerdasarkanPendidikan.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: {...defaultFormBerpendidikan},
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateGrafikBerpendidikanYangMenganggur.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/ekonomi/grafikmenganggurberdasarkanpendidikan/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await grafikBerdasarkanPendidikan.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik berdasarkan pendidikan yang menganggur");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikBerdasarkanPendidikan.delete.loading = true;
+
+ const response = await fetch(`/api/ekonomi/grafikmenganggurberdasarkanpendidikan/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Grafik berdasarkan pendidikan yang menganggur berhasil dihapus");
+ await grafikBerdasarkanPendidikan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus grafik berdasarkan pendidikan yang menganggur");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan pendidikan yang menganggur");
+ } finally {
+ grafikBerdasarkanPendidikan.delete.loading = false;
+ }
+ },
+ }
+});
+
+const grafikNganggur = proxy({
+ grafikBerdasarkanUsiaKerjaNganggur,
+ grafikBerdasarkanPendidikan
+})
+
+export default grafikNganggur;
diff --git a/src/app/admin/(dashboard)/_state/inovasi/ajukan-ide-inovatif.ts b/src/app/admin/(dashboard)/_state/inovasi/ajukan-ide-inovatif.ts
new file mode 100644
index 00000000..295c1409
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/ajukan-ide-inovatif.ts
@@ -0,0 +1,156 @@
+/* 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({
+ name: z.string().min(1).max(50),
+ deskripsi: z.string().min(1).max(5000),
+ alamat: z.string().min(1).max(5000),
+ namaIde: z.string().min(1).max(5000),
+ masalah: z.string().min(1).max(5000),
+ benefit: z.string().min(1).max(5000),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ alamat: "",
+ namaIde: "",
+ masalah: "",
+ benefit: "",
+};
+
+const ajukanIdeInovatifState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(ajukanIdeInovatifState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ ajukanIdeInovatifState.create.loading = true;
+ const res = await ApiFetch.api.inovasi.ajukanideinovatif["create"].post(
+ ajukanIdeInovatifState.create.form
+ );
+ if (res.status === 200) {
+ ajukanIdeInovatifState.findMany.load();
+ return toast.success("Ajukan Ide Inovatif berhasil di kirim");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ ajukanIdeInovatifState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.AjukanIdeInovatifGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ ajukanIdeInovatifState.findMany.loading = true; // ✅ Akses langsung via nama path
+ ajukanIdeInovatifState.findMany.page = page;
+ ajukanIdeInovatifState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.inovasi.ajukanideinovatif[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ ajukanIdeInovatifState.findMany.data = res.data.data ?? [];
+ ajukanIdeInovatifState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ ajukanIdeInovatifState.findMany.data = [];
+ ajukanIdeInovatifState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch ajukan ide inovatif paginated:", err);
+ ajukanIdeInovatifState.findMany.data = [];
+ ajukanIdeInovatifState.findMany.totalPages = 1;
+ } finally {
+ ajukanIdeInovatifState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.AjukanIdeInovatifGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/inovasi/ajukanideinovatif/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ ajukanIdeInovatifState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ ajukanIdeInovatifState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading ajukan ide inovatif:", error);
+ ajukanIdeInovatifState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ ajukanIdeInovatifState.delete.loading = true;
+ const response = await fetch(
+ `/api/inovasi/ajukanideinovatif/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ const result = await response.json();
+
+ if (response.ok) {
+ toast.success(
+ result.message || "Ajukan Ide Inovatif berhasil dihapus"
+ );
+ await ajukanIdeInovatifState.findMany.load();
+ } else {
+ toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");
+ }
+ } catch (error) {
+ console.log((error as Error).message);
+ toast.error("Terjadi kesalahan saat menghapus ajukan ide inovatif");
+ } finally {
+ ajukanIdeInovatifState.delete.loading = false;
+ }
+ },
+ },
+});
+export default ajukanIdeInovatifState;
diff --git a/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts b/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts
new file mode 100644
index 00000000..83d4d5cd
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts
@@ -0,0 +1,241 @@
+/* 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({
+ name: z.string().min(1).max(50),
+ deskripsi: z.string().min(1).max(5000),
+ imageId: z.string().min(1).max(50),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+};
+
+const desaDigitalState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(desaDigitalState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ desaDigitalState.create.loading = true;
+ const res = await ApiFetch.api.inovasi.desadigital["create"].post(
+ desaDigitalState.create.form
+ );
+ if (res.status === 200) {
+ desaDigitalState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ desaDigitalState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.DesaDigitalGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ desaDigitalState.findMany.loading = true; // ✅ Akses langsung via nama path
+ desaDigitalState.findMany.page = page;
+ desaDigitalState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.inovasi.desadigital["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ desaDigitalState.findMany.data = res.data.data ?? [];
+ desaDigitalState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ desaDigitalState.findMany.data = [];
+ desaDigitalState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch desa digital paginated:", err);
+ desaDigitalState.findMany.data = [];
+ desaDigitalState.findMany.totalPages = 1;
+ } finally {
+ desaDigitalState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DesaDigitalGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/inovasi/desadigital/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ desaDigitalState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ desaDigitalState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading desa digital:", error);
+ desaDigitalState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ desaDigitalState.delete.loading = true;
+ const response = await fetch(`/api/inovasi/desadigital/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+
+ if (response.ok) {
+ toast.success(result.message || "Desa Digital berhasil dihapus");
+ await desaDigitalState.findMany.load();
+ } else {
+ toast.error(result?.message || "Gagal menghapus desa digital");
+ }
+ } catch (error) {
+ console.log((error as Error).message);
+ toast.error("Terjadi kesalahan saat menghapus desa digital");
+ } finally {
+ desaDigitalState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/inovasi/desadigital/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading desa digital:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(desaDigitalState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ desaDigitalState.edit.loading = true;
+ const response = await fetch(
+ `/api/inovasi/desadigital/${desaDigitalState.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update desa digital");
+ await desaDigitalState.findMany.load();
+ return true;
+ } else {
+ throw new Error(result?.message || "Gagal update desa digital");
+ }
+ } catch (error) {
+ console.error("Error updating desa digital:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update desa digital"
+ );
+ return false;
+ } finally {
+ desaDigitalState.edit.loading = false;
+ }
+ },
+ reset() {
+ desaDigitalState.edit.id = "";
+ desaDigitalState.edit.form = { ...defaultForm };
+ },
+ },
+});
+export default desaDigitalState;
diff --git a/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts
new file mode 100644
index 00000000..710bbcd8
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts
@@ -0,0 +1,241 @@
+/* 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({
+ name: z.string().min(1).max(50),
+ deskripsi: z.string().min(1).max(5000),
+ imageId: z.string().min(1).max(50),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+};
+
+const infoTeknoState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(infoTeknoState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ infoTeknoState.create.loading = true;
+ const res = await ApiFetch.api.inovasi.infotekno["create"].post(
+ infoTeknoState.create.form
+ );
+ if (res.status === 200) {
+ infoTeknoState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ infoTeknoState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.InfoTeknoGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ infoTeknoState.findMany.loading = true; // ✅ Akses langsung via nama path
+ infoTeknoState.findMany.page = page;
+ infoTeknoState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.inovasi.infotekno["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ infoTeknoState.findMany.data = res.data.data ?? [];
+ infoTeknoState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ infoTeknoState.findMany.data = [];
+ infoTeknoState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch info teknologi paginated:", err);
+ infoTeknoState.findMany.data = [];
+ infoTeknoState.findMany.totalPages = 1;
+ } finally {
+ infoTeknoState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.InfoTeknoGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/inovasi/infotekno/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ infoTeknoState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ infoTeknoState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading desa digital:", error);
+ infoTeknoState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ infoTeknoState.delete.loading = true;
+ const response = await fetch(`/api/inovasi/infotekno/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+
+ if (response.ok) {
+ toast.success(result.message || "Info Tekno berhasil dihapus");
+ await infoTeknoState.findMany.load();
+ } else {
+ toast.error(result?.message || "Gagal menghapus info tekno");
+ }
+ } catch (error) {
+ console.log((error as Error).message);
+ toast.error("Terjadi kesalahan saat menghapus info tekno");
+ } finally {
+ infoTeknoState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/inovasi/infotekno/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading info tekno:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(infoTeknoState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ infoTeknoState.edit.loading = true;
+ const response = await fetch(
+ `/api/inovasi/infotekno/${infoTeknoState.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update info tekno");
+ await infoTeknoState.findMany.load();
+ return true;
+ } else {
+ throw new Error(result?.message || "Gagal update info tekno");
+ }
+ } catch (error) {
+ console.error("Error updating info tekno:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update info tekno"
+ );
+ return false;
+ } finally {
+ infoTeknoState.edit.loading = false;
+ }
+ },
+ reset() {
+ infoTeknoState.edit.id = "";
+ infoTeknoState.edit.form = { ...defaultForm };
+ },
+ },
+});
+export default infoTeknoState;
diff --git a/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts b/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts
new file mode 100644
index 00000000..50ad7168
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi.ts
@@ -0,0 +1,249 @@
+/* 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({
+ name: z.string().min(1, "Nama kolaborasi inovasi harus diisi"),
+ tahun: z.number().min(1900, "Tahun tidak valid").max(new Date().getFullYear() + 1, "Tahun tidak boleh lebih dari tahun depan"),
+ slug: z.string().min(1, "Slug harus dihasilkan otomatis"),
+ deskripsi: z.string().min(1, "Deskripsi harus diisi"),
+ kolaborator: z.string().min(1, "Kolaborator harus diisi"),
+})
+
+const defaultForm = {
+ name: "",
+ tahun: 0,
+ slug: "",
+ deskripsi: "",
+ kolaborator: "",
+}
+
+const kolaborasiInovasiState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ try {
+ // Validate form
+ const validation = templateForm.safeParse(kolaborasiInovasiState.create.form);
+ if (!validation.success) {
+ const errorMessages = validation.error.issues
+ .map(issue => `- ${issue.path.join('.')}: ${issue.message}`)
+ .join('\n');
+ return toast.error(`Validasi gagal:\n${errorMessages}`);
+ }
+
+ kolaborasiInovasiState.create.loading = true;
+
+ const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
+ kolaborasiInovasiState.create.form
+ );
+
+ if (res.status === 200) {
+ await kolaborasiInovasiState.findMany.load();
+ return { success: true, data: res.data };
+ }
+
+ console.error('Create failed:', res);
+ toast.error(res.data?.message || "Gagal menyimpan data");
+ return { success: false, error: res.data };
+ } catch (error) {
+ console.error('Error in create:', error);
+ toast.error("Terjadi kesalahan saat menyimpan data");
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error'
+ };
+ } finally {
+ kolaborasiInovasiState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ year: "",
+ load: async (page = 1, limit = 10, search = "", year?: string) => {
+ kolaborasiInovasiState.findMany.loading = true;
+ kolaborasiInovasiState.findMany.page = page;
+ kolaborasiInovasiState.findMany.search = search;
+ kolaborasiInovasiState.findMany.year = year || "";
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (year) query.year = year;
+
+ const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kolaborasiInovasiState.findMany.data = res.data.data || [];
+ kolaborasiInovasiState.findMany.total = res.data.total || 0;
+ kolaborasiInovasiState.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load grafik berdasarkan jenis kelamin:",
+ res.data?.message
+ );
+ kolaborasiInovasiState.findMany.data = [];
+ kolaborasiInovasiState.findMany.total = 0;
+ kolaborasiInovasiState.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan jenis kelamin:", error);
+ kolaborasiInovasiState.findMany.data = [];
+ kolaborasiInovasiState.findMany.total = 0;
+ kolaborasiInovasiState.findMany.totalPages = 1;
+ } finally {
+ kolaborasiInovasiState.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ tahun: data.tahun,
+ slug: data.slug,
+ deskripsi: data.deskripsi,
+ kolaborator: data.kolaborator,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data");
+ }
+ } catch (error) {
+ console.error("Error loading kolaborasi inovasi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await kolaborasiInovasiState.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data kolaborasi inovasi");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KolaborasiInovasiGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kolaborasiInovasiState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kolaborasiInovasiState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading kolaborasi inovasi:", error);
+ kolaborasiInovasiState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kolaborasiInovasiState.delete.loading = true;
+
+ const response = await fetch(`/api/inovasi/kolaborasiinovasi/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kolaborasi inovasi berhasil dihapus");
+ await kolaborasiInovasiState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kolaborasi inovasi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kolaborasi inovasi");
+ } finally {
+ kolaborasiInovasiState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default kolaborasiInovasiState;
+
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/inovasi/layanan-online-desa.ts b/src/app/admin/(dashboard)/_state/inovasi/layanan-online-desa.ts
new file mode 100644
index 00000000..85b4cd8e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/layanan-online-desa.ts
@@ -0,0 +1,871 @@
+/* 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";
+
+// ========================================= ADMINISTRASI ONLINE ========================================= //
+const templateAdministrasiOnlineForm = z.object({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ alamat: z.string().min(1, "Alamat minimal 1 karakter"),
+ nomorTelepon: z.string().min(1, "Nomor telepon minimal 1 karakter"),
+ jenisLayananId: z.string().min(1, "Jenis layanan minimal 1 karakter"),
+});
+
+const defaultAdministrasiOnlineForm = {
+ name: "",
+ alamat: "",
+ nomorTelepon: "",
+ jenisLayananId: "",
+};
+
+const administrasiOnline = proxy({
+ create: {
+ form: { ...defaultAdministrasiOnlineForm },
+ loading: false,
+ async create() {
+ const cek = templateAdministrasiOnlineForm.safeParse(
+ administrasiOnline.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ administrasiOnline.create.loading = true;
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
+ "create"
+ ].post(administrasiOnline.create.form);
+ if (res.status === 200) {
+ administrasiOnline.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ administrasiOnline.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.AdministrasiOnlineGetPayload<{
+ include: {
+ jenisLayanan: true;
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ async load(page = 1, limit = 10, search = "") {
+ administrasiOnline.findMany.loading = true;
+ administrasiOnline.findMany.page = page;
+ administrasiOnline.findMany.search = search;
+ try {
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.administrasionline[
+ "find-many"
+ ].get({
+ query: {
+ page,
+ limit,
+ search,
+ },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ administrasiOnline.findMany.data = res.data.data ?? [];
+ administrasiOnline.findMany.totalPages = res.data.totalPages ?? 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch administrasi online paginated:", err);
+ } finally {
+ administrasiOnline.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.AdministrasiOnlineGetPayload<{
+ include: {
+ jenisLayanan: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/inovasi/layananonlinedesa/administrasionline/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ administrasiOnline.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch administrasi online:", res.statusText);
+ administrasiOnline.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching administrasi online:", error);
+ administrasiOnline.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ administrasiOnline.delete.loading = true;
+
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/administrasionline/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Administrasi online berhasil dihapus"
+ );
+ await administrasiOnline.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus administrasi online");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus administrasi online");
+ } finally {
+ administrasiOnline.delete.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= JENIS LAYANAN ========================================= //
+const templateJenisLayananForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+});
+
+const defaultJenisLayananForm = {
+ nama: "",
+ deskripsi: "",
+};
+
+const jenisLayanan = proxy({
+ create: {
+ form: { ...defaultJenisLayananForm },
+ loading: false,
+ async create() {
+ const cek = templateJenisLayananForm.safeParse(jenisLayanan.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ jenisLayanan.create.loading = true;
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
+ "create"
+ ].post(jenisLayanan.create.form);
+ if (res.status === 200) {
+ jenisLayanan.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ jenisLayanan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string;
+ }> | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ jenisLayanan.findMany.loading = true; // ✅ Akses langsung via nama path
+ jenisLayanan.findMany.page = page;
+ jenisLayanan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ jenisLayanan.findMany.data = res.data.data ?? [];
+ jenisLayanan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ jenisLayanan.findMany.data = [];
+ jenisLayanan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch jenis layanan paginated:", err);
+ jenisLayanan.findMany.data = [];
+ jenisLayanan.findMany.totalPages = 1;
+ } finally {
+ jenisLayanan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.JenisLayananGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ jenisLayanan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ jenisLayanan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ jenisLayanan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ jenisLayanan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Jenis layanan berhasil dihapus");
+ await jenisLayanan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus jenis layanan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus jenis layanan");
+ } finally {
+ jenisLayanan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultJenisLayananForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ deskripsi: data.deskripsi,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading jenis layanan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateJenisLayananForm.safeParse(jenisLayanan.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ jenisLayanan.edit.loading = true;
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/administrasionline/jenislayanan/${jenisLayanan.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: jenisLayanan.edit.form.nama,
+ deskripsi: jenisLayanan.edit.form.deskripsi,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate jenis layanan (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui jenis layanan"
+ );
+ await jenisLayanan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate jenis layanan");
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating jenis layanan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate jenis layanan"
+ );
+ return false;
+ } finally {
+ jenisLayanan.edit.loading = false;
+ }
+ },
+ reset() {
+ jenisLayanan.edit.id = "";
+ jenisLayanan.edit.form = { ...defaultJenisLayananForm };
+ },
+ },
+});
+
+// ========================================= PENGADUAN MASYARAKAT ========================================= //
+const templatePengaduanMasyarakatForm = z.object({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ email: z.string().min(1, "Alamat minimal 1 karakter"),
+ nomorTelepon: z.string().min(1, "Nomor telepon minimal 1 karakter"),
+ nik: z.string().min(1, "NIK minimal 1 karakter"),
+ judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"),
+ lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"),
+ deskripsiPengaduan: z
+ .string()
+ .min(1, "Deskripsi pengaduan minimal 1 karakter"),
+ jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"),
+ imageId: z.string().min(1, "Image minimal 1 karakter"),
+});
+
+const defaultPengaduanMasyarakatForm = {
+ name: "",
+ email: "",
+ nomorTelepon: "",
+ nik: "",
+ judulPengaduan: "",
+ lokasiKejadian: "",
+ deskripsiPengaduan: "",
+ jenisPengaduanId: "",
+ imageId: "",
+};
+
+const pengaduanMasyarakat = proxy({
+ create: {
+ form: { ...defaultPengaduanMasyarakatForm },
+ loading: false,
+ async create() {
+ const cek = templatePengaduanMasyarakatForm.safeParse(
+ pengaduanMasyarakat.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pengaduanMasyarakat.create.loading = true;
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
+ "create"
+ ].post(pengaduanMasyarakat.create.form);
+ if (res.status === 200) {
+ pengaduanMasyarakat.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ pengaduanMasyarakat.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.PengaduanMasyarakatGetPayload<{
+ include: {
+ jenisPengaduan: true;
+ image: true;
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ pengaduanMasyarakat.findMany.loading = true; // ✅ Akses langsung via nama path
+ pengaduanMasyarakat.findMany.page = page;
+ pengaduanMasyarakat.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pengaduanMasyarakat.findMany.data = res.data.data ?? [];
+ pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ pengaduanMasyarakat.findMany.data = [];
+ pengaduanMasyarakat.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch pengaduan masyarakat paginated:", err);
+ pengaduanMasyarakat.findMany.data = [];
+ pengaduanMasyarakat.findMany.totalPages = 1;
+ } finally {
+ pengaduanMasyarakat.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PengaduanMasyarakatGetPayload<{
+ include: {
+ jenisPengaduan: true;
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/inovasi/layananonlinedesa/pengaduanmasyarakat/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ pengaduanMasyarakat.findUnique.data = data.data ?? null;
+ } else {
+ console.error(
+ "Failed to fetch pengaduan masyarakat:",
+ res.statusText
+ );
+ pengaduanMasyarakat.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching pengaduan masyarakat:", error);
+ pengaduanMasyarakat.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pengaduanMasyarakat.delete.loading = true;
+
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/pengaduanmasyarakat/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Pengaduan masyarakat berhasil dihapus"
+ );
+ await pengaduanMasyarakat.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus pengaduan masyarakat"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pengaduan masyarakat");
+ } finally {
+ pengaduanMasyarakat.delete.loading = false;
+ }
+ },
+ },
+});
+// ========================================= JENIS PENGADUAN ========================================= //
+const templateJenisPengaduanForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+});
+
+const defaultJenisPengaduanForm = {
+ nama: "",
+};
+
+const jenisPengaduan = proxy({
+ create: {
+ form: { ...defaultJenisPengaduanForm },
+ loading: false,
+ async create() {
+ const cek = templateJenisPengaduanForm.safeParse(
+ jenisPengaduan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ jenisPengaduan.create.loading = true;
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
+ "create"
+ ].post(jenisPengaduan.create.form);
+ if (res.status === 200) {
+ jenisPengaduan.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ jenisPengaduan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<{
+ id: string;
+ nama: string;
+ }> | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ jenisPengaduan.findMany.loading = true; // ✅ Akses langsung via nama path
+ jenisPengaduan.findMany.page = page;
+ jenisPengaduan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ jenisPengaduan.findMany.data = res.data.data ?? [];
+ jenisPengaduan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ jenisPengaduan.findMany.data = [];
+ jenisPengaduan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch jenis pengaduan paginated:", err);
+ jenisPengaduan.findMany.data = [];
+ jenisPengaduan.findMany.totalPages = 1;
+ } finally {
+ jenisPengaduan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.JenisPengaduanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ jenisPengaduan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ jenisPengaduan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ jenisPengaduan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ jenisPengaduan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Jenis pengduan berhasil dihapus");
+ await jenisPengaduan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus jenis pengaduan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus jenis pengaduan");
+ } finally {
+ jenisPengaduan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultJenisPengaduanForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading jenis pengaduan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateJenisPengaduanForm.safeParse(
+ jenisPengaduan.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ jenisPengaduan.edit.loading = true;
+ const response = await fetch(
+ `/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${jenisPengaduan.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: jenisPengaduan.edit.form.nama,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate jenis pengaduan (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui jenis pengaduan"
+ );
+ await jenisPengaduan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate jenis pengaduan"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating jenis pengaduan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate jenis pengaduan"
+ );
+ return false;
+ } finally {
+ jenisPengaduan.edit.loading = false;
+ }
+ },
+ reset() {
+ jenisPengaduan.edit.id = "";
+ jenisPengaduan.edit.form = { ...defaultJenisPengaduanForm };
+ },
+ },
+});
+
+const layananonlineDesa = proxy({
+ administrasiOnline,
+ jenisLayanan,
+ pengaduanMasyarakat,
+ jenisPengaduan,
+});
+
+export default layananonlineDesa;
diff --git a/src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts b/src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts
new file mode 100644
index 00000000..d931135e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts
@@ -0,0 +1,229 @@
+/* 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 mitraKolaborasiForm = z.object({
+ name: z.string().min(1, { message: "Name is required" }),
+ imageId: z.string().nonempty(),
+});
+
+const defaultForm = {
+ name: "",
+ imageId: "",
+};
+
+const mitraKolaborasi = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = mitraKolaborasiForm.safeParse(mitraKolaborasi.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ mitraKolaborasi.create.loading = true;
+ const res = await ApiFetch.api.inovasi.mitrakolaborasi["create"].post(
+ mitraKolaborasi.create.form
+ );
+ if (res.status === 200) {
+ mitraKolaborasi.findMany.load();
+ return toast.success("mitraKolaborasi berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan mitraKolaborasi");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ mitraKolaborasi.create.loading = false;
+ }
+ },
+ resetForm() {
+ mitraKolaborasi.create.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.MitraKolaborasiGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ mitraKolaborasi.findMany.loading = true; // ✅ Akses langsung via nama path
+ mitraKolaborasi.findMany.page = page;
+ mitraKolaborasi.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.inovasi.mitrakolaborasi["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ mitraKolaborasi.findMany.data = res.data.data ?? [];
+ mitraKolaborasi.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ mitraKolaborasi.findMany.data = [];
+ mitraKolaborasi.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch mitraKolaborasi paginated:", err);
+ mitraKolaborasi.findMany.data = [];
+ mitraKolaborasi.findMany.totalPages = 1;
+ } finally {
+ mitraKolaborasi.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.MitraKolaborasiGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/inovasi/mitrakolaborasi/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ mitraKolaborasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch mitraKolaborasi:", res.statusText);
+ mitraKolaborasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching mitraKolaborasi:", error);
+ mitraKolaborasi.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ mitraKolaborasi.delete.loading = true;
+ const response = await fetch(`/api/inovasi/mitrakolaborasi/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+ if (response.ok) {
+ toast.success(result.message || "mitraKolaborasi berhasil dihapus");
+ await mitraKolaborasi.findMany.load(); // refresh list
+ } else {
+ toast.error(result.message || "Gagal menghapus mitraKolaborasi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus mitraKolaborasi");
+ } finally {
+ mitraKolaborasi.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(`/api/inovasi/mitrakolaborasi/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ imageId: data.imageId,
+ };
+ return data;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading mitraKolaborasi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = mitraKolaborasiForm.safeParse(mitraKolaborasi.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+ try {
+ mitraKolaborasi.update.loading = true;
+ const response = await fetch(`/api/inovasi/mitrakolaborasi/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ imageId: this.form.imageId,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "mitraKolaborasi berhasil diupdate");
+ await mitraKolaborasi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate mitraKolaborasi");
+ }
+ } catch (error) {
+ console.error("Error updating mitraKolaborasi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate mitraKolaborasi"
+ );
+ return false;
+ } finally {
+ mitraKolaborasi.update.loading = false;
+ }
+ },
+ reset() {
+ mitraKolaborasi.update.id = "";
+ mitraKolaborasi.update.form = { ...defaultForm };
+ },
+ },
+});
+
+export default mitraKolaborasi;
diff --git a/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts b/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts
new file mode 100644
index 00000000..345910cc
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts
@@ -0,0 +1,225 @@
+/* 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({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
+ icon: z.string().min(1, "Icon minimal 1 karakter"),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ slug: "",
+ icon: "",
+};
+
+const programKreatifState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(programKreatifState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ programKreatifState.create.loading = true;
+ const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
+ programKreatifState.create.form
+ );
+ if (res.status === 200) {
+ programKreatifState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ programKreatifState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ programKreatifState.findMany.loading = true; // ✅ Akses langsung via nama path
+ programKreatifState.findMany.page = page;
+ programKreatifState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.inovasi.programkreatif[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ programKreatifState.findMany.data = res.data.data ?? [];
+ programKreatifState.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ programKreatifState.findMany.data = [];
+ programKreatifState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch program kreatif paginated:", err);
+ programKreatifState.findMany.data = [];
+ programKreatifState.findMany.totalPages = 1;
+ } finally {
+ programKreatifState.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/inovasi/programkreatif/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ slug: data.slug,
+ icon: data.icon,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data");
+ }
+ } catch (error) {
+ console.error("Error loading program kreatif:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/inovasi/programkreatif/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await programKreatifState.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data program kreatif");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ProgramKreatifGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/inovasi/programkreatif/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ programKreatifState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ programKreatifState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading program kreatif:", error);
+ programKreatifState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ programKreatifState.delete.loading = true;
+
+ const response = await fetch(`/api/inovasi/programkreatif/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Program kreatif berhasil dihapus");
+ await programKreatifState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus program kreatif");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus program kreatif");
+ } finally {
+ programKreatifState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default programKreatifState;
diff --git a/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts b/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts
new file mode 100644
index 00000000..3fd458f4
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts
@@ -0,0 +1,257 @@
+/* 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({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+};
+
+const keamananLingkunganState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(keamananLingkunganState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ keamananLingkunganState.create.loading = true;
+ const res = await ApiFetch.api.keamanan.keamananlingkungan[
+ "create"
+ ].post(keamananLingkunganState.create.form);
+ if (res.status === 200) {
+ keamananLingkunganState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ keamananLingkunganState.create.loading = false;
+ }
+ },
+ resetForm() {
+ keamananLingkunganState.create.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.KeamananLingkunganGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ keamananLingkunganState.findMany.loading = true; // ✅ Akses langsung via nama path
+ keamananLingkunganState.findMany.page = page;
+ keamananLingkunganState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.keamananlingkungan["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ keamananLingkunganState.findMany.data = res.data.data ?? [];
+ keamananLingkunganState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ keamananLingkunganState.findMany.data = [];
+ keamananLingkunganState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch keamanan lingkungan paginated:", err);
+ keamananLingkunganState.findMany.data = [];
+ keamananLingkunganState.findMany.totalPages = 1;
+ } finally {
+ keamananLingkunganState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KeamananLingkunganGetPayload<{
+ include: { image: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/keamananlingkungan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ keamananLingkunganState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ keamananLingkunganState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ keamananLingkunganState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ keamananLingkunganState.delete.loading = true;
+
+ const response = await fetch(
+ `/api/keamanan/keamananlingkungan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Keamanan ingkungan berhasil dihapus"
+ );
+ await keamananLingkunganState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus keamanan ingkungan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus keamanan ingkungan");
+ } finally {
+ keamananLingkunganState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/keamanan/keamananlingkungan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId || "",
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading keamanan lingkungan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(keamananLingkunganState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ keamananLingkunganState.edit.loading = true;
+
+ const response = await fetch(
+ `/api/keamanan/keamananlingkungan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update keamanan lingkungan");
+ await keamananLingkunganState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update keamanan lingkungan");
+ }
+ } catch (error) {
+ console.error("Error updating keamanan lingkungan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update keamanan lingkungan"
+ );
+ return false;
+ } finally {
+ keamananLingkunganState.edit.loading = false;
+ }
+ },
+ reset() {
+ keamananLingkunganState.edit.id = "";
+ keamananLingkunganState.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default keamananLingkunganState;
diff --git a/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts b/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts
new file mode 100644
index 00000000..af0d2401
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts
@@ -0,0 +1,504 @@
+/* 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 minimal 1 karakter"),
+ icon: z.string().nonempty(),
+ kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
+});
+
+const defaultForm = {
+ nama: "",
+ icon: "",
+ kategoriId: [] as string[],
+};
+
+const kontakDaruratKeamananState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(
+ kontakDaruratKeamananState.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kontakDaruratKeamananState.create.loading = true;
+ const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
+ "create"
+ ].post(kontakDaruratKeamananState.create.form);
+ if (res.status === 200) {
+ kontakDaruratKeamananState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ kontakDaruratKeamananState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.KontakDaruratKeamananGetPayload<{
+ include: {
+ kategori: true;
+ kontakItems: {
+ include: {
+ kontakItem: true;
+ };
+ };
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kontakDaruratKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
+ kontakDaruratKeamananState.findMany.page = page;
+ kontakDaruratKeamananState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kontakDaruratKeamananState.findMany.data = res.data.data ?? [];
+ kontakDaruratKeamananState.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ kontakDaruratKeamananState.findMany.data = [];
+ kontakDaruratKeamananState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kontak darurat paginated:", err);
+ kontakDaruratKeamananState.findMany.data = [];
+ kontakDaruratKeamananState.findMany.totalPages = 1;
+ } finally {
+ kontakDaruratKeamananState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KontakDaruratKeamananGetPayload<{
+ include: {
+ kontakItems: {
+ include: {
+ kontakItem: true;
+ };
+ };
+ kategori: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/kontakdaruratkeamanan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kontakDaruratKeamananState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kontakDaruratKeamananState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kontakDaruratKeamananState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ kontakDaruratKeamananState.delete.loading = true;
+ const response = await fetch(
+ `/api/keamanan/kontakdaruratkeamanan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kontak darurat berhasil dihapus");
+ await kontakDaruratKeamananState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kontak darurat");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kontak darurat");
+ } finally {
+ kontakDaruratKeamananState.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/keamanan/kontakdaruratkeamanan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ icon: data.icon || "",
+ kategoriId:
+ data.kontakItems?.map((item: any) => item.kontakItemId) || [],
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kontak darurat:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(
+ kontakDaruratKeamananState.update.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kontakDaruratKeamananState.update.loading = true;
+ const response = await fetch(
+ `/api/keamanan/kontakdaruratkeamanan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ icon: this.form.icon,
+ kategoriId: this.form.kategoriId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update kontak darurat");
+ await kontakDaruratKeamananState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate kontak darurat");
+ }
+ } catch (error) {
+ console.error("Error updating kontak darurat:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kontak darurat"
+ );
+ return false;
+ } finally {
+ kontakDaruratKeamananState.update.loading = false;
+ }
+ },
+ reset() {
+ kontakDaruratKeamananState.update.id = "";
+ kontakDaruratKeamananState.update.form = { ...defaultForm };
+ },
+ },
+});
+
+const templateFormItem = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+ nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
+ icon: z.string().nonempty(),
+});
+
+const defaultFormItem = {
+ nama: "",
+ nomorTelepon: "",
+ icon: "",
+};
+
+const kontakDaruratItem = proxy({
+ create: {
+ form: { ...defaultFormItem },
+ loading: false,
+ async create() {
+ const cek = templateFormItem.safeParse(kontakDaruratItem.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kontakDaruratItem.create.loading = true;
+ const res = await ApiFetch.api.keamanan.kontakitem["create"].post(
+ kontakDaruratItem.create.form
+ );
+ if (res.status === 200) {
+ kontakDaruratItem.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ kontakDaruratItem.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.KontakItemGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kontakDaruratItem.findMany.loading = true; // ✅ Akses langsung via nama path
+ kontakDaruratItem.findMany.page = page;
+ kontakDaruratItem.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.kontakitem["find-many"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kontakDaruratItem.findMany.data = res.data.data ?? [];
+ kontakDaruratItem.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kontakDaruratItem.findMany.data = [];
+ kontakDaruratItem.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kontak darurat paginated:", err);
+ kontakDaruratItem.findMany.data = [];
+ kontakDaruratItem.findMany.totalPages = 1;
+ } finally {
+ kontakDaruratItem.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KontakItemGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/kontakitem/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kontakDaruratItem.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kontakDaruratItem.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kontakDaruratItem.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ kontakDaruratItem.delete.loading = true;
+ const response = await fetch(`/api/keamanan/kontakitem/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kontak item berhasil dihapus");
+ await kontakDaruratItem.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kontak item");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kontak item");
+ } finally {
+ kontakDaruratItem.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormItem },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/keamanan/kontakitem/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ nomorTelepon: data.nomorTelepon,
+ icon: data.icon,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kontak darurat:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateFormItem.safeParse(kontakDaruratItem.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kontakDaruratItem.update.loading = true;
+ const response = await fetch(`/api/keamanan/kontakitem/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ nomorTelepon: this.form.nomorTelepon,
+ icon: this.form.icon,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update kontak item");
+ await kontakDaruratItem.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate kontak item");
+ }
+ } catch (error) {
+ console.error("Error updating kontak item:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kontak item"
+ );
+ return false;
+ } finally {
+ kontakDaruratItem.update.loading = false;
+ }
+ },
+ reset() {
+ kontakDaruratItem.update.id = "";
+ kontakDaruratItem.update.form = { ...defaultFormItem };
+ },
+ },
+});
+
+const kontakDarurat = proxy({
+ kontakDaruratKeamananState,
+ kontakDaruratItem,
+});
+
+export default kontakDarurat;
diff --git a/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts b/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts
new file mode 100644
index 00000000..6db65c2b
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts
@@ -0,0 +1,311 @@
+/* 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";
+
+export type Status = "Selesai" | "Proses" | "Gagal";
+
+const templateForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ lokasi: z.string().min(3, "Lokasi minimal 3 karakter"),
+ tanggalWaktu: z.string().min(3, "Tanggal Waktu minimal 3 karakter"),
+ kronologi: z.string().optional(),
+});
+
+interface FormData {
+ judul: string;
+ lokasi: string;
+ tanggalWaktu: string;
+ kronologi: string;
+}
+
+const defaultForm: FormData = {
+ judul: "",
+ lokasi: "",
+ tanggalWaktu: new Date().toISOString(),
+ kronologi: "",
+};
+
+interface FormEditData {
+ judul: string;
+ lokasi: string;
+ tanggalWaktu: string;
+ status: Status;
+ penanganan: string;
+ kronologi: string;
+}
+
+const editForm: FormEditData = {
+ judul: "",
+ lokasi: "",
+ tanggalWaktu: new Date().toISOString(),
+ kronologi: "",
+ status: "Proses",
+ penanganan: "",
+};
+
+
+const laporanPublikState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(laporanPublikState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ laporanPublikState.create.loading = true;
+
+ // Ensure we have a valid date
+ if (!laporanPublikState.create.form.tanggalWaktu) {
+ return toast.error("Tanggal laporan harus diisi");
+ }
+
+ // Format the data before sending
+ const formData = {
+ ...laporanPublikState.create.form,
+ // Ensure the date is in the correct format for the API
+ tanggalWaktu: new Date(laporanPublikState.create.form.tanggalWaktu).toISOString()
+ };
+
+ console.log("Sending form data:", formData); // Debug log
+
+ const res = await ApiFetch.api.keamanan.laporanpublik["create"].post(
+ formData
+ );
+
+ if (res.error) {
+ console.error("API Error:", res.error);
+ throw new Error("Failed to create laporan publik");
+ }
+
+ if (res.status === 200) {
+ laporanPublikState.findMany.load();
+ return toast.success("success create");
+ }
+
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.error("Error creating laporan publik:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal membuat laporan publik");
+ throw error; // Re-throw to be handled by the caller
+ } finally {
+ laporanPublikState.create.loading = false;
+ }
+ },
+ resetForm() {
+ laporanPublikState.create.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.LaporanPublikGetPayload<{
+ include: { penanganan: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ laporanPublikState.findMany.loading = true; // ✅ Akses langsung via nama path
+ laporanPublikState.findMany.page = page;
+ laporanPublikState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ laporanPublikState.findMany.data = res.data.data ?? [];
+ laporanPublikState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ laporanPublikState.findMany.data = [];
+ laporanPublikState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch laporan publik paginated:", err);
+ laporanPublikState.findMany.data = [];
+ laporanPublikState.findMany.totalPages = 1;
+ } finally {
+ laporanPublikState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.LaporanPublikGetPayload<{
+ include: { penanganan: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/laporanpublik/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ laporanPublikState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ laporanPublikState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ laporanPublikState.findUnique.data = null;
+ }
+ },
+ resetForm() {
+ laporanPublikState.findUnique.data = null;
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ laporanPublikState.delete.loading = true;
+ const response = await fetch(`/api/keamanan/laporanpublik/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Laporan publik berhasil dihapus"
+ );
+ await laporanPublikState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus laporan publik");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus laporan publik");
+ } finally {
+ laporanPublikState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...editForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/keamanan/laporanpublik/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ lokasi: data.lokasi,
+ tanggalWaktu: data.tanggalWaktu,
+ status: data.status,
+ penanganan: data.penanganan,
+ kronologi: data.kronologi,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading keamanan lingkungan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(laporanPublikState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ laporanPublikState.edit.loading = true;
+
+ const response = await fetch(
+ `/api/keamanan/laporanpublik/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ lokasi: this.form.lokasi,
+ tanggalWaktu: this.form.tanggalWaktu,
+ status: this.form.status,
+ penanganan: this.form.penanganan,
+ kronologi: this.form.kronologi,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update laporan publik");
+ await laporanPublikState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update laporan publik");
+ }
+ } catch (error) {
+ console.error("Error updating laporan publik:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update laporan publik"
+ );
+ return false;
+ } finally {
+ laporanPublikState.edit.loading = false;
+ }
+ },
+ reset() {
+ laporanPublikState.edit.id = "";
+ laporanPublikState.edit.form = { ...editForm };
+ },
+ }
+});
+export default laporanPublikState;
diff --git a/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts b/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts
new file mode 100644
index 00000000..8530d809
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts
@@ -0,0 +1,285 @@
+/* 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({
+ judul: z.string().min(1, "Judul minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
+ linkVideo: z.string().min(1, "Link video minimal 1 karakter"),
+});
+
+const defaultForm = {
+ judul: "",
+ deskripsi: "",
+ deskripsiSingkat: "",
+ linkVideo: "",
+};
+
+const pencegahanKriminalitasState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(
+ pencegahanKriminalitasState.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pencegahanKriminalitasState.create.loading = true;
+ const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
+ "create"
+ ].post(pencegahanKriminalitasState.create.form);
+ if (res.status === 200) {
+ pencegahanKriminalitasState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ pencegahanKriminalitasState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.PencegahanKriminalitasGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ pencegahanKriminalitasState.findMany.loading = true; // ✅ Akses langsung via nama path
+ pencegahanKriminalitasState.findMany.page = page;
+ pencegahanKriminalitasState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pencegahanKriminalitasState.findMany.data = res.data.data ?? [];
+ pencegahanKriminalitasState.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ pencegahanKriminalitasState.findMany.data = [];
+ pencegahanKriminalitasState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch pencegahan kriminalitas paginated:", err);
+ pencegahanKriminalitasState.findMany.data = [];
+ pencegahanKriminalitasState.findMany.totalPages = 1;
+ } finally {
+ pencegahanKriminalitasState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PencegahanKriminalitasGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/pencegahankriminalitas/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ pencegahanKriminalitasState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ pencegahanKriminalitasState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ pencegahanKriminalitasState.findUnique.data = null;
+ }
+ },
+ },
+ findFirst: {
+ data: null as Prisma.PencegahanKriminalitasGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ loading: false,
+ // findFirst.load()
+ async load() {
+ this.loading = true;
+ try {
+ const res = await ApiFetch.api.keamanan.pencegahankriminalitas["find-first"].get();
+
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data || null;
+ } else {
+ this.data = null;
+ }
+ } catch (err) {
+ console.error("Gagal fetch pencegahan kriminalitas terbaru:", err);
+ this.data = null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pencegahanKriminalitasState.delete.loading = true;
+ const response = await fetch(
+ `/api/keamanan/pencegahankriminalitas/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Pencegahan kriminalitas berhasil dihapus"
+ );
+ await pencegahanKriminalitasState.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus pencegahan kriminalitas"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pencegahan kriminalitas");
+ } finally {
+ pencegahanKriminalitasState.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ pencegahanKriminalitasState.update.loading = true;
+ const response = await fetch(
+ `/api/keamanan/pencegahankriminalitas/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ pencegahanKriminalitasState.update.id = data.id;
+ pencegahanKriminalitasState.update.form = {
+ judul: data.judul,
+ deskripsi: data.deskripsi,
+ deskripsiSingkat: data.deskripsiSingkat,
+ linkVideo: data.linkVideo,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Gagal update:", error);
+ toast.error(
+ "Terjadi kesalahan saat mengupdate pencegahan kriminalitas"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(
+ pencegahanKriminalitasState.update.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ pencegahanKriminalitasState.update.loading = true;
+ const response = await fetch(
+ `/api/keamanan/pencegahankriminalitas/${pencegahanKriminalitasState.update.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: pencegahanKriminalitasState.update.form.judul,
+ deskripsi: pencegahanKriminalitasState.update.form.deskripsi,
+ deskripsiSingkat:
+ pencegahanKriminalitasState.update.form.deskripsiSingkat,
+ linkVideo: pencegahanKriminalitasState.update.form.linkVideo,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update pencegahan kriminalitas");
+ await pencegahanKriminalitasState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate pencegahan kriminalitas"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal update:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate pencegahan kriminalitas"
+ );
+ return false;
+ } finally {
+ pencegahanKriminalitasState.update.loading = false;
+ }
+ },
+ reset() {
+ pencegahanKriminalitasState.update.id = "";
+ pencegahanKriminalitasState.update.form = { ...defaultForm };
+ },
+ },
+});
+export default pencegahanKriminalitasState;
diff --git a/src/app/admin/(dashboard)/_state/keamanan/polsek-terdekat.ts b/src/app/admin/(dashboard)/_state/keamanan/polsek-terdekat.ts
new file mode 100644
index 00000000..ad0c696f
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/keamanan/polsek-terdekat.ts
@@ -0,0 +1,294 @@
+/* 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 minimal 1 karakter"),
+ jarakKeDesa: z.string().min(1, "Jarak minimal 1 karakter"),
+ alamat: z.string().min(1, "Alamat minimal 1 karakter"),
+ nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
+ jamOperasional: z.string().min(1, "Jam Operasional minimal 1 karakter"),
+ embedMapUrl: z.string().min(1, "Embed Map Url minimal 1 karakter"),
+ namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
+ alamatMaps: z.string().min(1, "Alamat Maps minimal 1 karakter"),
+ linkPetunjukArah: z.string().min(1, "Link Petunjuk Arah minimal 1 karakter"),
+ layananPolsekId: z.string().min(1, "Layanan Polsek Id minimal 1 karakter"),
+});
+
+const defaultForm = {
+ nama: "",
+ jarakKeDesa: "",
+ alamat: "",
+ nomorTelepon: "",
+ jamOperasional: "",
+ embedMapUrl: "",
+ namaTempatMaps: "",
+ alamatMaps: "",
+ linkPetunjukArah: "",
+ layananPolsekId: "",
+};
+
+const polsekTerdekatState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(polsekTerdekatState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ polsekTerdekatState.create.loading = true;
+ const res = await ApiFetch.api.keamanan.polsekterdekat["create"].post(
+ polsekTerdekatState.create.form
+ );
+ if (res.status === 200) {
+ polsekTerdekatState.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ polsekTerdekatState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.PolsekTerdekatGetPayload<{
+ include: {
+ layananPolsek: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ polsekTerdekatState.findMany.loading = true; // ✅ Akses langsung via nama path
+ polsekTerdekatState.findMany.page = page;
+ polsekTerdekatState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get(
+ { query }
+ );
+
+ if (res.status === 200 && res.data?.success) {
+ polsekTerdekatState.findMany.data = res.data.data ?? [];
+ polsekTerdekatState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ polsekTerdekatState.findMany.data = [];
+ polsekTerdekatState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch polsek terdekat paginated:", err);
+ polsekTerdekatState.findMany.data = [];
+ polsekTerdekatState.findMany.totalPages = 1;
+ } finally {
+ polsekTerdekatState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PolsekTerdekatGetPayload<{
+ include: { layananPolsek: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/polsekterdekat/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ polsekTerdekatState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ polsekTerdekatState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ polsekTerdekatState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ polsekTerdekatState.delete.loading = true;
+
+ const response = await fetch(`/api/keamanan/polsekterdekat/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Polsek terdekat berhasil dihapus");
+ await polsekTerdekatState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus polsek terdekat");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus polsek terdekat");
+ } finally {
+ polsekTerdekatState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/keamanan/polsekterdekat/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ jarakKeDesa: data.jarakKeDesa,
+ alamat: data.alamat,
+ nomorTelepon: data.nomorTelepon,
+ jamOperasional: data.jamOperasional,
+ embedMapUrl: data.embedMapUrl,
+ namaTempatMaps: data.namaTempatMaps,
+ alamatMaps: data.alamatMaps,
+ linkPetunjukArah: data.linkPetunjukArah,
+ layananPolsekId: data.layananPolsekId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading polsek terdekat:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(polsekTerdekatState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ polsekTerdekatState.edit.loading = true;
+ const response = await fetch(
+ `/api/keamanan/polsekterdekat/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ jarakKeDesa: this.form.jarakKeDesa,
+ alamat: this.form.alamat,
+ nomorTelepon: this.form.nomorTelepon,
+ jamOperasional: this.form.jamOperasional,
+ embedMapUrl: this.form.embedMapUrl,
+ namaTempatMaps: this.form.namaTempatMaps,
+ alamatMaps: this.form.alamatMaps,
+ linkPetunjukArah: this.form.linkPetunjukArah,
+ layananPolsekId: this.form.layananPolsekId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update polsek terdekat");
+ await polsekTerdekatState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate polsek terdekat");
+ }
+ } catch (error) {
+ console.error("Error updating polsek terdekat:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate polsek terdekat"
+ );
+ return false;
+ } finally {
+ polsekTerdekatState.edit.loading = false;
+ }
+ },
+ reset() {
+ polsekTerdekatState.edit.id = "";
+ polsekTerdekatState.edit.form = { ...defaultForm };
+ },
+ },
+ findFirst: {
+ data: null as Prisma.PolsekTerdekatGetPayload<{
+ include: {
+ layananPolsek: true;
+ };
+ }> | null,
+ loading: false,
+ load: async () => { // Changed to arrow function
+ polsekTerdekatState.findFirst.loading = true;
+ try {
+ const res = await ApiFetch.api.keamanan.polsekterdekat["find-first"].get();
+ if (res.status === 200 && res.data?.success) {
+ polsekTerdekatState.findFirst.data = res.data.data || null;
+ } else {
+ polsekTerdekatState.findFirst.data = null;
+ }
+ } catch (err) {
+ console.error("Gagal fetch polsek terdekat terbaru:", err);
+ } finally {
+ polsekTerdekatState.findFirst.loading = false;
+ }
+ }
+ }
+});
+
+export default polsekTerdekatState;
diff --git a/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts b/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts
new file mode 100644
index 00000000..ad1ee158
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts
@@ -0,0 +1,237 @@
+/* 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({
+ judul: z.string().min(3, "Nama minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+});
+
+const defaultForm = {
+ judul: "",
+ deskripsi: "",
+ imageId: "",
+};
+
+const tipsKeamananState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(tipsKeamananState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ tipsKeamananState.create.loading = true;
+ const res = await ApiFetch.api.keamanan.menutipskeamanan["create"].post(
+ tipsKeamananState.create.form
+ );
+ if (res.status === 200) {
+ tipsKeamananState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ tipsKeamananState.create.loading = false;
+ }
+ },
+ resetForm() {
+ tipsKeamananState.create.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.MenuTipsKeamananGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ tipsKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
+ tipsKeamananState.findMany.page = page;
+ tipsKeamananState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.keamanan.menutipskeamanan["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ tipsKeamananState.findMany.data = res.data.data ?? [];
+ tipsKeamananState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ tipsKeamananState.findMany.data = [];
+ tipsKeamananState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch menu tips keamanan paginated:", err);
+ tipsKeamananState.findMany.data = [];
+ tipsKeamananState.findMany.totalPages = 1;
+ } finally {
+ tipsKeamananState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.MenuTipsKeamananGetPayload<{
+ include: { image: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/keamanan/menutipskeamanan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ tipsKeamananState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ tipsKeamananState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ tipsKeamananState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ tipsKeamananState.delete.loading = true;
+ const response = await fetch(
+ `/api/keamanan/menutipskeamanan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ const result = await response.json();
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Tips keamanan berhasil dihapus");
+ await tipsKeamananState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus tips keamanan");
+ }
+ } catch (error) {
+ toast.error("Terjadi kesalahan saat menghapus tips keamanan");
+ console.error("Gagal delete:", error);
+ } finally {
+ tipsKeamananState.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ loading: false,
+ form: { ...defaultForm },
+
+ async load(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ tipsKeamananState.update.loading = true;
+ const response = await fetch(`/api/keamanan/menutipskeamanan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId || "",
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ toast.error("Gagal memuat data");
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(tipsKeamananState.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ tipsKeamananState.update.loading = true;
+ const response = await fetch(
+ `/api/keamanan/menutipskeamanan/${tipsKeamananState.update.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update tips keamanan");
+ await tipsKeamananState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update tips keamanan");
+ }
+ } catch (error) {
+ console.error("Error updating data:", error);
+ toast.error("Gagal update data");
+ return false;
+ } finally {
+ tipsKeamananState.update.loading = false;
+ }
+ },
+ reset() {
+ tipsKeamananState.update.id = "";
+ tipsKeamananState.update.form = { ...defaultForm };
+ },
+ },
+});
+export default tipsKeamananState;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan.ts
new file mode 100644
index 00000000..e0f4452b
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan.ts
@@ -0,0 +1,321 @@
+/* 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({
+ title: z.string().min(1, "Judul harus diisi"),
+ content: z.string().min(1, "Content harus diisi"),
+ introduction: z.object({
+ content: z.string().min(1, "Content harus diisi"),
+ }),
+ symptom: z.object({
+ title: z.string().min(1, "Judul harus diisi"),
+ content: z.string().min(1, "Content harus diisi"),
+ }),
+ prevention: z.object({
+ title: z.string().min(1, "Judul harus diisi"),
+ content: z.string().min(1, "Content harus diisi"),
+ }),
+ firstAid: z.object({
+ title: z.string().min(1, "Judul harus diisi"),
+ content: z.string().min(1, "Content harus diisi"),
+ }),
+ mythVsFact: z.object({
+ title: z.string().min(1, "Judul harus diisi"),
+ mitos: z.string().min(1, "Mitos harus diisi"),
+ fakta: z.string().min(1, "Fakta harus diisi"),
+ }),
+ doctorSign: z.object({
+ content: z.string().min(1, "Content harus diisi"),
+ }),
+ imageId: z.string().min(1, "Image ID harus diisi"),
+});
+
+const defaultForm = {
+ title: "",
+ content: "",
+ imageId: "",
+ introduction: {
+ content: "",
+ },
+ symptom: {
+ title: "",
+ content: "",
+ },
+ prevention: {
+ title: "",
+ content: "",
+ },
+ firstAid: {
+ title: "",
+ content: "",
+ },
+ mythVsFact: {
+ title: "",
+ mitos: "",
+ fakta: "",
+ },
+ doctorSign: {
+ content: "",
+ },
+
+};
+
+const artikelKesehatanState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const payload = { ...this.form };
+
+ const res = await (ApiFetch.api.kesehatan as any)[
+ "artikel-kesehatan"
+ ].create.post(payload);
+
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan artikel kesehatan");
+ this.resetForm();
+ await artikelKesehatanState.findMany.load();
+ return res.data;
+ }
+ } catch (err: any) {
+ const msg = err?.message || "Terjadi kesalahan saat mengirim data";
+ toast.error(msg);
+ console.error("SUBMIT ERROR:", err);
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ resetForm() {
+ this.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.ArtikelKesehatanGetPayload<{
+ include: {
+ introduction: true;
+ symptom: true;
+ prevention: true;
+ firstaid: true;
+ mythvsfact: true;
+ doctorsign: true;
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ artikelKesehatanState.findMany.loading = true; // ✅ Akses langsung via nama path
+ artikelKesehatanState.findMany.page = page;
+ artikelKesehatanState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan["artikel-kesehatan"][
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ artikelKesehatanState.findMany.data =
+ res.data.data ?? [];
+ artikelKesehatanState.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ artikelKesehatanState.findMany.data = [];
+ artikelKesehatanState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch artikel kesehatan paginated:", err);
+ artikelKesehatanState.findMany.data = [];
+ artikelKesehatanState.findMany.totalPages = 1;
+ } finally {
+ artikelKesehatanState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ArtikelKesehatanGetPayload<{
+ include: {
+ introduction: true;
+ symptom: true;
+ prevention: true;
+ firstaid: true;
+ mythvsfact: true;
+ doctorsign: true;
+ image: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/artikel-kesehatan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ artikelKesehatanState.findUnique.data = data.data ?? null;
+ } else {
+ toast.error("Gagal load data artikel kesehatan");
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/artikel-kesehatan/${id}`);
+ if (!res.ok) {
+ toast.error("Gagal load data artikel kesehatan");
+ return;
+ }
+
+ const result = await res.json();
+ const data = result.data;
+
+ artikelKesehatanState.edit.id = data.id;
+ artikelKesehatanState.edit.form = {
+ title: data.title,
+ content: data.content,
+ introduction: {
+ content: data.introduction.content,
+ },
+ symptom: {
+ title: data.symptom.title,
+ content: data.symptom.content,
+ },
+ prevention: {
+ title: data.prevention.title,
+ content: data.prevention.content,
+ },
+ firstAid: {
+ title: data.firstaid.title,
+ content: data.firstaid.content,
+ },
+ mythVsFact: {
+ title: data.mythvsfact.title,
+ mitos: data.mythvsfact.mitos,
+ fakta: data.mythvsfact.fakta,
+ },
+ doctorSign: {
+ content: data.doctorsign.content,
+ },
+ imageId: data.imageId,
+ };
+ },
+ async submit() {
+ const cek = templateForm.safeParse(artikelKesehatanState.edit.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ artikelKesehatanState.edit.loading = true;
+ const payload = {
+ title: artikelKesehatanState.edit.form.title,
+ content: artikelKesehatanState.edit.form.content,
+ introduction: {
+ content: artikelKesehatanState.edit.form.introduction.content,
+ },
+ symptom: {
+ title: artikelKesehatanState.edit.form.symptom.title,
+ content: artikelKesehatanState.edit.form.symptom.content,
+ },
+ prevention: {
+ title: artikelKesehatanState.edit.form.prevention.title,
+ content: artikelKesehatanState.edit.form.prevention.content,
+ },
+ firstAid: {
+ title: artikelKesehatanState.edit.form.firstAid.title,
+ content: artikelKesehatanState.edit.form.firstAid.content,
+ },
+ mythVsFact: {
+ title: artikelKesehatanState.edit.form.mythVsFact.title,
+ mitos: artikelKesehatanState.edit.form.mythVsFact.mitos,
+ fakta: artikelKesehatanState.edit.form.mythVsFact.fakta,
+ },
+ doctorSign: {
+ content: artikelKesehatanState.edit.form.doctorSign.content,
+ },
+ imageId: artikelKesehatanState.edit.form.imageId,
+ };
+
+ const res = await fetch(
+ `/api/kesehatan/artikel-kesehatan/${artikelKesehatanState.edit.id}`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ }
+ );
+
+ if (!res.ok) {
+ const error = await res.json();
+ throw new Error(error.message || "Update gagal");
+ }
+
+ toast.success("Berhasil update artikel kesehatan");
+ await artikelKesehatanState.findMany.load();
+ return true;
+ } catch (err) {
+ toast.error(
+ err instanceof Error ? err.message : "Terjadi kesalahan saat update"
+ );
+ return false;
+ } finally {
+ artikelKesehatanState.edit.loading = false;
+ }
+ },
+ resetForm() {
+ artikelKesehatanState.edit.id = "";
+ artikelKesehatanState.edit.form = { ...defaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ artikelKesehatanState.delete.loading = true;
+ const res = await fetch(`/api/kesehatan/artikel-kesehatan/del/${id}`, {
+ method: "DELETE",
+ });
+
+ const result = await res.json();
+ if (res.ok && result.success) {
+ toast.success("Artikel kesehatan berhasil dihapus");
+ await artikelKesehatanState.findMany.load();
+ } else {
+ toast.error(result.message || "Gagal menghapus");
+ }
+ } catch {
+ toast.error("Terjadi kesalahan saat menghapus");
+ } finally {
+ artikelKesehatanState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default artikelKesehatanState;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts
new file mode 100644
index 00000000..71c04389
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts
@@ -0,0 +1,575 @@
+/* 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";
+
+//fasilitas kesehatan aja
+// Validasi form
+const templateForm = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+ informasiUmum: z.object({
+ fasilitas: z.string().min(1, "Fasilitas harus diisi"),
+ alamat: z.string().min(1, "Alamat harus diisi"),
+ jamOperasional: z.string().min(1, "Jam operasional harus diisi"),
+ }),
+ layananUnggulan: z.object({
+ content: z.string().min(1, "Layanan unggulan harus diisi"),
+ }),
+ dokterdanTenagaMedis: z.object({
+ name: z.string().min(1, "Nama dokter harus diisi"),
+ specialist: z.string().min(1, "Spesialis harus diisi"),
+ jadwal: z.string().min(1, "Jadwal harus diisi"),
+ }),
+ fasilitasPendukung: z.object({
+ content: z.string().min(1, "Fasilitas pendukung harus diisi"),
+ }),
+ prosedurPendaftaran: z.object({
+ content: z.string().min(1, "Prosedur pendaftaran harus diisi"),
+ }),
+ tarifDanLayanan: z.object({
+ layanan: z.string().min(1, "Layanan harus diisi"),
+ tarif: z.string().min(1, "Tarif harus diisi"),
+ }),
+});
+
+// Default form kosong
+const defaultForm = {
+ name: "",
+ informasiUmum: {
+ fasilitas: "",
+ alamat: "",
+ jamOperasional: "",
+ },
+ layananUnggulan: {
+ content: "",
+ },
+ dokterdanTenagaMedis: {
+ name: "",
+ specialist: "",
+ jadwal: "",
+ },
+ fasilitasPendukung: {
+ content: "",
+ },
+ prosedurPendaftaran: {
+ content: "",
+ },
+ tarifDanLayanan: {
+ layanan: "",
+ tarif: "",
+ },
+};
+
+const fasilitasKesehatan = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const payload = { ...this.form };
+
+ const res = await (ApiFetch.api.kesehatan as any)[
+ "fasilitas-kesehatan"
+ ].create.post(payload);
+
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan fasilitas kesehatan");
+ this.resetForm();
+ await fasilitasKesehatan.findMany.load();
+ return res.data;
+ }
+ } catch (err: any) {
+ const msg = err?.message || "Terjadi kesalahan saat mengirim data";
+ toast.error(msg);
+ console.error("SUBMIT ERROR:", err);
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ resetForm() {
+ this.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.FasilitasKesehatanGetPayload<{
+ include: {
+ informasiumum: true;
+ layananunggulan: true;
+ dokterdantenagamedis: true;
+ fasilitaspendukung: true;
+ prosedurpendaftaran: true;
+ tarifdanlayanan: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.page = page;
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan["fasilitas-kesehatan"][
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.data =
+ res.data.data ?? [];
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch fasilitas kesehatan paginated:", err);
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
+ } finally {
+ fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.FasilitasKesehatanGetPayload<{
+ include: {
+ informasiumum: true;
+ layananunggulan: true;
+ dokterdantenagamedis: true;
+ fasilitaspendukung: true;
+ prosedurpendaftaran: true;
+ tarifdanlayanan: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ fasilitasKesehatan.findUnique.data = data.data ?? null;
+ } else {
+ toast.error("Gagal load data fasilitas kesehatan");
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
+ if (!res.ok) {
+ toast.error("Gagal load data fasilitas kesehatan");
+ return;
+ }
+
+ const result = await res.json();
+ const data = result.data;
+
+ fasilitasKesehatan.edit.id = data.id;
+ fasilitasKesehatan.edit.form = {
+ name: data.name,
+ informasiUmum: {
+ fasilitas: data.informasiumum.fasilitas,
+ alamat: data.informasiumum.alamat,
+ jamOperasional: data.informasiumum.jamOperasional,
+ },
+ layananUnggulan: {
+ content: data.layananunggulan.content,
+ },
+ dokterdanTenagaMedis: {
+ name: data.dokterdantenagamedis.name,
+ specialist: data.dokterdantenagamedis.specialist,
+ jadwal: data.dokterdantenagamedis.jadwal,
+ },
+ fasilitasPendukung: {
+ content: data.fasilitaspendukung.content,
+ },
+ prosedurPendaftaran: {
+ content: data.prosedurpendaftaran.content,
+ },
+ tarifDanLayanan: {
+ layanan: data.tarifdanlayanan.layanan,
+ tarif: data.tarifdanlayanan.tarif,
+ },
+ };
+ },
+ async submit() {
+ const cek = templateForm.safeParse(fasilitasKesehatan.edit.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ fasilitasKesehatan.edit.loading = true;
+ const payload = {
+ name: fasilitasKesehatan.edit.form.name,
+ informasiUmum: {
+ fasilitas: fasilitasKesehatan.edit.form.informasiUmum.fasilitas,
+ alamat: fasilitasKesehatan.edit.form.informasiUmum.alamat,
+ jamOperasional:
+ fasilitasKesehatan.edit.form.informasiUmum.jamOperasional,
+ },
+ layananUnggulan: {
+ content: fasilitasKesehatan.edit.form.layananUnggulan.content,
+ },
+ dokterdanTenagaMedis: {
+ name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
+ specialist:
+ fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
+ jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
+ },
+ fasilitasPendukung: {
+ content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
+ },
+ prosedurPendaftaran: {
+ content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
+ },
+ tarifDanLayanan: {
+ layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
+ tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
+ },
+ };
+
+ const res = await fetch(
+ `/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatan.edit.id}`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ }
+ );
+
+ if (!res.ok) {
+ const error = await res.json();
+ throw new Error(error.message || "Update gagal");
+ }
+
+ toast.success("Berhasil update fasilitas kesehatan");
+ await fasilitasKesehatan.findMany.load();
+ return true;
+ } catch (err) {
+ toast.error(
+ err instanceof Error ? err.message : "Terjadi kesalahan saat update"
+ );
+ return false;
+ } finally {
+ fasilitasKesehatan.edit.loading = false;
+ }
+ },
+ resetForm() {
+ fasilitasKesehatan.edit.id = "";
+ fasilitasKesehatan.edit.form = { ...defaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ fasilitasKesehatan.delete.loading = true;
+ const res = await fetch(
+ `/api/kesehatan/fasilitas-kesehatan/del/${id}`,
+ {
+ method: "DELETE",
+ }
+ );
+
+ const result = await res.json();
+ if (res.ok && result.success) {
+ toast.success("Fasilitas kesehatan berhasil dihapus");
+ await fasilitasKesehatan.findMany.load();
+ } else {
+ toast.error(result.message || "Gagal menghapus");
+ }
+ } catch {
+ toast.error("Terjadi kesalahan saat menghapus");
+ } finally {
+ fasilitasKesehatan.delete.loading = false;
+ }
+ },
+ },
+});
+
+//dokter & tenaga medis
+const templateDokterForm = z.object({
+ name: z.string().min(1, "Nama tidak boleh kosong"),
+ specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
+ jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
+});
+
+const defaultDokterForm = {
+ name: "",
+ specialist: "",
+ jadwal: "",
+};
+
+const dokter = proxy({
+ create: {
+ create: {
+ form: defaultDokterForm,
+ loading: false,
+ async create() {
+ const cek = templateDokterForm.safeParse(dokter.create.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ try {
+ dokter.create.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.doktertenagamedis[
+ "create"
+ ].post(dokter.create.create.form);
+
+ if (res.status === 200) {
+ const id = res.data?.data;
+ if (id) {
+ toast.success("Success create");
+ dokter.create.create.form = { ...defaultDokterForm };
+ dokter.findMany.load();
+ return id;
+ }
+ }
+ toast.error("failed create");
+ return null;
+ } catch (error) {
+ console.log((error as Error).message);
+ return null;
+ } finally {
+ dokter.create.create.loading = false;
+ }
+ },
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.DokterdanTenagaMedisGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ dokter.findMany.loading = true; // ✅ Akses langsung via nama path
+ dokter.findMany.page = page;
+ dokter.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.doktertenagamedis[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ dokter.findMany.data = res.data.data ?? [];
+ dokter.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ dokter.findMany.data = [];
+ dokter.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch dokter tenaga medis paginated:", err);
+ dokter.findMany.data = [];
+ dokter.findMany.totalPages = 1;
+ } finally {
+ dokter.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DokterdanTenagaMedisGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ dokter.findUnique.data = data.data ?? null;
+ } else {
+ console.error(
+ "Failed to fetch dokter dan tenaga medis",
+ res.statusText
+ );
+ dokter.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching dokter dan tenaga medis", error);
+ dokter.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultDokterForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ specialist: data.specialist,
+ jadwal: data.jadwal,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading dokter dan tenaga medis:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ const formData = {
+ name: this.form.name,
+ specialist: this.form.specialist,
+ jadwal: this.form.jadwal,
+ };
+
+ const cek = templateDokterForm.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const result = await res.json();
+
+ if (!res.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+ await dokter.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Update error:", error);
+ toast.error("Gagal update data dokter dan tenaga medis");
+ throw error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) {
+ return toast.warn("ID tidak valid");
+ }
+ try {
+ dokter.delete.loading = true;
+
+ const response = await fetch(
+ `/api/kesehatan/doktertenagamedis/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Dokter dan tenaga medis berhasil dihapus"
+ );
+ await dokter.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus dokter dan tenaga medis"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus dokter dan tenaga medis");
+ } finally {
+ dokter.delete.loading = false;
+ }
+ },
+ },
+});
+
+const fasilitasKesehatanState = proxy({
+ fasilitasKesehatan,
+ dokter,
+});
+
+export default fasilitasKesehatanState;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts
new file mode 100644
index 00000000..19f7e80a
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts
@@ -0,0 +1,258 @@
+/* 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 templateGrafikKepuasan = z.object({
+ nama: z.string().min(2, "Nama harus diisi"),
+ tanggal: z.string().min(1, "Tanggal harus diisi"),
+ jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
+ alamat: z.string().min(1, "Alamat harus diisi"),
+ penyakit: z.string().min(1, "Penyakit harus diisi"),
+});
+
+const defaultForm = {
+ nama: "",
+ tanggal: "",
+ jenisKelamin: "",
+ alamat: "",
+ penyakit: "",
+};
+
+const grafikkepuasan = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templateGrafikKepuasan.safeParse(grafikkepuasan.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ try {
+ grafikkepuasan.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(
+ grafikkepuasan.create.form
+ );
+
+ if (res.status === 200) {
+ const id = res.data?.data;
+ if (id) {
+ toast.success("Success create");
+ grafikkepuasan.create.form = { ...defaultForm };
+ grafikkepuasan.findMany.load();
+ return id;
+ }
+ }
+ toast.error("failed create");
+ return null;
+ } catch (error) {
+ console.log((error as Error).message);
+ return null;
+ } finally {
+ grafikkepuasan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.GrafikKepuasanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ grafikkepuasan.findMany.loading = true; // ✅ Akses langsung via nama path
+ grafikkepuasan.findMany.page = page;
+ grafikkepuasan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.grafikkepuasan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikkepuasan.findMany.data = res.data.data ?? [];
+ grafikkepuasan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ grafikkepuasan.findMany.data = [];
+ grafikkepuasan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita paginated:", err);
+ grafikkepuasan.findMany.data = [];
+ grafikkepuasan.findMany.totalPages = 1;
+ } finally {
+ grafikkepuasan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikKepuasanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ grafikkepuasan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch grafikkepuasan:", res.statusText);
+ grafikkepuasan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching grafikkepuasan:", error);
+ grafikkepuasan.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ tanggal: data.tanggal,
+ jenisKelamin: data.jenisKelamin,
+ alamat: data.alamat,
+ penyakit: data.penyakit,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading grafik kepuasan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ const formData = {
+ nama: this.form.nama,
+ tanggal: this.form.tanggal,
+ jenisKelamin: this.form.jenisKelamin,
+ alamat: this.form.alamat,
+ penyakit: this.form.penyakit,
+ };
+
+ const cek = templateGrafikKepuasan.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const result = await res.json();
+
+ if (!res.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+ await grafikkepuasan.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Update error:", error);
+ toast.error("Gagal update data grafik kepuasan");
+ throw error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) {
+ return toast.warn("ID tidak valid");
+ }
+ try {
+ grafikkepuasan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/kesehatan/grafikkepuasan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Grafik kepuasan berhasil dihapus");
+ await grafikkepuasan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus grafik kepuasan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus grafik kepuasan");
+ } finally {
+ grafikkepuasan.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default grafikkepuasan;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan.ts
new file mode 100644
index 00000000..3e8d8fc5
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan.ts
@@ -0,0 +1,294 @@
+/* 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";
+
+/* Informasi Kegiatan */
+const templateForm = z.object({
+ content: z.string().min(1, "Content minimal 1 karakter"),
+ informasiJadwalKegiatan: z.object({
+ name: z.string().min(1, "Name minimal 1 karakter"),
+ tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
+ waktu: z.string().min(1, "Waktu minimal 1 karakter"),
+ lokasi: z.string().min(1, "Lokasi minimal 1 karakter"),
+ }),
+ deskripsiJadwalKegiatan: z.object({
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ }),
+ layananJadwalKegiatan: z.object({
+ content: z.string().min(1, "Content minimal 1 karakter"),
+ }),
+ syaratKetentuanJadwalKegiatan: z.object({
+ content: z.string().min(1, "Content minimal 1 karakter"),
+ }),
+ dokumenJadwalKegiatan: z.object({
+ content: z.string().min(1, "Content minimal 1 karakter"),
+ }),
+});
+
+const defaultForm = {
+ content: "",
+ informasiJadwalKegiatan: {
+ name: "",
+ tanggal: "",
+ waktu: "",
+ lokasi: "",
+ },
+ deskripsiJadwalKegiatan: {
+ deskripsi: "",
+ },
+ layananJadwalKegiatan: {
+ content: "",
+ },
+ syaratKetentuanJadwalKegiatan: {
+ content: "",
+ },
+ dokumenJadwalKegiatan: {
+ content: "",
+ }
+};
+
+const jadwalkegiatanState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const payload = { ...this.form };
+
+ const res = await (ApiFetch.api.kesehatan as any)[
+ "jadwal-kegiatan"
+ ].create.post(payload);
+
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan jadwal kegiatan");
+ this.resetForm();
+ await jadwalkegiatanState.findMany.load();
+ return res.data;
+ }
+ } catch (err: any) {
+ const msg = err?.message || "Terjadi kesalahan saat mengirim data";
+ toast.error(msg);
+ console.error("SUBMIT ERROR:", err);
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ resetForm() {
+ this.form = { ...defaultForm };
+ },
+ },
+
+ findMany: {
+ data: null as
+ | Prisma.JadwalKegiatanGetPayload<{
+ include: {
+ informasijadwalkegiatan: true;
+ deskripsijadwalkegiatan: true;
+ layananjadwalkegiatan: true;
+ dokumenjadwalkegiatan: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ jadwalkegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
+ jadwalkegiatanState.findMany.page = page;
+ jadwalkegiatanState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan["jadwal-kegiatan"][
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ jadwalkegiatanState.findMany.data = res.data.data ?? [];
+ jadwalkegiatanState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ jadwalkegiatanState.findMany.data = [];
+ jadwalkegiatanState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch jadwal kegiatan paginated:", err);
+ jadwalkegiatanState.findMany.data = [];
+ jadwalkegiatanState.findMany.totalPages = 1;
+ } finally {
+ jadwalkegiatanState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.JadwalKegiatanGetPayload<{
+ include: {
+ informasijadwalkegiatan: true;
+ deskripsijadwalkegiatan: true;
+ layananjadwalkegiatan: true;
+ syaratketentuanjadwalkegiatan: true;
+ dokumenjadwalkegiatan: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/jadwal-kegiatan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ jadwalkegiatanState.findUnique.data = data.data ?? null;
+ } else {
+ toast.error("Gagal load data jadwal kegiatan");
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/jadwal-kegiatan/${id}`);
+ if (!res.ok) {
+ toast.error("Gagal load data jadwal kegiatan");
+ return;
+ }
+
+ const result = await res.json();
+ const data = result.data;
+
+ jadwalkegiatanState.edit.id = data.id;
+ jadwalkegiatanState.edit.form = {
+ content: data.content,
+ informasiJadwalKegiatan: {
+ name: data.informasijadwalkegiatan.name,
+ tanggal: data.informasijadwalkegiatan.tanggal,
+ waktu: data.informasijadwalkegiatan.waktu,
+ lokasi: data.informasijadwalkegiatan.lokasi,
+ },
+ layananJadwalKegiatan: {
+ content: data.layananjadwalkegiatan.content,
+ },
+ deskripsiJadwalKegiatan: {
+ deskripsi: data.deskripsijadwalkegiatan.deskripsi,
+ },
+ syaratKetentuanJadwalKegiatan: {
+ content: data.syaratketentuanjadwalkegiatan.content,
+ },
+ dokumenJadwalKegiatan: {
+ content: data.dokumenjadwalkegiatan.content,
+ }
+ };
+ },
+ async submit() {
+ const cek = templateForm.safeParse(jadwalkegiatanState.edit.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ jadwalkegiatanState.edit.loading = true;
+ const payload = {
+ content: jadwalkegiatanState.edit.form.content,
+ informasiJadwalKegiatan: {
+ name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name,
+ tanggal:
+ jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
+ waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu,
+ lokasi:
+ jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
+ },
+ layananJadwalKegiatan: {
+ content:
+ jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
+ },
+ deskripsiJadwalKegiatan: {
+ deskripsi:
+ jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
+ },
+ syaratKetentuanJadwalKegiatan: {
+ content:
+ jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan
+ .content,
+ },
+ dokumenJadwalKegiatan: {
+ content:
+ jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
+ },
+ };
+
+ const res = await fetch(
+ `/api/kesehatan/jadwal-kegiatan/${jadwalkegiatanState.edit.id}`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ }
+ );
+
+ if (!res.ok) {
+ const error = await res.json();
+ throw new Error(error.message || "Update gagal");
+ }
+
+ toast.success("Berhasil update jadwal kegiatan");
+ await jadwalkegiatanState.findMany.load();
+ return true;
+ } catch (err) {
+ toast.error(
+ err instanceof Error ? err.message : "Terjadi kesalahan saat update"
+ );
+ return false;
+ } finally {
+ jadwalkegiatanState.edit.loading = false;
+ }
+ },
+ resetForm() {
+ jadwalkegiatanState.edit.id = "";
+ jadwalkegiatanState.edit.form = { ...defaultForm };
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ jadwalkegiatanState.delete.loading = true;
+ const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, {
+ method: "DELETE",
+ });
+
+ const result = await res.json();
+ if (res.ok && result.success) {
+ toast.success("Jadwal kegiatan berhasil dihapus");
+ await jadwalkegiatanState.findMany.load();
+ } else {
+ toast.error(result.message || "Gagal menghapus");
+ }
+ } catch {
+ toast.error("Terjadi kesalahan saat menghapus");
+ } finally {
+ jadwalkegiatanState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default jadwalkegiatanState;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/pendafataranJadwalKegiatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/pendafataranJadwalKegiatan.ts
new file mode 100644
index 00000000..82357206
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/pendafataranJadwalKegiatan.ts
@@ -0,0 +1,290 @@
+/* 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({
+ name: z.string().min(1, "Name minimal 1 karakter"),
+ tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
+ namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
+ nomor: z.string().min(1, "Nomor minimal 1 karakter"),
+ alamat: z.string().min(1, "Alamat minimal 1 karakter"),
+ catatan: z.string().min(1, "Catatan minimal 1 karakter"),
+});
+
+const defaultForm = {
+ name: "",
+ tanggal: "",
+ namaOrangtua: "",
+ nomor: "",
+ alamat: "",
+ catatan: "",
+};
+
+const pendaftaranJadwalKegiatanState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const errMsg = cek.error.issues
+ .map((v) => `${v.path.join(".")}: ${v.message}`)
+ .join("\n");
+ toast.error(errMsg);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const payload = { ...this.form };
+
+ const res = await (ApiFetch.api.kesehatan as any)[
+ "pendaftaran-jadwal-kegiatan"
+ ].create.post(payload);
+
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan jadwal kegiatan");
+ this.resetForm();
+ await pendaftaranJadwalKegiatanState.findMany.load();
+ return res.data;
+ }
+ } catch (err: any) {
+ const msg = err?.message || "Terjadi kesalahan saat mengirim data";
+ toast.error(msg);
+ console.error("SUBMIT ERROR:", err);
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ resetForm() {
+ this.form = { ...defaultForm };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.PendaftaranJadwalKegiatanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ pendaftaranJadwalKegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
+ pendaftaranJadwalKegiatanState.findMany.page = page;
+ pendaftaranJadwalKegiatanState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan["pendaftaran-jadwal-kegiatan"][
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ pendaftaranJadwalKegiatanState.findMany.data = res.data.data ?? [];
+ pendaftaranJadwalKegiatanState.findMany.totalPages =
+ res.data.totalPages ?? 1;
+ } else {
+ pendaftaranJadwalKegiatanState.findMany.data = [];
+ pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error(
+ "Gagal fetch pendaftaran jadwal kegiatan paginated:",
+ err
+ );
+ pendaftaranJadwalKegiatanState.findMany.data = [];
+ pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
+ } finally {
+ pendaftaranJadwalKegiatanState.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PendaftaranJadwalKegiatanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ pendaftaranJadwalKegiatanState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ pendaftaranJadwalKegiatanState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ pendaftaranJadwalKegiatanState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pendaftaranJadwalKegiatanState.delete.loading = true;
+
+ const response = await fetch(
+ `/api/kesehatan/pendaftaran-jadwal-kegiatan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Pendaftaran jadwal kegiatan berhasil dihapus"
+ );
+ await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus pendaftaran jadwal kegiatan"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error(
+ "Terjadi kesalahan saat menghapus pendaftaran jadwal kegiatan"
+ );
+ } finally {
+ pendaftaranJadwalKegiatanState.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ tanggal: data.tanggal,
+ namaOrangtua: data.namaOrangtua,
+ nomor: data.nomor,
+ alamat: data.alamat,
+ catatan: data.catatan,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading pendaftaran jadwal kegiatan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(pendaftaranJadwalKegiatanState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ pendaftaranJadwalKegiatanState.edit.loading = true;
+
+ const response = await fetch(
+ `/api/kesehatan/pendaftaran-jadwal-kegiatan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ tanggal: this.form.tanggal,
+ namaOrangtua: this.form.namaOrangtua,
+ nomor: this.form.nomor,
+ alamat: this.form.alamat,
+ catatan: this.form.catatan,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update pendaftaran jadwal kegiatan");
+ await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update pendaftaran jadwal kegiatan");
+ }
+ } catch (error) {
+ console.error("Error updating pendaftaran jadwal kegiatan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update pendaftaran jadwal kegiatan"
+ );
+ return false;
+ } finally {
+ pendaftaranJadwalKegiatanState.edit.loading = false;
+ }
+ },
+ reset() {
+ pendaftaranJadwalKegiatanState.edit.id = "";
+ pendaftaranJadwalKegiatanState.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default pendaftaranJadwalKegiatanState;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts
new file mode 100644
index 00000000..4da79df8
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts
@@ -0,0 +1,746 @@
+/* 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";
+
+//persentase kelahiran kematian
+
+const templatePersentaseKelahiran = z.object({
+ tahun: z.string().min(4, "Tahun harus diisi"),
+ kematianKasar: z.string().min(1, "Kematian kasar harus diisi"),
+ kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"),
+ kematianBayi: z.string().min(1, "Kematian bayi harus diisi"),
+});
+
+type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
+ select: {
+ kematianId: true;
+ kelahiranId: true;
+ };
+}>;
+
+const defaultForm: Persentase = {
+ kematianId: "",
+ kelahiranId: "",
+};
+
+const persentasekelahiran = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templatePersentaseKelahiran.safeParse(
+ persentasekelahiran.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ try {
+ persentasekelahiran.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.persentasekelahiran[
+ "create"
+ ].post(persentasekelahiran.create.form);
+
+ if (res.status === 200) {
+ const id = res.data?.data;
+ if (id) {
+ toast.success("Success create");
+ persentasekelahiran.create.form = { ...defaultForm };
+ persentasekelahiran.findMany.load();
+ return id;
+ }
+ }
+ toast.error("failed create");
+ return null;
+ } catch (error) {
+ console.log((error as Error).message);
+ return null;
+ } finally {
+ persentasekelahiran.create.loading = false;
+ }
+ },
+ },
+
+ findMany: {
+ data: null as
+ | Prisma.DataKematian_KelahiranGetPayload<{
+ include: {
+ kematian: true;
+ kelahiran: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ persentasekelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
+ persentasekelahiran.findMany.page = page;
+ persentasekelahiran.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.persentasekelahiran[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ persentasekelahiran.findMany.data = res.data.data ?? [];
+ persentasekelahiran.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ persentasekelahiran.findMany.data = [];
+ persentasekelahiran.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita paginated:", err);
+ persentasekelahiran.findMany.data = [];
+ persentasekelahiran.findMany.totalPages = 1;
+ } finally {
+ persentasekelahiran.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DataKematian_KelahiranGetPayload<{
+ include: {
+ kematian: true;
+ kelahiran: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/persentasekelahiran/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ persentasekelahiran.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch persentasekelahiran:", res.statusText);
+ persentasekelahiran.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching persentasekelahiran:", error);
+ persentasekelahiran.findUnique.data = null;
+ }
+ },
+ },
+
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ const formData = {
+ kematianId: this.form.kematianId,
+ kelahiranId: this.form.kelahiranId,
+ };
+
+ const cek = templatePersentaseKelahiran.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ try {
+ this.loading = true;
+ const res = await fetch(`/api/kesehatan/persentasekelahiran/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const result = await res.json();
+
+ if (!res.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+ await persentasekelahiran.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Update error:", error);
+ toast.error("Gagal update data persentase kelahiran");
+ throw error;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ persentasekelahiran.delete.loading = true;
+
+ const response = await fetch(
+ `/api/kesehatan/persentasekelahiran/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Persentase kelahiran berhasil dihapus"
+ );
+ await persentasekelahiran.findMany.load();
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus persentase kelahiran"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus persentase kelahiran");
+ } finally {
+ persentasekelahiran.delete.loading = false;
+ }
+ },
+ },
+});
+
+// data kelahiran
+
+const templateKelahiran = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+ tanggal: z.string().min(4, "Tahun harus diisi"),
+ jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
+ alamat: z.string().min(1, "Alamat harus diisi"),
+});
+
+const defaultKelahiran = {
+ nama: "",
+ tanggal: "",
+ jenisKelamin: "",
+ alamat: "",
+};
+
+const kelahiran = proxy({
+ create: {
+ form: { ...defaultKelahiran }, // ✅ ini kunci fix-nya
+ loading: false,
+ async create() {
+ const cek = templateKelahiran.safeParse(kelahiran.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kelahiran.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.kelahiran["create"].post(
+ kelahiran.create.form
+ );
+ if (res.status === 200) {
+ kelahiran.findMany.load();
+ return toast.success("Kelahiran berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan kelahiran");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ kelahiran.create.loading = false;
+ }
+ },
+ resetForm() {
+ kelahiran.create.form = { ...defaultKelahiran };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.KelahiranGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
+ kelahiran.findMany.page = page;
+ kelahiran.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.kelahiran["findMany"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kelahiran.findMany.data = res.data.data ?? [];
+ kelahiran.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kelahiran.findMany.data = [];
+ kelahiran.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kelahiran paginated:", err);
+ kelahiran.findMany.data = [];
+ kelahiran.findMany.totalPages = 1;
+ } finally {
+ kelahiran.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KelahiranGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/kelahiran/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kelahiran.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch kelahiran:", res.statusText);
+ kelahiran.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching kelahiran:", error);
+ kelahiran.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kelahiran.delete.loading = true;
+
+ const response = await fetch(`/api/kesehatan/kelahiran/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kelahiran berhasil dihapus");
+ await kelahiran.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kelahiran");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kelahiran");
+ } finally {
+ kelahiran.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultKelahiran },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/kelahiran/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ tanggal: data.tanggal,
+ jenisKelamin: data.jenisKelamin,
+ alamat: data.alamat,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading data kelahiran:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateKelahiran.safeParse(kelahiran.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kelahiran.edit.loading = true;
+
+ const response = await fetch(`/api/kesehatan/kelahiran/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ tanggal: this.form.tanggal,
+ jenisKelamin: this.form.jenisKelamin,
+ alamat: this.form.alamat,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data kelahiran");
+ await kelahiran.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update data kelahiran");
+ }
+ } catch (error) {
+ console.error("Error updating data kelahiran:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data kelahiran"
+ );
+ return false;
+ } finally {
+ kelahiran.edit.loading = false;
+ }
+ },
+
+ reset() {
+ kelahiran.edit.id = "";
+ kelahiran.edit.form = { ...defaultKelahiran };
+ },
+ },
+});
+
+
+// data kematian
+
+const templateKematian = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+ tanggal: z.string().min(4, "Tahun harus diisi"),
+ jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
+ alamat: z.string().min(1, "Alamat harus diisi"),
+ penyebab: z.string().min(1, "Penyebab harus diisi"),
+});
+
+const defaultKematian = {
+ nama: "",
+ tanggal: "",
+ jenisKelamin: "",
+ alamat: "",
+ penyebab: "",
+};
+
+const kematian = proxy({
+ create: {
+ form: { ...defaultKematian }, // ✅ ini kunci fix-nya
+ loading: false,
+ async create() {
+ const cek = templateKematian.safeParse(kematian.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kematian.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.kematian["create"].post(
+ kematian.create.form
+ );
+ if (res.status === 200) {
+ kematian.findMany.load();
+ return toast.success("Kematian berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan kematian");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ kematian.create.loading = false;
+ }
+ },
+ resetForm() {
+ kematian.create.form = { ...defaultKematian };
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.KematianGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kematian.findMany.loading = true; // ✅ Akses langsung via nama path
+ kematian.findMany.page = page;
+ kematian.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.kematian["findMany"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kematian.findMany.data = res.data.data ?? [];
+ kematian.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kematian.findMany.data = [];
+ kematian.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kematian paginated:", err);
+ kematian.findMany.data = [];
+ kematian.findMany.totalPages = 1;
+ } finally {
+ kematian.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KematianGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/kematian/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kematian.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch kematian:", res.statusText);
+ kematian.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching kematian:", error);
+ kematian.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kematian.delete.loading = true;
+
+ const response = await fetch(`/api/kesehatan/kematian/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kematian berhasil dihapus");
+ await kematian.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kematian");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kematian");
+ } finally {
+ kematian.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultKematian },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/kematian/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ tanggal: data.tanggal,
+ jenisKelamin: data.jenisKelamin,
+ alamat: data.alamat,
+ penyebab: data.penyebab,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading data kematian:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateKematian.safeParse(kematian.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kematian.edit.loading = true;
+
+ const response = await fetch(`/api/kesehatan/kematian/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ tanggal: this.form.tanggal,
+ jenisKelamin: this.form.jenisKelamin,
+ alamat: this.form.alamat,
+ penyebab: this.form.penyebab,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data kematian");
+ await kematian.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update data kematian");
+ }
+ } catch (error) {
+ console.error("Error updating data kematian:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data kematian"
+ );
+ return false;
+ } finally {
+ kematian.edit.loading = false;
+ }
+ },
+
+ reset() {
+ kematian.edit.id = "";
+ kematian.edit.form = { ...defaultKematian };
+ },
+ },
+});
+
+const persentaseKelahiranKematian = proxy({
+ persentasekelahiran,
+ kelahiran,
+ kematian
+});
+
+export default persentaseKelahiranKematian;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit.ts b/src/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit.ts
new file mode 100644
index 00000000..f2dcb9da
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit.ts
@@ -0,0 +1,239 @@
+/* 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({
+ name: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"),
+ deskripsiLengkap: z.string().min(3, "Deskripsi lengkap minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsiSingkat: "",
+ deskripsiLengkap: "",
+ imageId: "",
+};
+
+const infoWabahPenyakit = proxy({
+ findMany: {
+ data: null as
+ | Prisma.InfoWabahPenyakitGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ infoWabahPenyakit.findMany.loading = true; // ✅ Akses langsung via nama path
+ infoWabahPenyakit.findMany.page = page;
+ infoWabahPenyakit.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.infowabahpenyakit["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ infoWabahPenyakit.findMany.data = res.data.data ?? [];
+ infoWabahPenyakit.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ infoWabahPenyakit.findMany.data = [];
+ infoWabahPenyakit.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch info wabah penyakit paginated:", err);
+ infoWabahPenyakit.findMany.data = [];
+ infoWabahPenyakit.findMany.totalPages = 1;
+ } finally {
+ infoWabahPenyakit.findMany.loading = false;
+ }
+ },
+ },
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(infoWabahPenyakit.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ infoWabahPenyakit.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.infowabahpenyakit[
+ "create"
+ ].post(infoWabahPenyakit.create.form);
+ if (res.status === 200) {
+ infoWabahPenyakit.findMany.load();
+ return toast.success("Info wabah penyakit berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan info wabah penyakit");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ infoWabahPenyakit.create.loading = false;
+ }
+ },
+ resetForm() {
+ infoWabahPenyakit.create.form = { ...defaultForm };
+ },
+ },
+ findUnique: {
+ data: null as Prisma.InfoWabahPenyakitGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/infowabahpenyakit/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ infoWabahPenyakit.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch info wabah penyakit:", res.statusText);
+ infoWabahPenyakit.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching info wabah penyakit:", error);
+ infoWabahPenyakit.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ infoWabahPenyakit.delete.loading = true;
+
+ const response = await fetch(`/api/kesehatan/infowabahpenyakit/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Info wabah penyakit berhasil dihapus");
+ await infoWabahPenyakit.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus info wabah penyakit");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus info wabah penyakit");
+ } finally {
+ infoWabahPenyakit.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/infowabahpenyakit/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsiSingkat: data.deskripsiSingkat,
+ deskripsiLengkap: data.deskripsiLengkap,
+ imageId: data.imageId,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching info wabah penyakit:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal memuat data");
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(infoWabahPenyakit.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ infoWabahPenyakit.edit.loading = true;
+ const response = await fetch(`/api/kesehatan/infowabahpenyakit/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsiSingkat: this.form.deskripsiSingkat,
+ deskripsiLengkap: this.form.deskripsiLengkap,
+ imageId: this.form.imageId,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Info wabah penyakit berhasil diupdate");
+ await infoWabahPenyakit.findMany.load();
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update info wabah penyakit");
+ }
+ } catch (error) {
+ console.error("Gagal update:", error);
+ toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate info wabah penyakit");
+ return false;
+ } finally {
+ infoWabahPenyakit.edit.loading = false;
+ }
+ },
+ reset() {
+ infoWabahPenyakit.edit.id = "";
+ infoWabahPenyakit.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default infoWabahPenyakit;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts b/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts
new file mode 100644
index 00000000..85480912
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts
@@ -0,0 +1,250 @@
+/* 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({
+ name: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+ whatsapp: z.string().min(10, "Whatsapp minimal 10 karakter"),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+ whatsapp: "",
+};
+
+const kontakDarurat = proxy({
+ findMany: {
+ data: null as
+ | Prisma.KontakDaruratGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kontakDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
+ kontakDarurat.findMany.page = page;
+ kontakDarurat.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.kontakdarurat[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kontakDarurat.findMany.data = res.data.data ?? [];
+ kontakDarurat.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kontakDarurat.findMany.data = [];
+ kontakDarurat.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kontak darurat paginated:", err);
+ kontakDarurat.findMany.data = [];
+ kontakDarurat.findMany.totalPages = 1;
+ } finally {
+ kontakDarurat.findMany.loading = false;
+ }
+ },
+ },
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(kontakDarurat.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kontakDarurat.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.kontakdarurat["create"].post(
+ kontakDarurat.create.form
+ );
+ if (res.status === 200) {
+ kontakDarurat.findMany.load();
+ return toast.success("Kontak Darurat berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan kontak darurat");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ kontakDarurat.create.loading = false;
+ }
+ },
+ resetForm() {
+ kontakDarurat.create.form = { ...defaultForm };
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KontakDaruratGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/kontakdarurat/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kontakDarurat.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kontakDarurat.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kontakDarurat.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ kontakDarurat.delete.loading = true;
+ const response = await fetch(`/api/kesehatan/kontakdarurat/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kontak darurat berhasil dihapus");
+ await kontakDarurat.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kontak darurat");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kontak darurat");
+ } finally {
+ kontakDarurat.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/kontakdarurat/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ whatsapp: data.whatsapp,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching kontak darurat:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(kontakDarurat.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kontakDarurat.edit.loading = true;
+ const response = await fetch(
+ `/api/kesehatan/kontakdarurat/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ whatsapp: this.form.whatsapp,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Kontak darurat berhasil diupdate");
+ await kontakDarurat.findMany.load();
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update kontak darurat");
+ }
+ } catch (error) {
+ console.error("Gagal update:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat mengupdate kontak darurat"
+ );
+ return false;
+ } finally {
+ kontakDarurat.edit.loading = false;
+ }
+ },
+ reset() {
+ kontakDarurat.edit.id = "";
+ kontakDarurat.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default kontakDarurat;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat.ts b/src/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat.ts
new file mode 100644
index 00000000..d423b3ec
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat.ts
@@ -0,0 +1,233 @@
+/* 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({
+ name: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+})
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+}
+
+const penangananDarurat = proxy({
+ findMany: {
+ data: null as
+ | Prisma.PenangananDaruratGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ penangananDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
+ penangananDarurat.findMany.page = page;
+ penangananDarurat.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.penanganandarurat["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ penangananDarurat.findMany.data = res.data.data ?? [];
+ penangananDarurat.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ penangananDarurat.findMany.data = [];
+ penangananDarurat.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita paginated:", err);
+ penangananDarurat.findMany.data = [];
+ penangananDarurat.findMany.totalPages = 1;
+ } finally {
+ penangananDarurat.findMany.loading = false;
+ }
+ },
+ },
+ create:{
+ form: {...defaultForm},
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(penangananDarurat.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ penangananDarurat.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.penanganandarurat[
+ "create"
+ ].post(penangananDarurat.create.form);
+ if (res.status === 200) {
+ penangananDarurat.findMany.load();
+ return toast.success("Penanganan Darurat berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan penanganan darurat");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ penangananDarurat.create.loading = false;
+ }
+ },
+ resetForm() {
+ penangananDarurat.create.form = {...defaultForm};
+ }
+ },
+ findUnique: {
+ data: null as Prisma.PenangananDaruratGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/penanganandarurat/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ penangananDarurat.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ penangananDarurat.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ penangananDarurat.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ penangananDarurat.delete.loading = true;
+ const response = await fetch(`/api/kesehatan/penanganandarurat/del/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Penanganan darurat berhasil dihapus");
+ await penangananDarurat.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus penanganan darurat");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus penanganan darurat");
+ } finally {
+ penangananDarurat.delete.loading = false;
+ }
+ }
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/penanganandarurat/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching penanganan darurat:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal memuat data");
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(penangananDarurat.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ penangananDarurat.edit.loading = true;
+ const response = await fetch(`/api/kesehatan/penanganandarurat/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(result.message || "Penanganan darurat berhasil diupdate");
+ await penangananDarurat.findMany.load();
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update penanganan darurat");
+ }
+ } catch (error) {
+ console.error("Gagal update:", error);
+ toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate penanganan darurat");
+ return false;
+ } finally {
+ penangananDarurat.edit.loading = false;
+ }
+ },
+ reset() {
+ penangananDarurat.edit.id = "";
+ penangananDarurat.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default penangananDarurat
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu.ts b/src/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu.ts
new file mode 100644
index 00000000..38ccec4c
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu.ts
@@ -0,0 +1,246 @@
+/* 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({
+ name: z.string().min(1, { message: "Name is required" }),
+ nomor: z.string().min(1, { message: "Nomor is required" }),
+ deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
+ imageId: z.string().nonempty(),
+ jadwalPelayanan: z.string().min(1, { message: "Jadwal Pelayanan is required" }),
+});
+
+const defaultForm = {
+ name: "",
+ nomor: "",
+ deskripsi: "",
+ imageId: "",
+ jadwalPelayanan: "",
+};
+
+const posyandustate = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(posyandustate.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ posyandustate.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.posyandu["create"].post(posyandustate.create.form);
+ if (res.status === 200) {
+ posyandustate.findMany.load();
+ return toast.success("Posyandu berhasil disimpan!");
+ }
+ return toast.error("Gagal menyimpan posyandu");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ posyandustate.create.loading = false;
+ }
+ },
+ resetForm(){
+ posyandustate.create.form = { ...defaultForm };
+ }
+ },
+ findMany: {
+ data: null as
+ | Prisma.PosyanduGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ posyandustate.findMany.loading = true; // ✅ Akses langsung via nama path
+ posyandustate.findMany.page = page;
+ posyandustate.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ posyandustate.findMany.data = res.data.data ?? [];
+ posyandustate.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ posyandustate.findMany.data = [];
+ posyandustate.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch posyandu paginated:", err);
+ posyandustate.findMany.data = [];
+ posyandustate.findMany.totalPages = 1;
+ } finally {
+ posyandustate.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as
+ | Prisma.PosyanduGetPayload<{
+ include: {
+ image: true;
+ }
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/posyandu/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ posyandustate.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch posyandu:", res.statusText);
+ posyandustate.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching posyandu:", error);
+ posyandustate.findUnique.data = null;
+ }
+ }
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ posyandustate.delete.loading = true;
+ const response = await fetch(`/api/kesehatan/posyandu/del/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Posyandu berhasil dihapus");
+ await posyandustate.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus posyandu");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus posyandu");
+ } finally {
+ posyandustate.delete.loading = false;
+ }
+ }
+ },
+ edit: {
+ id: "",
+ form: {...defaultForm},
+ loading: false,
+
+ async load(id: string) {
+ if(!id){
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/posyandu/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if(!response.ok){
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if(result?.success){
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ nomor: data.nomor,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId || "",
+ jadwalPelayanan: data.jadwalPelayanan || "",
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching posyandu:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal memuat data");
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateForm.safeParse(posyandustate.edit.form);
+ if(!cek.success){
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ posyandustate.edit.loading = true;
+ const response = await fetch(`/api/kesehatan/posyandu/${this.id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ nomor: this.form.nomor,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ jadwalPelayanan: this.form.jadwalPelayanan,
+ }),
+ });
+
+ if(!response.ok){
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if(result.success){
+ toast.success(result.message || "Posyandu berhasil diperbarui");
+ await posyandustate.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching posyandu:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal memuat data");
+ return false;
+ } finally {
+ posyandustate.edit.loading = false;
+ }
+ },
+
+ reset() {
+ posyandustate.edit.id = "";
+ posyandustate.edit.form = {...defaultForm};
+ }
+ }
+})
+
+export default posyandustate;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan.ts
new file mode 100644
index 00000000..911b31b3
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan.ts
@@ -0,0 +1,257 @@
+/* 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({
+ name: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ imageId: z.string().nonempty(),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsiSingkat: "",
+ deskripsi: "",
+ imageId: "",
+};
+
+const programKesehatan = proxy({
+ findMany: {
+ data: null as
+ | Prisma.ProgramKesehatanGetPayload<{
+ include: {
+ image: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ programKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
+ programKesehatan.findMany.page = page;
+ programKesehatan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.programkesehatan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ programKesehatan.findMany.data = res.data.data ?? [];
+ programKesehatan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ programKesehatan.findMany.data = [];
+ programKesehatan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita paginated:", err);
+ programKesehatan.findMany.data = [];
+ programKesehatan.findMany.totalPages = 1;
+ } finally {
+ programKesehatan.findMany.loading = false;
+ }
+ },
+ },
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(programKesehatan.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ programKesehatan.create.loading = true;
+ const res = await ApiFetch.api.kesehatan.programkesehatan[
+ "create"
+ ].post(programKesehatan.create.form);
+ if (res.status === 200) {
+ programKesehatan.findMany.load();
+ return toast.success("Program Kesehatan berhasil disimpan!");
+ }
+
+ return toast.error("Gagal menyimpan program kesehatan");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ programKesehatan.create.loading = false;
+ }
+ },
+ resetForm() {
+ programKesehatan.create.form = { ...defaultForm };
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ProgramKesehatanGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/kesehatan/programkesehatan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ programKesehatan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch program kesehatan:", res.statusText);
+ programKesehatan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching program kesehatan:", error);
+ programKesehatan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ programKesehatan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/kesehatan/programkesehatan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Program kesehatan berhasil dihapus");
+ await programKesehatan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus program kesehatan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus program kesehatan");
+ } finally {
+ programKesehatan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/kesehatan/programkesehatan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsiSingkat: data.deskripsiSingkat,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error fetching program kesehatan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateForm.safeParse(programKesehatan.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ programKesehatan.edit.loading = true;
+ const response = await fetch(
+ `/api/kesehatan/programkesehatan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsiSingkat: this.form.deskripsiSingkat,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success(
+ result.message || "Program kesehatan berhasil diupdate"
+ );
+ await programKesehatan.findMany.load();
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update program kesehatan");
+ }
+ } catch (error) {
+ console.error("Gagal update:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat mengupdate program kesehatan"
+ );
+ return false;
+ } finally {
+ programKesehatan.edit.loading = false;
+ }
+ },
+ reset() {
+ programKesehatan.edit.id = "";
+ programKesehatan.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default programKesehatan;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas.ts b/src/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas.ts
new file mode 100644
index 00000000..dcd19dca
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas.ts
@@ -0,0 +1,329 @@
+/* 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";
+
+// Validasi form
+const templateForm = z.object({
+ name: z.string().min(1),
+ alamat: z.string().min(1),
+ imageId: z.string().min(1),
+ jam: z.object({
+ workDays: z.string().min(1),
+ weekDays: z.string().min(1),
+ holiday: z.string().min(1),
+ }),
+ kontak: z.object({
+ kontakPuskesmas: z.string().min(1),
+ email: z.string().min(1),
+ facebook: z.string().min(1),
+ kontakUGD: z.string().min(1),
+ }),
+});
+
+// Default form
+const defaultForm = {
+ name: "",
+ alamat: "",
+ imageId: "",
+ jam: {
+ workDays: "",
+ weekDays: "",
+ holiday: "",
+ },
+ kontak: {
+ kontakPuskesmas: "",
+ email: "",
+ facebook: "",
+ kontakUGD: "",
+ },
+ image: undefined,
+};
+
+const puskesmasState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async submit() {
+ const cek = templateForm.safeParse(puskesmasState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues.map((v) => v.path.join(".")).join(", ")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ puskesmasState.create.loading = true;
+
+ console.log('Form data:', puskesmasState.create.form);
+ interface ErrorResponse {
+ message?: string;
+ error?: string;
+ errors?: Record;
+ }
+
+ const payload = {
+ name: puskesmasState.create.form.name,
+ alamat: puskesmasState.create.form.alamat,
+ imageId: puskesmasState.create.form.imageId,
+ jam: {
+ workDays: puskesmasState.create.form.jam.workDays,
+ weekDays: puskesmasState.create.form.jam.weekDays,
+ holiday: puskesmasState.create.form.jam.holiday,
+ },
+ kontak: {
+ kontakPuskesmas: puskesmasState.create.form.kontak.kontakPuskesmas,
+ email: puskesmasState.create.form.kontak.email,
+ facebook: puskesmasState.create.form.kontak.facebook,
+ kontakUGD: puskesmasState.create.form.kontak.kontakUGD,
+ },
+ };
+
+
+
+
+ console.log('Sending payload:', JSON.stringify(payload, null, 2));
+
+ try {
+ const res = await ApiFetch.api.kesehatan.puskesmas.create.post(payload);
+ console.log('API Response:', res);
+
+ if (res.status === 200) {
+ await puskesmasState.findMany.load();
+ toast.success("Berhasil menambahkan puskesmas");
+ return res;
+ } else {
+ console.error('API Error Response:', {
+ status: res.status,
+ data: res.data
+ });
+
+ const errorData = res.data as ErrorResponse;
+ let errorMessage = 'Gagal menambahkan puskesmas';
+
+ if (errorData?.message) {
+ errorMessage = errorData.message;
+ } else if (errorData?.error) {
+ errorMessage = errorData.error;
+ } else if (errorData?.errors) {
+ errorMessage = Object.entries(errorData.errors)
+ .map(([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`)
+ .join('; ');
+ }
+
+ console.error('Extracted error message:', errorMessage);
+ toast.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+ } catch (error) {
+ console.error('Error in API call:', {
+ error,
+ errorString: String(error),
+ jsonError: error instanceof Error ? {
+ name: error.name,
+ message: error.message,
+ stack: error.stack
+ } : 'Not an Error instance'
+ });
+ throw error;
+ }
+ } catch (error) {
+ console.error("Error in puskesmas submit:", {
+ error,
+ errorString: String(error),
+ errorType: typeof error,
+ isErrorInstance: error instanceof Error,
+ errorDetails: error instanceof Error ? {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ cause: error.cause
+ } : null
+ });
+
+ let errorMessage = "Terjadi kesalahan saat menambahkan puskesmas";
+ if (error instanceof Error) {
+ errorMessage = error.message || errorMessage;
+ } else if (error && typeof error === 'object' && 'message' in error) {
+ errorMessage = String((error as { message: unknown }).message);
+ } else if (typeof error === 'string') {
+ errorMessage = error;
+ }
+
+ console.error('Displaying error to user:', errorMessage);
+ toast.error(errorMessage);
+ throw error;
+ } finally {
+ puskesmasState.create.loading = false;
+ }
+ },
+ resetForm() {
+ puskesmasState.create.form = { ...defaultForm };
+ }
+ },
+
+ findMany: {
+ data: null as
+ | Prisma.PuskesmasGetPayload<{
+ include: {
+ image: true;
+ jam: true;
+ kontak: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ puskesmasState.findMany.loading = true; // ✅ Akses langsung via nama path
+ puskesmasState.findMany.page = page;
+ puskesmasState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ puskesmasState.findMany.data = res.data.data ?? [];
+ puskesmasState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ puskesmasState.findMany.data = [];
+ puskesmasState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch berita paginated:", err);
+ puskesmasState.findMany.data = [];
+ puskesmasState.findMany.totalPages = 1;
+ } finally {
+ puskesmasState.findMany.loading = false;
+ }
+ },
+ },
+
+ findUnique: {
+ data: null as Prisma.PuskesmasGetPayload<{
+ include: { image: true; jam: true; kontak: true };
+ }> | null,
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/puskesmas/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ puskesmasState.findUnique.data = data.data ?? null;
+ } else {
+ toast.error("Gagal load data puskesmas");
+ }
+ },
+ },
+
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ const res = await fetch(`/api/kesehatan/puskesmas/${id}`);
+ if (!res.ok) {
+ toast.error("Gagal memuat data");
+ return;
+ }
+
+ const result = await res.json();
+ const data = result.data;
+
+ puskesmasState.edit.id = data.id;
+ puskesmasState.edit.form = {
+ name: data.name,
+ alamat: data.alamat,
+ imageId: data.imageId,
+ jam: {
+ workDays: data.jam.workDays,
+ weekDays: data.jam.weekDays,
+ holiday: data.jam.holiday,
+ },
+ kontak: {
+ kontakPuskesmas: data.kontak.kontakPuskesmas,
+ email: data.kontak.email,
+ facebook: data.kontak.facebook,
+ kontakUGD: data.kontak.kontakUGD,
+ },
+ image: data.image,
+ };
+ },
+
+ async submit() {
+ const cek = templateForm.safeParse(puskesmasState.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues.map((v) => v.path.join(".")).join(", ")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ puskesmasState.edit.loading = true;
+ const payload = {
+ name: puskesmasState.edit.form.name,
+ alamat: puskesmasState.edit.form.alamat,
+ imageId: puskesmasState.edit.form.imageId,
+ jam: { ...puskesmasState.edit.form.jam },
+ kontak: { ...puskesmasState.edit.form.kontak }
+ };
+
+ const res = await fetch(`/api/kesehatan/puskesmas/${puskesmasState.edit.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+
+ if (!res.ok) {
+ const error = await res.json();
+ throw new Error(error.message || "Update gagal");
+ }
+
+ toast.success("Berhasil update puskesmas");
+ await puskesmasState.findMany.load();
+ return true;
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : "Terjadi kesalahan saat update");
+ return false;
+ } finally {
+ puskesmasState.edit.loading = false;
+ }
+ },
+
+ reset() {
+ puskesmasState.edit.id = "";
+ puskesmasState.edit.form = { ...defaultForm };
+ }
+ },
+
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ try {
+ puskesmasState.delete.loading = true;
+ const res = await fetch(`/api/kesehatan/puskesmas/del/${id}`, {
+ method: "DELETE",
+ });
+
+ const result = await res.json();
+ if (res.ok && result.success) {
+ toast.success("Puskesmas berhasil dihapus");
+ await puskesmasState.findMany.load();
+ } else {
+ toast.error(result.message || "Gagal menghapus");
+ }
+ } catch {
+ toast.error("Terjadi kesalahan saat menghapus");
+ } finally {
+ puskesmasState.delete.loading = false;
+ }
+ }
+ }
+});
+
+export default puskesmasState;
diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
new file mode 100644
index 00000000..c780b1a9
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
@@ -0,0 +1,257 @@
+/* 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 templateapbDesaForm = z.object({
+ name: z.string().min(1, "Judul minimal 1 karakter"),
+ jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ imageId: z.string().min(1, "File minimal 1"),
+ fileId: z.string().min(1, "File minimal 1"),
+});
+
+const defaultapbdesForm = {
+ name: "",
+ jumlah: "",
+ imageId: "",
+ fileId: "",
+};
+
+const apbdes = proxy({
+ create: {
+ form: { ...defaultapbdesForm },
+ loading: false,
+ async create() {
+ const cek = templateapbDesaForm.safeParse(apbdes.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ apbdes.create.loading = true;
+ const res = await ApiFetch.api.landingpage.apbdes["create"].post({
+ ...apbdes.create.form,
+ });
+
+ if (res.status === 200) {
+ apbdes.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ apbdes.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.APBDesGetPayload<{
+ include: {
+ image: true;
+ file: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
+ apbdes.findMany.loading = true; // Use the full path to access the property
+ apbdes.findMany.page = page;
+ apbdes.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.apbdes[
+ "findMany"
+ ].get({
+ query
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ apbdes.findMany.data = res.data.data || [];
+ apbdes.findMany.total = res.data.total || 0;
+ apbdes.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ apbdes.findMany.data = [];
+ apbdes.findMany.total = 0;
+ apbdes.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
+ apbdes.findMany.data = [];
+ apbdes.findMany.total = 0;
+ apbdes.findMany.totalPages = 1;
+ } finally {
+ apbdes.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.APBDesGetPayload<{
+ include: {
+ image: true;
+ file: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/apbdes/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ apbdes.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ apbdes.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ apbdes.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ apbdes.delete.loading = true;
+
+ const response = await fetch(`/api/landingpage/apbdes/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "apbdes berhasil dihapus");
+ await apbdes.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus apbdes");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus apbdes");
+ } finally {
+ apbdes.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultapbdesForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ apbdes.edit.loading = true;
+
+ const response = await fetch(`/api/landingpage/apbdes/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ jumlah: data.jumlah,
+ imageId: data.imageId,
+ fileId: data.fileId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading apbdes:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ } finally {
+ apbdes.edit.loading = false;
+ }
+ },
+
+ async update() {
+ const cek = templateapbDesaForm.safeParse(apbdes.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ apbdes.edit.loading = true;
+ const response = await fetch(`/api/landingpage/apbdes/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ jumlah: this.form.jumlah,
+ imageId: this.form.imageId,
+ fileId: this.form.fileId,
+ }),
+ });
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update apbdes");
+ await apbdes.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate apbdes");
+ }
+ } catch (error) {
+ console.error("Error updating apbdes:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate apbdes"
+ );
+ return false;
+ } finally {
+ apbdes.edit.loading = false;
+ }
+ },
+ reset() {
+ apbdes.edit.id = "";
+ apbdes.edit.form = { ...defaultapbdesForm };
+ },
+ },
+});
+
+export default apbdes;
diff --git a/src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts b/src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts
new file mode 100644
index 00000000..c5fe9818
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts
@@ -0,0 +1,542 @@
+/* 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 templateDesaAntiKorupsiForm = z.object({
+ name: z.string().min(1, "Judul minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ kategoriId: z.string().min(1, "Kategori minimal 1"),
+ fileId: z.string().min(1, "File minimal 1"),
+});
+
+const defaultDesaAntiKorupsiForm = {
+ name: "",
+ deskripsi: "",
+ kategoriId: "",
+ fileId: "",
+};
+
+const desaAntikorupsi = proxy({
+ create: {
+ form: { ...defaultDesaAntiKorupsiForm },
+ loading: false,
+ async create() {
+ const cek = templateDesaAntiKorupsiForm.safeParse(
+ desaAntikorupsi.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ desaAntikorupsi.create.loading = true;
+ const res = await ApiFetch.api.landingpage.desaantikorupsi[
+ "create"
+ ].post({
+ ...desaAntikorupsi.create.form,
+ });
+
+ if (res.status === 200) {
+ desaAntikorupsi.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ desaAntikorupsi.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
+ desaAntikorupsi.findMany.page = page;
+ desaAntikorupsi.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.desaantikorupsi[
+ "findMany"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ desaAntikorupsi.findMany.data = res.data.data || [];
+ desaAntikorupsi.findMany.total = res.data.total || 0;
+ desaAntikorupsi.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load media sosial:", res.data?.message);
+ desaAntikorupsi.findMany.data = [];
+ desaAntikorupsi.findMany.total = 0;
+ desaAntikorupsi.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading media sosial:", error);
+ desaAntikorupsi.findMany.data = [];
+ desaAntikorupsi.findMany.total = 0;
+ desaAntikorupsi.findMany.totalPages = 1;
+ } finally {
+ desaAntikorupsi.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DesaAntiKorupsiGetPayload<{
+ include: {
+ file: true;
+ kategori: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ desaAntikorupsi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ desaAntikorupsi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ desaAntikorupsi.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ desaAntikorupsi.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/desaantikorupsi/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "desa anti korupsi berhasil dihapus");
+ await desaAntikorupsi.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus desa anti korupsi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus desa anti korupsi");
+ } finally {
+ desaAntikorupsi.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultDesaAntiKorupsiForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ desaAntikorupsi.edit.loading = true;
+
+ const response = await fetch(`/api/landingpage/desaantikorupsi/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ kategoriId: data.kategoriId,
+ fileId: data.fileId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading desa anti korupsi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ } finally {
+ desaAntikorupsi.edit.loading = false;
+ }
+ },
+
+ async update() {
+ const cek = templateDesaAntiKorupsiForm.safeParse(
+ desaAntikorupsi.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ desaAntikorupsi.edit.loading = true;
+ const response = await fetch(
+ `/api/landingpage/desaantikorupsi/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ kategoriId: this.form.kategoriId,
+ fileId: this.form.fileId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update desa anti korupsi");
+ await desaAntikorupsi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate desa anti korupsi"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating desa anti korupsi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate desa anti korupsi"
+ );
+ return false;
+ } finally {
+ desaAntikorupsi.edit.loading = false;
+ }
+ },
+ reset() {
+ desaAntikorupsi.edit.id = "";
+ desaAntikorupsi.edit.form = { ...defaultDesaAntiKorupsiForm };
+ },
+ },
+});
+
+// ========================================= KATEGORI desa anti korupsi ========================================= //
+const kategoriDesaAntiKorupsiForm = z.object({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+});
+
+const kategoriDesaAntiKorupsiDefaultForm = {
+ name: "",
+};
+
+const kategoriDesaAntiKorupsi = proxy({
+ create: {
+ form: { ...kategoriDesaAntiKorupsiDefaultForm },
+ loading: false,
+ async create() {
+ const cek = kategoriDesaAntiKorupsiForm.safeParse(
+ kategoriDesaAntiKorupsi.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kategoriDesaAntiKorupsi.create.loading = true;
+ const res = await ApiFetch.api.landingpage.kategoridak["create"].post(
+ kategoriDesaAntiKorupsi.create.form
+ );
+ if (res.status === 200) {
+ kategoriDesaAntiKorupsi.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ kategoriDesaAntiKorupsi.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
+ kategoriDesaAntiKorupsi.findMany.page = page;
+ kategoriDesaAntiKorupsi.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.kategoridak["findMany"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriDesaAntiKorupsi.findMany.data = res.data.data || [];
+ kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0;
+ kategoriDesaAntiKorupsi.findMany.totalPages =
+ res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load media sosial:", res.data?.message);
+ kategoriDesaAntiKorupsi.findMany.data = [];
+ kategoriDesaAntiKorupsi.findMany.total = 0;
+ kategoriDesaAntiKorupsi.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading media sosial:", error);
+ kategoriDesaAntiKorupsi.findMany.data = [];
+ kategoriDesaAntiKorupsi.findMany.total = 0;
+ kategoriDesaAntiKorupsi.findMany.totalPages = 1;
+ } finally {
+ kategoriDesaAntiKorupsi.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/kategoridak/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kategoriDesaAntiKorupsi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriDesaAntiKorupsi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriDesaAntiKorupsi.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriDesaAntiKorupsi.delete.loading = true;
+
+ const response = await fetch(`/api/landingpage/kategoridak/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Kategori desa anti korupsi berhasil dihapus"
+ );
+ await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus kategori desa anti korupsi"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error(
+ "Terjadi kesalahan saat menghapus kategori desa anti korupsi"
+ );
+ } finally {
+ kategoriDesaAntiKorupsi.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...kategoriDesaAntiKorupsiDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/landingpage/kategoridak/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori desa anti korupsi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = kategoriDesaAntiKorupsiForm.safeParse(
+ kategoriDesaAntiKorupsi.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriDesaAntiKorupsi.edit.loading = true;
+ const response = await fetch(
+ `/api/landingpage/kategoridak/${kategoriDesaAntiKorupsi.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: kategoriDesaAntiKorupsi.edit.form.name,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate kategori desa anti korupsi (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message ||
+ "Berhasil memperbarui kategori desa anti korupsi"
+ );
+ await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate kategori desa anti korupsi"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating kategori desa anti korupsi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kategori desa anti korupsi"
+ );
+ return false;
+ } finally {
+ kategoriDesaAntiKorupsi.edit.loading = false;
+ }
+ },
+ reset() {
+ kategoriDesaAntiKorupsi.edit.id = "";
+ kategoriDesaAntiKorupsi.edit.form = {
+ ...kategoriDesaAntiKorupsiDefaultForm,
+ };
+ },
+ },
+});
+
+const korupsiState = proxy({
+ desaAntikorupsi,
+ kategoriDesaAntiKorupsi,
+});
+export default korupsiState;
diff --git a/src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts b/src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts
new file mode 100644
index 00000000..48d707b7
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts
@@ -0,0 +1,834 @@
+/* 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";
+
+// Template form responden
+
+const templateResponden = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+ tanggal: z.string().min(1, "Tanggal harus diisi"),
+ jenisKelaminId: z.string().min(1, "Jenis kelamin harus diisi"),
+ ratingId: z.string().min(1, "Rating harus diisi"),
+ kelompokUmurId: z.string().min(1, "Kelompok umur harus diisi"),
+});
+
+const defaultFormResponden = {
+ name: "",
+ tanggal: "",
+ jenisKelaminId: "",
+ ratingId: "",
+ kelompokUmurId: "",
+};
+
+const responden = proxy({
+ create: {
+ form: { ...defaultFormResponden },
+ loading: false,
+ async create() {
+ const cek = templateResponden.safeParse(responden.create.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+
+ try {
+ responden.create.loading = true;
+ const res = await ApiFetch.api.landingpage.responden["create"].post(
+ responden.create.form
+ );
+ if (res.status === 200) {
+ toast.success("Responden berhasil ditambahkan");
+ await responden.findMany.load();
+ } else {
+ toast.error(res.data?.message ?? "Gagal tambah responden");
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error("Terjadi kesalahan saat menambahkan responden");
+ } finally {
+ responden.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => {
+ // Change to arrow function
+ responden.findMany.loading = true; // Use the full path to access the property
+ responden.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.landingpage.responden["findMany"].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ responden.findMany.data = res.data.data || [];
+ responden.findMany.total = res.data.total || 0;
+ responden.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load responden:", res.data?.message);
+ responden.findMany.data = [];
+ responden.findMany.total = 0;
+ responden.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading responden:", error);
+ responden.findMany.data = [];
+ responden.findMany.total = 0;
+ responden.findMany.totalPages = 1;
+ } finally {
+ responden.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.RespondenGetPayload<{
+ include: {
+ jenisKelamin: true;
+ rating: true;
+ kelompokUmur: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/responden/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ responden.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ responden.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading responden:", error);
+ responden.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormResponden },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ responden.update.loading = true;
+
+ const response = await fetch(`/api/landingpage/responden/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ tanggal: data.tanggal,
+ jenisKelaminId: data.jenisKelaminId,
+ ratingId: data.ratingId,
+ kelompokUmurId: data.kelompokUmurId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading responden:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ } finally {
+ responden.update.loading = false;
+ }
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateResponden.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/landingpage/responden/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ tanggal: this.form.tanggal,
+ jenisKelaminId: this.form.jenisKelaminId,
+ ratingId: this.form.ratingId,
+ kelompokUmurId: this.form.kelompokUmurId,
+ }),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await responden.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data responden");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ responden.delete.loading = true;
+
+ const response = await fetch(`/api/landingpage/responden/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "responden berhasil dihapus");
+ await responden.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus responden");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus responden");
+ } finally {
+ responden.delete.loading = false;
+ }
+ },
+ },
+});
+
+// Template form jenis kelamin responden
+const templateJenisKelaminResponden = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultFormJenisKelaminResponden = {
+ name: "",
+};
+
+const jenisKelaminResponden = proxy({
+ create: {
+ form: { ...defaultFormJenisKelaminResponden },
+ loading: false,
+ async create() {
+ const cek = templateJenisKelaminResponden.safeParse(
+ jenisKelaminResponden.create.form
+ );
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+ jenisKelaminResponden.create.loading = true;
+ try {
+ jenisKelaminResponden.create.loading = true;
+ const res = await ApiFetch.api.landingpage.jeniskelaminresponden[
+ "create"
+ ].post(jenisKelaminResponden.create.form);
+ if (res.status === 200) {
+ toast.success("Jenis kelamin responden berhasil ditambahkan");
+ await jenisKelaminResponden.findMany.load();
+ } else {
+ toast.error(
+ res.data?.message ?? "Gagal tambah jenis kelamin responden"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error(
+ "Terjadi kesalahan saat menambahkan jenis kelamin responden"
+ );
+ } finally {
+ jenisKelaminResponden.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => {
+ // Change to arrow function
+ jenisKelaminResponden.findMany.loading = true; // Use the full path to access the property
+ jenisKelaminResponden.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.landingpage.jeniskelaminresponden[
+ "findMany"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ jenisKelaminResponden.findMany.data = res.data.data || [];
+ jenisKelaminResponden.findMany.total = res.data.total || 0;
+ jenisKelaminResponden.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load jenis kelamin responden:",
+ res.data?.message
+ );
+ jenisKelaminResponden.findMany.data = [];
+ jenisKelaminResponden.findMany.total = 0;
+ jenisKelaminResponden.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading jenis kelamin responden:", error);
+ jenisKelaminResponden.findMany.data = [];
+ jenisKelaminResponden.findMany.total = 0;
+ jenisKelaminResponden.findMany.totalPages = 1;
+ } finally {
+ jenisKelaminResponden.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.JenisKelaminRespondenGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/jeniskelaminresponden/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ jenisKelaminResponden.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ jenisKelaminResponden.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading jenis kelamin responden:", error);
+ jenisKelaminResponden.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormJenisKelaminResponden },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateJenisKelaminResponden.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/landingpage/jeniskelaminresponden/${id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ }
+ );
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await jenisKelaminResponden.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data jenis kelamin responden");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ jenisKelaminResponden.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/jeniskelaminresponden/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "jenis kelamin responden berhasil dihapus"
+ );
+ await jenisKelaminResponden.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus jenis kelamin responden"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus jenis kelamin responden");
+ } finally {
+ jenisKelaminResponden.delete.loading = false;
+ }
+ },
+ },
+});
+
+// Template form pilihan rating responden
+
+const templatePilihanRatingResponden = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultFormPilihanRatingResponden = {
+ name: "",
+};
+
+const pilihanRatingResponden = proxy({
+ create: {
+ form: { ...defaultFormPilihanRatingResponden },
+ loading: false,
+ async create() {
+ const cek = templatePilihanRatingResponden.safeParse(
+ pilihanRatingResponden.create.form
+ );
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+ pilihanRatingResponden.create.loading = true;
+ try {
+ pilihanRatingResponden.create.loading = true;
+ const res = await ApiFetch.api.landingpage.pilihanratingresponden[
+ "create"
+ ].post(pilihanRatingResponden.create.form);
+ if (res.status === 200) {
+ toast.success("Jenis kelamin responden berhasil ditambahkan");
+ await pilihanRatingResponden.findMany.load();
+ } else {
+ toast.error(
+ res.data?.message ?? "Gagal tambah jenis kelamin responden"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error(
+ "Terjadi kesalahan saat menambahkan jenis kelamin responden"
+ );
+ } finally {
+ pilihanRatingResponden.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => {
+ // Change to arrow function
+ pilihanRatingResponden.findMany.loading = true; // Use the full path to access the property
+ pilihanRatingResponden.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.landingpage.pilihanratingresponden[
+ "findMany"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pilihanRatingResponden.findMany.data = res.data.data || [];
+ pilihanRatingResponden.findMany.total = res.data.total || 0;
+ pilihanRatingResponden.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load pilihan rating responden:",
+ res.data?.message
+ );
+ pilihanRatingResponden.findMany.data = [];
+ pilihanRatingResponden.findMany.total = 0;
+ pilihanRatingResponden.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pilihan rating responden:", error);
+ pilihanRatingResponden.findMany.data = [];
+ pilihanRatingResponden.findMany.total = 0;
+ pilihanRatingResponden.findMany.totalPages = 1;
+ } finally {
+ pilihanRatingResponden.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PilihanRatingRespondenGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/pilihanratingresponden/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ pilihanRatingResponden.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ pilihanRatingResponden.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading pilihan rating responden:", error);
+ pilihanRatingResponden.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormPilihanRatingResponden },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templatePilihanRatingResponden.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/landingpage/pilihanratingresponden/${id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ }
+ );
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await pilihanRatingResponden.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data pilihan rating responden");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pilihanRatingResponden.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/pilihanratingresponden/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "pilihan rating responden berhasil dihapus"
+ );
+ await pilihanRatingResponden.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus pilihan rating responden"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pilihan rating responden");
+ } finally {
+ pilihanRatingResponden.delete.loading = false;
+ }
+ },
+ },
+});
+
+// Template form kelompok umur responden
+
+const templateKelompokUmurResponden = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultFormKelompokUmurResponden = {
+ name: "",
+};
+
+const kelompokUmurResponden = proxy({
+ create: {
+ form: { ...defaultFormKelompokUmurResponden },
+ loading: false,
+ async create() {
+ const cek = templateKelompokUmurResponden.safeParse(
+ kelompokUmurResponden.create.form
+ );
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+ kelompokUmurResponden.create.loading = true;
+ try {
+ kelompokUmurResponden.create.loading = true;
+ const res = await ApiFetch.api.landingpage.umurresponden["create"].post(
+ kelompokUmurResponden.create.form
+ );
+ if (res.status === 200) {
+ toast.success("Kelompok umur responden berhasil ditambahkan");
+ await kelompokUmurResponden.findMany.load();
+ } else {
+ toast.error(
+ res.data?.message ?? "Gagal tambah kelompok umur responden"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error(
+ "Terjadi kesalahan saat menambahkan kelompok umur responden"
+ );
+ } finally {
+ kelompokUmurResponden.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => {
+ // Change to arrow function
+ kelompokUmurResponden.findMany.loading = true; // Use the full path to access the property
+ kelompokUmurResponden.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.landingpage.umurresponden[
+ "findMany"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kelompokUmurResponden.findMany.data = res.data.data || [];
+ kelompokUmurResponden.findMany.total = res.data.total || 0;
+ kelompokUmurResponden.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load kelompok umur responden:",
+ res.data?.message
+ );
+ kelompokUmurResponden.findMany.data = [];
+ kelompokUmurResponden.findMany.total = 0;
+ kelompokUmurResponden.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading kelompok umur responden:", error);
+ kelompokUmurResponden.findMany.data = [];
+ kelompokUmurResponden.findMany.total = 0;
+ kelompokUmurResponden.findMany.totalPages = 1;
+ } finally {
+ kelompokUmurResponden.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.UmurRespondenGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/umurresponden/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kelompokUmurResponden.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kelompokUmurResponden.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading kelompok umur responden:", error);
+ kelompokUmurResponden.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultFormKelompokUmurResponden },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateKelompokUmurResponden.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/landingpage/umurresponden/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await kelompokUmurResponden.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data kelompok umur responden");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kelompokUmurResponden.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/umurresponden/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "kelompok umur responden berhasil dihapus"
+ );
+ await kelompokUmurResponden.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus kelompok umur responden"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kelompok umur responden");
+ } finally {
+ kelompokUmurResponden.delete.loading = false;
+ }
+ },
+ },
+});
+
+const indeksKepuasanState = proxy({
+ responden,
+ kelompokUmurResponden,
+ jenisKelaminResponden,
+ pilihanRatingResponden
+})
+
+export default indeksKepuasanState
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts b/src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts
new file mode 100644
index 00000000..6fb15f98
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts
@@ -0,0 +1,536 @@
+/* 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 templateprestasiDesaForm = z.object({
+ name: z.string().min(1, "Judul minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ imageId: z.string().min(1, "File minimal 1"),
+ kategoriId: z.string().min(1, "Kategori minimal 1 karakter"),
+});
+
+const defaultprestasiDesaForm = {
+ name: "",
+ deskripsi: "",
+ imageId: "",
+ kategoriId: "",
+};
+
+const prestasiDesa = proxy({
+ create: {
+ form: { ...defaultprestasiDesaForm },
+ loading: false,
+ async create() {
+ const cek = templateprestasiDesaForm.safeParse(
+ prestasiDesa.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ prestasiDesa.create.loading = true;
+ const res = await ApiFetch.api.landingpage.prestasidesa[
+ "create"
+ ].post({
+ ...prestasiDesa.create.form,
+ });
+
+ if (res.status === 200) {
+ prestasiDesa.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ prestasiDesa.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.PrestasiDesaGetPayload<{
+ include: {
+ image: true;
+ kategori: {
+ select: {
+ id: true;
+ name: true;
+ };
+ };
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ prestasiDesa.findMany.loading = true; // ✅ Akses langsung via nama path
+ prestasiDesa.findMany.page = page;
+ prestasiDesa.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ prestasiDesa.findMany.data = res.data.data ?? [];
+ prestasiDesa.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ prestasiDesa.findMany.data = [];
+ prestasiDesa.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch prestasi desa paginated:", err);
+ prestasiDesa.findMany.data = [];
+ prestasiDesa.findMany.totalPages = 1;
+ } finally {
+ prestasiDesa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PrestasiDesaGetPayload<{
+ include: {
+ image: true;
+ kategori: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ prestasiDesa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ prestasiDesa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ prestasiDesa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ prestasiDesa.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/prestasidesa/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "prestasi desa berhasil dihapus");
+ await prestasiDesa.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus prestasi desa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus prestasi desa");
+ } finally {
+ prestasiDesa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultprestasiDesaForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ prestasiDesa.edit.loading = true;
+
+ const response = await fetch(`/api/landingpage/prestasidesa/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ kategoriId: data.kategoriId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading prestasi desa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ } finally {
+ prestasiDesa.edit.loading = false;
+ }
+ },
+
+ async update() {
+ const cek = templateprestasiDesaForm.safeParse(
+ prestasiDesa.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ prestasiDesa.edit.loading = true;
+ const response = await fetch(
+ `/api/landingpage/prestasidesa/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ kategoriId: this.form.kategoriId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update prestasi desa");
+ await prestasiDesa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate prestasi desa"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating prestasi desa:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate prestasi desa"
+ );
+ return false;
+ } finally {
+ prestasiDesa.edit.loading = false;
+ }
+ },
+ reset() {
+ prestasiDesa.edit.id = "";
+ prestasiDesa.edit.form = { ...defaultprestasiDesaForm };
+ },
+ },
+});
+
+// ========================================= KATEGORI kegiatan ========================================= //
+const kategoriPrestasiForm = z.object({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+});
+
+const kategoriPrestasiDefaultForm = {
+ name: "",
+};
+
+const kategoriPrestasi = proxy({
+ create: {
+ form: { ...kategoriPrestasiDefaultForm },
+ loading: false,
+ async create() {
+ const cek = kategoriPrestasiForm.safeParse(kategoriPrestasi.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kategoriPrestasi.create.loading = true;
+ const res = await ApiFetch.api.landingpage.kategoriprestasi[
+ "create"
+ ].post(kategoriPrestasi.create.form);
+ if (res.status === 200) {
+ kategoriPrestasi.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ kategoriPrestasi.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<{
+ id: string;
+ name: string;
+ }> | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kategoriPrestasi.findMany.loading = true; // ✅ Akses langsung via nama path
+ kategoriPrestasi.findMany.page = page;
+ kategoriPrestasi.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.kategoriprestasi["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriPrestasi.findMany.data = res.data.data ?? [];
+ kategoriPrestasi.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kategoriPrestasi.findMany.data = [];
+ kategoriPrestasi.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori prestasi paginated:", err);
+ kategoriPrestasi.findMany.data = [];
+ kategoriPrestasi.findMany.totalPages = 1;
+ } finally {
+ kategoriPrestasi.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriPrestasiDesaGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/landingpage/kategoriprestasi/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ kategoriPrestasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriPrestasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriPrestasi.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriPrestasi.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/kategoriprestasi/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kategori prestasi berhasil dihapus");
+ await kategoriPrestasi.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kategori prestasi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kategori prestasi");
+ } finally {
+ kategoriPrestasi.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...kategoriPrestasiDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/landingpage/kategoriprestasi/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori prestasi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = kategoriPrestasiForm.safeParse(kategoriPrestasi.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriPrestasi.edit.loading = true;
+ const response = await fetch(
+ `/api/landingpage/kategoriprestasi/${kategoriPrestasi.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: kategoriPrestasi.edit.form.name,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate kategori prestasi (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui kategori prestasi"
+ );
+ await kategoriPrestasi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate kategori prestasi"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating kategori prestasi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kategori prestasi"
+ );
+ return false;
+ } finally {
+ kategoriPrestasi.edit.loading = false;
+ }
+ },
+ reset() {
+ kategoriPrestasi.edit.id = "";
+ kategoriPrestasi.edit.form = { ...kategoriPrestasiDefaultForm };
+ },
+ },
+});
+
+const prestasiState = proxy({
+ prestasiDesa,
+ kategoriPrestasi,
+});
+
+export default prestasiState;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts
new file mode 100644
index 00000000..be6b2e7f
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts
@@ -0,0 +1,697 @@
+/* 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 templateProgramInovasi = z.object({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ description: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+ link: z.string().min(1, "Link minimal 1 karakter"),
+});
+
+type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
+ select: {
+ name: true;
+ description: true;
+ imageId: true;
+ link: true;
+ };
+}>;
+
+const programInovasi = proxy({
+ create: {
+ form: {
+ name: "",
+ description: "",
+ imageId: "",
+ link: ""
+ } as ProgramInovasiForm,
+ loading: false,
+ async create() {
+ // Ensure all required fields are non-null
+ const formData = {
+ name: programInovasi.create.form.name || "",
+ description: programInovasi.create.form.description || "",
+ imageId: programInovasi.create.form.imageId || "",
+ link: programInovasi.create.form.link || "",
+ };
+
+ const cek = templateProgramInovasi.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ programInovasi.create.loading = true;
+ const res = await ApiFetch.api.landingpage.programinovasi[
+ "create"
+ ].post(formData);
+ if (res.status === 200) {
+ programInovasi.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ programInovasi.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
+ programInovasi.findMany.loading = true; // Use the full path to access the property
+ programInovasi.findMany.page = page;
+ programInovasi.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.programinovasi[
+ "findMany"
+ ].get({
+ query
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ programInovasi.findMany.data = res.data.data || [];
+ programInovasi.findMany.total = res.data.total || 0;
+ programInovasi.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ programInovasi.findMany.data = [];
+ programInovasi.findMany.total = 0;
+ programInovasi.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
+ programInovasi.findMany.data = [];
+ programInovasi.findMany.total = 0;
+ programInovasi.findMany.totalPages = 1;
+ } finally {
+ programInovasi.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ProgramInovasiGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/programinovasi/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ programInovasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch program inovasi:", res.statusText);
+ programInovasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching program inovasi:", error);
+ programInovasi.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ programInovasi.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/programinovasi/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Program inovasi berhasil dihapus");
+ await programInovasi.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus program inovasi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus program inovasi");
+ } finally {
+ programInovasi.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: {} as ProgramInovasiForm,
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/landingpage/programinovasi/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ description: data.description,
+ imageId: data.imageId,
+ link: data.link,
+ };
+ return data;
+ } else {
+ throw new Error(
+ result?.message || "Gagal mengambil data program inovasi"
+ );
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data program inovasi");
+ } finally {
+ programInovasi.update.loading = false;
+ }
+ },
+
+ async update() {
+ const cek = templateProgramInovasi.safeParse(programInovasi.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ programInovasi.update.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/programinovasi/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ description: this.form.description,
+ imageId: this.form.imageId,
+ link: this.form.link,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update program inovasi");
+ await programInovasi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update program inovasi");
+ }
+ } catch (error) {
+ console.error("Error updating program inovasi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update program inovasi"
+ );
+ return false;
+ } finally {
+ programInovasi.update.loading = false;
+ }
+ },
+ },
+});
+
+const templatePejabatDesa = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ position: z.string().min(3, "Posisi minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+});
+
+const defaultFormPejabatDesa = {
+ name: "",
+ position: "",
+ imageId: "",
+};
+
+type PejabatDesaForm = {
+ id: string;
+ name: string;
+ position: string;
+ imageId: string | null;
+ image?: {
+ id: string;
+ name: string;
+ link: string;
+ path: string;
+ mimeType: string;
+ realName: string;
+ isActive: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date | null;
+ } | null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: Date | null;
+ isActive: boolean;
+};
+
+const pejabatDesa = proxy({
+ findUnique: {
+ data: null as PejabatDesaForm | null,
+ loading: false,
+ error: null as string | null,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/landingpage/pejabatdesa/${id}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengambil data pejabat desa"
+ );
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Load pejabat desa error:", errorMessage);
+ toast.error("Terjadi kesalahan saat mengambil data pejabat desa");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultFormPejabatDesa },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(profileData: PejabatDesaForm) {
+ this.id = profileData.id;
+ this.isReadOnly = false; // Semua data bisa diedit
+ this.form = {
+ name: profileData.name || "",
+ position: profileData.position || "",
+ imageId: profileData.imageId || "",
+ };
+ },
+
+ // Update form field
+ updateField(field: keyof typeof defaultFormPejabatDesa, value: string) {
+ this.form[field] = value;
+ },
+
+ // Submit form
+ async submit() {
+ // Validate form
+ const validation = templatePejabatDesa.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ // Ensure ID is properly encoded in the URL
+ const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
+ const response = await fetch(url.toString(), {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update profile");
+ // Refresh profile data
+ await pejabatDesa.findUnique.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update profile");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update profile error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update profile");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.id = "";
+ this.form = { ...defaultFormPejabatDesa };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+});
+
+const templateMediaSosial = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+ iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
+});
+
+type MediaSosialForm = {
+ name: string;
+ imageId: string;
+ iconUrl: string;
+};
+
+const mediaSosial = proxy({
+ create: {
+ form: {} as MediaSosialForm,
+ loading: false,
+ async create() {
+ // Ensure all required fields are non-null
+ const formData = {
+ name: mediaSosial.create.form.name || "",
+ imageId: mediaSosial.create.form.imageId || "",
+ iconUrl: mediaSosial.create.form.iconUrl || "",
+ };
+
+ const cek = templateMediaSosial.safeParse(formData);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ mediaSosial.create.loading = true;
+ const res = await ApiFetch.api.landingpage.mediasosial["create"].post(
+ formData
+ );
+ if (res.status === 200) {
+ mediaSosial.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ mediaSosial.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
+ mediaSosial.findMany.loading = true; // Use the full path to access the property
+ mediaSosial.findMany.page = page;
+ mediaSosial.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.mediasosial[
+ "findMany"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ mediaSosial.findMany.data = res.data.data || [];
+ mediaSosial.findMany.total = res.data.total || 0;
+ mediaSosial.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load media sosial:", res.data?.message);
+ mediaSosial.findMany.data = [];
+ mediaSosial.findMany.total = 0;
+ mediaSosial.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading media sosial:", error);
+ mediaSosial.findMany.data = [];
+ mediaSosial.findMany.total = 0;
+ mediaSosial.findMany.totalPages = 1;
+ } finally {
+ mediaSosial.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.MediaSosialGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ mediaSosial.update.loading = true;
+ try {
+ const res = await fetch(`/api/landingpage/mediasosial/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ mediaSosial.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch media sosial:", res.statusText);
+ mediaSosial.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching media sosial:", error);
+ mediaSosial.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ mediaSosial.delete.loading = true;
+
+ const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Media Sosial berhasil dihapus");
+ await mediaSosial.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus media sosial");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus media sosial");
+ } finally {
+ mediaSosial.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: {} as MediaSosialForm,
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
+
+ try {
+ const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name || "",
+ imageId: data.imageId || "",
+ iconUrl: data.iconUrl || "",
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data media sosial");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data media sosial");
+ } finally {
+ mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
+ }
+ },
+
+ async update() {
+ const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ mediaSosial.update.loading = true;
+
+ const response = await fetch(`/api/landingpage/mediasosial/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ imageId: this.form.imageId,
+ iconUrl: this.form.iconUrl,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update media sosial");
+ await mediaSosial.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update media sosial");
+ }
+ } catch (error) {
+ console.error("Error updating media sosial:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update media sosial"
+ );
+ return false;
+ } finally {
+ mediaSosial.update.loading = false;
+ }
+ },
+ },
+});
+
+const profileLandingPageState = proxy({
+ programInovasi,
+ pejabatDesa,
+ mediaSosial,
+});
+
+export default profileLandingPageState;
diff --git a/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts b/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
new file mode 100644
index 00000000..51686ee5
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
@@ -0,0 +1,261 @@
+/* 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 templatesdgsDesaForm = z.object({
+ name: z.string().min(1, "Judul minimal 1 karakter"),
+ jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ imageId: z.string().min(1, "File minimal 1"),
+});
+
+const defaultsdgsDesaForm = {
+ name: "",
+ jumlah: "",
+ imageId: "",
+};
+
+const sdgsDesa = proxy({
+ create: {
+ form: { ...defaultsdgsDesaForm },
+ loading: false,
+ async create() {
+ const cek = templatesdgsDesaForm.safeParse(
+ sdgsDesa.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ sdgsDesa.create.loading = true;
+ const res = await ApiFetch.api.landingpage.sdgsdesa[
+ "create"
+ ].post({
+ ...sdgsDesa.create.form,
+ });
+
+ if (res.status === 200) {
+ sdgsDesa.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ sdgsDesa.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
+ sdgsDesa.findMany.loading = true; // Use the full path to access the property
+ sdgsDesa.findMany.page = page;
+ sdgsDesa.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.landingpage.sdgsdesa[
+ "findMany"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ sdgsDesa.findMany.data = res.data.data || [];
+ sdgsDesa.findMany.total = res.data.total || 0;
+ sdgsDesa.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load media sosial:", res.data?.message);
+ sdgsDesa.findMany.data = [];
+ sdgsDesa.findMany.total = 0;
+ sdgsDesa.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading media sosial:", error);
+ sdgsDesa.findMany.data = [];
+ sdgsDesa.findMany.total = 0;
+ sdgsDesa.findMany.totalPages = 1;
+ } finally {
+ sdgsDesa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.SdgsDesaGetPayload<{
+ include: {
+ image: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ sdgsDesa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ sdgsDesa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ sdgsDesa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ sdgsDesa.delete.loading = true;
+
+ const response = await fetch(
+ `/api/landingpage/sdgsdesa/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "sdgs desa berhasil dihapus");
+ await sdgsDesa.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus sdgs desa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus sdgs desa");
+ } finally {
+ sdgsDesa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultsdgsDesaForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ sdgsDesa.edit.loading = true;
+
+ const response = await fetch(`/api/landingpage/sdgsdesa/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ jumlah: data.jumlah,
+ imageId: data.imageId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading sdgs desa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ } finally {
+ sdgsDesa.edit.loading = false;
+ }
+ },
+
+ async update() {
+ const cek = templatesdgsDesaForm.safeParse(
+ sdgsDesa.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ sdgsDesa.edit.loading = true;
+ const response = await fetch(
+ `/api/landingpage/sdgsdesa/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ jumlah: this.form.jumlah,
+ imageId: this.form.imageId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update sdgs desa");
+ await sdgsDesa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate sdgs desa"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating sdgs desa:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate sdgs desa"
+ );
+ return false;
+ } finally {
+ sdgsDesa.edit.loading = false;
+ }
+ },
+ reset() {
+ sdgsDesa.edit.id = "";
+ sdgsDesa.edit.form = { ...defaultsdgsDesaForm };
+ },
+ },
+});
+
+export default sdgsDesa;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts b/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts
new file mode 100644
index 00000000..f7092245
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts
@@ -0,0 +1,231 @@
+/* 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({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
+ icon: z.string().min(1, "Icon minimal 1 karakter"),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ jumlah: "",
+ icon: "",
+};
+
+const dataLingkunganDesaState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(dataLingkunganDesaState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ dataLingkunganDesaState.create.loading = true;
+ const res = await ApiFetch.api.lingkungan.datalingkungandesa["create"].post(
+ dataLingkunganDesaState.create.form
+ );
+ if (res.status === 200) {
+ dataLingkunganDesaState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ dataLingkunganDesaState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ dataLingkunganDesaState.findMany.loading = true; // Use the full path to access the property
+ dataLingkunganDesaState.findMany.page = page;
+ dataLingkunganDesaState.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.lingkungan.datalingkungandesa["find-many"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ dataLingkunganDesaState.findMany.data = res.data.data || [];
+ dataLingkunganDesaState.findMany.total = res.data.total || 0;
+ dataLingkunganDesaState.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load berdasarkan data lingkungan desa :",
+ res.data?.message
+ );
+ dataLingkunganDesaState.findMany.data = [];
+ dataLingkunganDesaState.findMany.total = 0;
+ dataLingkunganDesaState.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading berdasarkan data lingkungan desa :", error);
+ dataLingkunganDesaState.findMany.data = [];
+ dataLingkunganDesaState.findMany.total = 0;
+ dataLingkunganDesaState.findMany.totalPages = 1;
+ } finally {
+ dataLingkunganDesaState.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/lingkungan/datalingkungandesa/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ jumlah: data.jumlah,
+ icon: data.icon,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data");
+ }
+ } catch (error) {
+ console.error("Error loading data lingkungan desa :", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/lingkungan/datalingkungandesa/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await dataLingkunganDesaState.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data data lingkungan desa");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DataLingkunganDesaGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/lingkungan/datalingkungandesa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ dataLingkunganDesaState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ dataLingkunganDesaState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading data lingkungan desa:", error);
+ dataLingkunganDesaState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ dataLingkunganDesaState.delete.loading = true;
+
+ const response = await fetch(`/api/lingkungan/datalingkungandesa/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Data lingkungan desa berhasil dihapus");
+ await dataLingkunganDesaState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus data lingkungan desa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus data lingkungan desa");
+ } finally {
+ dataLingkunganDesaState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default dataLingkunganDesaState;
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan.ts b/src/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan.ts
new file mode 100644
index 00000000..aae10246
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan.ts
@@ -0,0 +1,240 @@
+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 templateTujuanEdukasiForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type TujuanEdukasiForm = Prisma.TujuanEdukasiLingkunganGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateTujuanEdukasi = proxy({
+ findById: {
+ data: null as TujuanEdukasiForm | null,
+ loading: false,
+ initialize() {
+ stateTujuanEdukasi.findById.data = {
+ id: '',
+ judul: '',
+ deskripsi: '',
+ } as TujuanEdukasiForm;
+ },
+ async load(id: string) {
+ try {
+ stateTujuanEdukasi.findById.loading = true;
+ const res = await ApiFetch.api.lingkungan.edukasilingkungan.tujuanedukasilingkungan["find-by-id"].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateTujuanEdukasi.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data tujuan edukasi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data tujuan edukasi");
+ } finally {
+ stateTujuanEdukasi.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: TujuanEdukasiForm) {
+ const cek = templateTujuanEdukasiForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateTujuanEdukasi.update.loading = true;
+ const res = await ApiFetch.api.lingkungan.edukasilingkungan.tujuanedukasilingkungan["update"].post(data);
+ if (res.status === 200) {
+ toast.success("Data tujuan edukasi berhasil diubah");
+ await stateTujuanEdukasi.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data tujuan edukasi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data tujuan edukasi");
+ } finally {
+ stateTujuanEdukasi.update.loading = false;
+ }
+ },
+ },
+});
+
+const templateMateriEdukasiLingkunganForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ });
+
+ type MateriEdukasiLingkunganForm = Prisma.MateriEdukasiLingkunganGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+ }>;
+
+ const stateMateriEdukasiLingkungan = proxy({
+ findById: {
+ data: null as MateriEdukasiLingkunganForm | null,
+ loading: false,
+ initialize() {
+ stateMateriEdukasiLingkungan.findById.data = {
+ id: '',
+ judul: '',
+ deskripsi: '',
+ } as MateriEdukasiLingkunganForm;
+ },
+ async load(id: string) {
+ try {
+ stateMateriEdukasiLingkungan.findById.loading = true;
+ const res = await ApiFetch.api.lingkungan.edukasilingkungan.materiedukasilingkungan["find-by-id"].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateMateriEdukasiLingkungan.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data materi edukasi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data materi edukasi");
+ } finally {
+ stateMateriEdukasiLingkungan.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: MateriEdukasiLingkunganForm) {
+ const cek = templateMateriEdukasiLingkunganForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateMateriEdukasiLingkungan.update.loading = true;
+ const res = await ApiFetch.api.lingkungan.edukasilingkungan.materiedukasilingkungan["update"].post(data);
+ if (res.status === 200) {
+ toast.success("Data materi edukasi berhasil diubah");
+ await stateMateriEdukasiLingkungan.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data materi edukasi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data materi edukasi");
+ } finally {
+ stateMateriEdukasiLingkungan.update.loading = false;
+ }
+ },
+ },
+ });
+
+ const templateContohEdukasiLingkunganForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ });
+
+ type ContohEdukasiLingkunganForm = Prisma.ContohEdukasiLingkunganGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+ }>;
+
+ const stateContohEdukasiLingkungan = proxy({
+ findById: {
+ data: null as ContohEdukasiLingkunganForm | null,
+ loading: false,
+ initialize() {
+ stateContohEdukasiLingkungan.findById.data = {
+ id: '',
+ judul: '',
+ deskripsi: '',
+ } as ContohEdukasiLingkunganForm;
+ },
+ async load(id: string) {
+ try {
+ stateContohEdukasiLingkungan.findById.loading = true;
+ const res = await ApiFetch.api.lingkungan.edukasilingkungan.contohkegiatandesa["find-by-id"].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateContohEdukasiLingkungan.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data contoh edukasi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data contoh edukasi");
+ } finally {
+ stateContohEdukasiLingkungan.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: ContohEdukasiLingkunganForm) {
+ const cek = templateContohEdukasiLingkunganForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateContohEdukasiLingkungan.update.loading = true;
+ const res = await ApiFetch.api.lingkungan.edukasilingkungan.contohkegiatandesa["update"].post(data);
+ if (res.status === 200) {
+ toast.success("Data contoh edukasi berhasil diubah");
+ await stateContohEdukasiLingkungan.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data contoh edukasi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data contoh edukasi");
+ } finally {
+ stateContohEdukasiLingkungan.update.loading = false;
+ }
+ },
+ },
+ });
+
+
+const stateEdukasiLingkungan = proxy({
+ stateTujuanEdukasi,
+ stateMateriEdukasiLingkungan,
+ stateContohEdukasiLingkungan
+})
+
+
+export default stateEdukasiLingkungan;
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/gotong-royong.ts b/src/app/admin/(dashboard)/_state/lingkungan/gotong-royong.ts
new file mode 100644
index 00000000..d966a861
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/lingkungan/gotong-royong.ts
@@ -0,0 +1,576 @@
+/* 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 templateKegiatanDesaForm = z.object({
+ judul: z.string().min(1, "Judul minimal 1 karakter"),
+ deskripsiSingkat: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
+ deskripsiLengkap: z.string().min(1, "Deskripsi lengkap minimal 1 karakter"),
+ tanggal: z.date(),
+ lokasi: z.string().min(1, "Lokasi minimal 1 karakter"),
+ partisipan: z.number().min(1, "Partisipan minimal 1"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+ kategoriKegiatanId: z.string().min(1, "Kategori kegiatan minimal 1"),
+});
+
+const defaultKegiatanDesaForm = {
+ judul: "",
+ deskripsiSingkat: "",
+ deskripsiLengkap: "",
+ tanggal: new Date(),
+ lokasi: "",
+ partisipan: 0,
+ imageId: "",
+ kategoriKegiatanId: "",
+};
+
+const kegiatanDesa = proxy({
+ create: {
+ form: { ...defaultKegiatanDesaForm },
+ loading: false,
+ async create() {
+ const cek = templateKegiatanDesaForm.safeParse(kegiatanDesa.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kegiatanDesa.create.loading = true;
+ const res = await ApiFetch.api.lingkungan.kegiatandesa["create"].post({
+ ...kegiatanDesa.create.form,
+ tanggal: kegiatanDesa.create.form.tanggal.toISOString(), // ✅ convert Date -> string
+ });
+
+ if (res.status === 200) {
+ kegiatanDesa.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ kegiatanDesa.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.KegiatanDesaGetPayload<{
+ include: {
+ image: true;
+ kategoriKegiatan: true;
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", kategori = "") => {
+ // Change to arrow function
+ kegiatanDesa.findMany.loading = true; // Use the full path to access the property
+ kegiatanDesa.findMany.page = page;
+ kegiatanDesa.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+ const res = await ApiFetch.api.lingkungan.kegiatandesa[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kegiatanDesa.findMany.data = res.data.data || [];
+ kegiatanDesa.findMany.total = res.data.total || 0;
+ kegiatanDesa.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load kegiatan desa:",
+ res.data?.message
+ );
+ kegiatanDesa.findMany.data = [];
+ kegiatanDesa.findMany.total = 0;
+ kegiatanDesa.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading kegiatan desa:", error);
+ kegiatanDesa.findMany.data = [];
+ kegiatanDesa.findMany.total = 0;
+ kegiatanDesa.findMany.totalPages = 1;
+ } finally {
+ kegiatanDesa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KegiatanDesaGetPayload<{
+ include: {
+ image: true;
+ kategoriKegiatan: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/lingkungan/kegiatandesa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kegiatanDesa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kegiatanDesa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kegiatanDesa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kegiatanDesa.delete.loading = true;
+
+ const response = await fetch(`/api/lingkungan/kegiatandesa/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "kegiatan desa berhasil dihapus");
+ await kegiatanDesa.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus pasar desa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pasar desa");
+ } finally {
+ kegiatanDesa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultKegiatanDesaForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ kegiatanDesa.edit.loading = true;
+
+ const response = await fetch(`/api/lingkungan/kegiatandesa/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ deskripsiSingkat: data.deskripsiSingkat,
+ deskripsiLengkap: data.deskripsiLengkap,
+ tanggal: data.tanggal,
+ lokasi: data.lokasi,
+ partisipan: data.partisipan,
+ imageId: data.imageId,
+ kategoriKegiatanId: data.kategoriKegiatanId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kegiatan desa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ } finally {
+ kegiatanDesa.edit.loading = false;
+ }
+ },
+
+ async update() {
+ const cek = templateKegiatanDesaForm.safeParse(kegiatanDesa.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kegiatanDesa.edit.loading = true;
+ const response = await fetch(
+ `/api/lingkungan/kegiatandesa/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ deskripsiSingkat: this.form.deskripsiSingkat,
+ deskripsiLengkap: this.form.deskripsiLengkap,
+ tanggal:
+ typeof this.form.tanggal === "string"
+ ? this.form.tanggal
+ : this.form.tanggal.toISOString(),
+ lokasi: this.form.lokasi,
+ partisipan: this.form.partisipan,
+ imageId: this.form.imageId,
+ kategoriKegiatanId: this.form.kategoriKegiatanId,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update kegiatan desa");
+ await kegiatanDesa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate kegiatan desa");
+ }
+ } catch (error) {
+ console.error("Error updating kegiatan desa:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kegiatan desa"
+ );
+ return false;
+ } finally {
+ kegiatanDesa.edit.loading = false;
+ }
+ },
+ reset() {
+ kegiatanDesa.edit.id = "";
+ kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
+ },
+ },
+ findFirst: {
+ data: null as Prisma.KegiatanDesaGetPayload<{
+ include: {
+ image: true;
+ kategoriKegiatan: true;
+ };
+ }> | null,
+ loading: false,
+ // findFirst.load()
+ async load(kategori?: string) {
+ this.loading = true;
+ try {
+ const res = await ApiFetch.api.lingkungan.kegiatandesa["find-first"].get({
+ query: kategori ? { kategori } : {},
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data || null;
+ } else {
+ this.data = null;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kegiatan desa terbaru:", err);
+ this.data = null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= KATEGORI kegiatan ========================================= //
+const kategoriKegiatanForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+});
+
+const kategoriKegiatanDefaultForm = {
+ nama: "",
+};
+
+const kategoriKegiatan = proxy({
+ create: {
+ form: { ...kategoriKegiatanDefaultForm },
+ loading: false,
+ async create() {
+ const cek = kategoriKegiatanForm.safeParse(kategoriKegiatan.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ kategoriKegiatan.create.loading = true;
+ const res = await ApiFetch.api.lingkungan.kategorikegiatan["create"].post(kategoriKegiatan.create.form);
+ if (res.status === 200) {
+ kategoriKegiatan.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ kategoriKegiatan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<{
+ id: string;
+ nama: string;
+ }> | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kategoriKegiatan.findMany.loading = true; // ✅ Akses langsung via nama path
+ kategoriKegiatan.findMany.page = page;
+ kategoriKegiatan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.lingkungan.kategorikegiatan[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriKegiatan.findMany.data = res.data.data ?? [];
+ kategoriKegiatan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kategoriKegiatan.findMany.data = [];
+ kategoriKegiatan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori kegiatan paginated:", err);
+ kategoriKegiatan.findMany.data = [];
+ kategoriKegiatan.findMany.totalPages = 1;
+ } finally {
+ kategoriKegiatan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriKegiatanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/lingkungan/kategorikegiatan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ kategoriKegiatan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriKegiatan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriKegiatan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriKegiatan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/lingkungan/kategorikegiatan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Kategori kegiatan berhasil dihapus");
+ await kategoriKegiatan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus kategori kegiatan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus kategori kegiatan");
+ } finally {
+ kategoriKegiatan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...kategoriKegiatanDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/lingkungan/kategorikegiatan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori kegiatan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = kategoriKegiatanForm.safeParse(kategoriKegiatan.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriKegiatan.edit.loading = true;
+ const response = await fetch(
+ `/api/lingkungan/kategorikegiatan/${kategoriKegiatan.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: kategoriKegiatan.edit.form.nama,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate kategori kegiatan (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui kategori kegiatan"
+ );
+ await kategoriKegiatan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate kategori kegiatan"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating kategori kegiatan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate kategori kegiatan"
+ );
+ return false;
+ } finally {
+ kategoriKegiatan.edit.loading = false;
+ }
+ },
+ reset() {
+ kategoriKegiatan.edit.id = "";
+ kategoriKegiatan.edit.form = { ...kategoriKegiatanDefaultForm };
+ },
+ },
+});
+
+const gotongRoyongState = proxy({
+ kegiatanDesa,
+ kategoriKegiatan,
+});
+export default gotongRoyongState;
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali.ts b/src/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali.ts
new file mode 100644
index 00000000..ae7598c7
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali.ts
@@ -0,0 +1,274 @@
+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 templateFilosofiTriHitaForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type FilosofiTriHitaForm = Prisma.FilosofiTriHitaGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateFilosofiTriHita = proxy({
+ findById: {
+ data: null as FilosofiTriHitaForm | null,
+ loading: false,
+ initialize() {
+ stateFilosofiTriHita.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as FilosofiTriHitaForm;
+ },
+ async load(id: string) {
+ try {
+ stateFilosofiTriHita.findById.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.konservasiadatbali.filosofitrihitakarana[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateFilosofiTriHita.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data filosofi tri hita karana");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengambil data filosofi tri hita karana"
+ );
+ } finally {
+ stateFilosofiTriHita.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: FilosofiTriHitaForm) {
+ const cek = templateFilosofiTriHitaForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateFilosofiTriHita.update.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.konservasiadatbali.filosofitrihitakarana[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data filosofi tri hita karana berhasil diubah");
+ await stateFilosofiTriHita.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data filosofi tri hita karana");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengubah data filosofi tri hita karana"
+ );
+ } finally {
+ stateFilosofiTriHita.update.loading = false;
+ }
+ },
+ },
+});
+
+const templateNilaiKonservasiAdatForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type NilaiKonservasiAdatForm = Prisma.NilaiKonservasiAdatGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateNilaiKonservasiAdat = proxy({
+ findById: {
+ data: null as NilaiKonservasiAdatForm | null,
+ loading: false,
+ initialize() {
+ stateNilaiKonservasiAdat.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as NilaiKonservasiAdatForm;
+ },
+ async load(id: string) {
+ try {
+ stateNilaiKonservasiAdat.findById.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.konservasiadatbali.nilaikonservasiadatbali[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateNilaiKonservasiAdat.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data nilai konservasi adat");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengambil data nilai konservasi adat"
+ );
+ } finally {
+ stateNilaiKonservasiAdat.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: NilaiKonservasiAdatForm) {
+ const cek = templateNilaiKonservasiAdatForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateNilaiKonservasiAdat.update.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.konservasiadatbali.nilaikonservasiadatbali[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data nilai konservasi adat berhasil diubah");
+ await stateNilaiKonservasiAdat.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data nilai konservasi adat");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengubah data nilai konservasi adat"
+ );
+ } finally {
+ stateNilaiKonservasiAdat.update.loading = false;
+ }
+ },
+ },
+});
+
+const templateBentukKonservasiBerdasarkanAdatForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type BentukKonservasiBerdasarkanAdatForm =
+ Prisma.BentukKonservasiBerdasarkanAdatGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+ }>;
+
+const stateBentukKonservasiBerdasarkanAdat = proxy({
+ findById: {
+ data: null as BentukKonservasiBerdasarkanAdatForm | null,
+ loading: false,
+ initialize() {
+ stateBentukKonservasiBerdasarkanAdat.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as BentukKonservasiBerdasarkanAdatForm;
+ },
+ async load(id: string) {
+ try {
+ stateBentukKonservasiBerdasarkanAdat.findById.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.konservasiadatbali.bentukkonservasiberdasarkanadat[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateBentukKonservasiBerdasarkanAdat.findById.data =
+ res.data?.data ?? null;
+ } else {
+ toast.error(
+ "Gagal mengambil data bentuk konservasi berdasarkan adat"
+ );
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengambil data bentuk konservasi berdasarkan adat"
+ );
+ } finally {
+ stateBentukKonservasiBerdasarkanAdat.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: BentukKonservasiBerdasarkanAdatForm) {
+ const cek = templateBentukKonservasiBerdasarkanAdatForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateBentukKonservasiBerdasarkanAdat.update.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.konservasiadatbali.bentukkonservasiberdasarkanadat[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success(
+ "Data bentuk konservasi berdasarkan adat berhasil diubah"
+ );
+ await stateBentukKonservasiBerdasarkanAdat.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data bentuk konservasi berdasarkan adat");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengubah data bentuk konservasi berdasarkan adat"
+ );
+ } finally {
+ stateBentukKonservasiBerdasarkanAdat.update.loading = false;
+ }
+ },
+ },
+});
+
+const stateKonservasiAdatBali = proxy({
+ stateFilosofiTriHita,
+ stateNilaiKonservasiAdat,
+ stateBentukKonservasiBerdasarkanAdat,
+});
+
+export default stateKonservasiAdatBali;
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts b/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts
new file mode 100644
index 00000000..584bad91
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts
@@ -0,0 +1,497 @@
+/* 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({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ icon: z.string().min(1, "Icon minimal 1 karakter"),
+});
+
+const defaultForm = {
+ name: "",
+ icon: "",
+};
+
+const pengelolaanSampah = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(pengelolaanSampah.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ pengelolaanSampah.create.loading = true;
+ const res = await ApiFetch.api.lingkungan.pengelolaansampah[
+ "create"
+ ].post(pengelolaanSampah.create.form);
+ if (res.status === 200) {
+ pengelolaanSampah.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ pengelolaanSampah.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
+ pengelolaanSampah.findMany.page = page;
+ pengelolaanSampah.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.lingkungan.pengelolaansampah[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pengelolaanSampah.findMany.data = res.data.data || [];
+ pengelolaanSampah.findMany.total = res.data.total || 0;
+ pengelolaanSampah.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load pengelolaan sampah:",
+ res.data?.message
+ );
+ pengelolaanSampah.findMany.data = [];
+ pengelolaanSampah.findMany.total = 0;
+ pengelolaanSampah.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pengelolaan sampah:", error);
+ pengelolaanSampah.findMany.data = [];
+ pengelolaanSampah.findMany.total = 0;
+ pengelolaanSampah.findMany.totalPages = 1;
+ } finally {
+ pengelolaanSampah.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/lingkungan/pengelolaansampah/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ icon: data.icon,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data");
+ }
+ } catch (error) {
+ console.error("Error loading pengelolaan sampah:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/lingkungan/pengelolaansampah/${id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ }
+ );
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await pengelolaanSampah.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data pengelolaan sampah");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ProgramKreatifGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/lingkungan/pengelolaansampah/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ pengelolaanSampah.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ pengelolaanSampah.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading pengelolaan sampah:", error);
+ pengelolaanSampah.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pengelolaanSampah.delete.loading = true;
+
+ const response = await fetch(
+ `/api/lingkungan/pengelolaansampah/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "pengelolaan sampah berhasil dihapus"
+ );
+ await pengelolaanSampah.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus pengelolaan sampah");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pengelolaan sampah");
+ } finally {
+ pengelolaanSampah.delete.loading = false;
+ }
+ },
+ },
+});
+
+const templateKeteranganSampahForm = z.object({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ alamat: z.string().min(1, "Alamat minimal 1 karakter"),
+ namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
+ lat: z.number(),
+ lng: z.number(),
+});
+
+const defaultKeteranganSampahForm = {
+ name: "",
+ alamat: "",
+ namaTempatMaps: "",
+ lat: 0,
+ lng: 0,
+};
+
+
+const keteranganSampah = proxy({
+ create: {
+ form: { ...defaultKeteranganSampahForm },
+ loading: false,
+ async create() {
+ const cek = templateKeteranganSampahForm.safeParse(
+ keteranganSampah.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ keteranganSampah.create.loading = true;
+ const res =
+ await ApiFetch.api.lingkungan.keteranganbankterdekat[
+ "create"
+ ].post(keteranganSampah.create.form);
+ if (res.status === 200) {
+ keteranganSampah.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ keteranganSampah.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.KeteranganBankSampahTerdekatGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ keteranganSampah.findMany.loading = true; // Use the full path to access the property
+ keteranganSampah.findMany.page = page;
+ keteranganSampah.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ keteranganSampah.findMany.data = res.data.data || [];
+ keteranganSampah.findMany.total = res.data.total || 0;
+ keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load keterangan bank sampah terdekat:",
+ res.data?.message
+ );
+ keteranganSampah.findMany.data = [];
+ keteranganSampah.findMany.total = 0;
+ keteranganSampah.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading keterangan bank sampah terdekat:", error);
+ keteranganSampah.findMany.data = [];
+ keteranganSampah.findMany.total = 0;
+ keteranganSampah.findMany.totalPages = 1;
+ } finally {
+ keteranganSampah.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ keteranganSampah.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ keteranganSampah.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ keteranganSampah.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ keteranganSampah.delete.loading = true;
+
+ const response = await fetch(`/api/lingkungan/keteranganbankterdekat/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Keterangan sampah berhasil dihapus");
+ await keteranganSampah.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus keterangan sampah");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus keterangan sampah");
+ } finally {
+ keteranganSampah.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultKeteranganSampahForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ alamat: data.alamat,
+ namaTempatMaps: data.namaTempatMaps,
+ lat: data.lat,
+ lng: data.lng,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading keterangan sampah:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateKeteranganSampahForm.safeParse(keteranganSampah.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ keteranganSampah.edit.loading = true;
+ const response = await fetch(
+ `/api/lingkungan/keteranganbankterdekat/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ alamat: this.form.alamat,
+ namaTempatMaps: this.form.namaTempatMaps,
+ lat: this.form.lat,
+ lng: this.form.lng,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update keterangan sampah");
+ await keteranganSampah.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate keterangan sampah");
+ }
+ } catch (error) {
+ console.error("Error updating keterangan sampah:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate keterangan sampah"
+ );
+ return false;
+ } finally {
+ keteranganSampah.edit.loading = false;
+ }
+ },
+ reset() {
+ keteranganSampah.edit.id = "";
+ keteranganSampah.edit.form = { ...defaultKeteranganSampahForm };
+ },
+ },
+});
+
+const pengelolaanSampahState = proxy({
+ pengelolaanSampah,
+ keteranganSampah,
+});
+
+export default pengelolaanSampahState;
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts b/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts
new file mode 100644
index 00000000..84d3083e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts
@@ -0,0 +1,231 @@
+/* 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({
+ name: z.string().min(1, "Nama minimal 1 karakter"),
+ deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
+ judul: z.string().min(1, "Judul minimal 1 karakter"),
+ icon: z.string().min(1, "Icon minimal 1 karakter"),
+});
+
+const defaultForm = {
+ name: "",
+ deskripsi: "",
+ judul: "",
+ icon: "",
+};
+
+const programPenghijauanState = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateForm.safeParse(programPenghijauanState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ programPenghijauanState.create.loading = true;
+ const res = await ApiFetch.api.lingkungan.programpenghijauan["create"].post(
+ programPenghijauanState.create.form
+ );
+ if (res.status === 200) {
+ programPenghijauanState.findMany.load();
+ return toast.success("success create");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ programPenghijauanState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ programPenghijauanState.findMany.loading = true; // Use the full path to access the property
+ programPenghijauanState.findMany.page = page;
+ programPenghijauanState.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.lingkungan.programpenghijauan["find-many"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ programPenghijauanState.findMany.data = res.data.data || [];
+ programPenghijauanState.findMany.total = res.data.total || 0;
+ programPenghijauanState.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load grafik berdasarkan program penghijauan:",
+ res.data?.message
+ );
+ programPenghijauanState.findMany.data = [];
+ programPenghijauanState.findMany.total = 0;
+ programPenghijauanState.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan program penghijauan:", error);
+ programPenghijauanState.findMany.data = [];
+ programPenghijauanState.findMany.total = 0;
+ programPenghijauanState.findMany.totalPages = 1;
+ } finally {
+ programPenghijauanState.findMany.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/lingkungan/programpenghijauan/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ deskripsi: data.deskripsi,
+ judul: data.judul,
+ icon: data.icon,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data");
+ }
+ } catch (error) {
+ console.error("Error loading program penghijauan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateForm.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/lingkungan/programpenghijauan/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await programPenghijauanState.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data program penghijauan");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.ProgramPenghijauanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/lingkungan/programpenghijauan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ programPenghijauanState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ programPenghijauanState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading program penghijauan:", error);
+ programPenghijauanState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ programPenghijauanState.delete.loading = true;
+
+ const response = await fetch(`/api/lingkungan/programpenghijauan/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Program penghijauan berhasil dihapus");
+ await programPenghijauanState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus program penghijauan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus program penghijauan");
+ } finally {
+ programPenghijauanState.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default programPenghijauanState;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts b/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
new file mode 100644
index 00000000..7337d64c
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
@@ -0,0 +1,558 @@
+/* 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";
+
+// ========================================= BEASISWA PENDAFTAR ========================================= //
+
+const templateBeasiswaPendaftar = z.object({
+ namaLengkap: z.string().min(1, "Nama harus diisi"),
+ nik: z.string().min(1, "NIK harus diisi"),
+ tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
+ tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
+ jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
+ kewarganegaraan: z.string().min(1, "Kewarganegaraan harus diisi"),
+ agama: z.string().min(1, "Agama harus diisi"),
+ alamatKTP: z.string().min(1, "Alamat KTP harus diisi"),
+ alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
+ noHp: z.string().min(1, "No HP harus diisi"),
+ email: z.string().min(1, "Email harus diisi"),
+ statusPernikahan: z.string().min(1, "Status pernikahan harus diisi"),
+ ukuranBaju: z.string().min(1, "Ukuran baju harus diisi"),
+});
+
+const defaultBeasiswaPendaftar = {
+ namaLengkap: "",
+ nik: "",
+ tempatLahir: "",
+ tanggalLahir: "",
+ jenisKelamin: "",
+ kewarganegaraan: "",
+ agama: "",
+ alamatKTP: "",
+ alamatDomisili: "",
+ noHp: "",
+ email: "",
+ statusPernikahan: "",
+ ukuranBaju: "",
+};
+
+const beasiswaPendaftar = proxy({
+ create: {
+ form: { ...defaultBeasiswaPendaftar },
+ loading: false,
+ async create() {
+ const cek = templateBeasiswaPendaftar.safeParse(
+ beasiswaPendaftar.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ beasiswaPendaftar.create.loading = true;
+ const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
+ "create"
+ ].post(beasiswaPendaftar.create.form);
+ if (res.status === 200) {
+ beasiswaPendaftar.findMany.load();
+ return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ beasiswaPendaftar.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.BeasiswaPendaftarGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
+ beasiswaPendaftar.findMany.page = page;
+ beasiswaPendaftar.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ beasiswaPendaftar.findMany.data = res.data.data ?? [];
+ beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ beasiswaPendaftar.findMany.data = [];
+ beasiswaPendaftar.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch beasiswa pendaftar paginated:", err);
+ beasiswaPendaftar.findMany.data = [];
+ beasiswaPendaftar.findMany.totalPages = 1;
+ } finally {
+ beasiswaPendaftar.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.BeasiswaPendaftarGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/beasiswa/beasiswapendaftar/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ beasiswaPendaftar.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ beasiswaPendaftar.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ beasiswaPendaftar.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ beasiswaPendaftar.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/beasiswa/beasiswapendaftar/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Beasiswa berhasil dihapus");
+ await beasiswaPendaftar.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus beasiswa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus beasiswa");
+ } finally {
+ beasiswaPendaftar.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultBeasiswaPendaftar },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/beasiswa/beasiswapendaftar/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ namaLengkap: data.namaLengkap,
+ nik: data.nik,
+ tempatLahir: data.tempatLahir,
+ tanggalLahir: data.tanggalLahir,
+ jenisKelamin: data.jenisKelamin,
+ kewarganegaraan: data.kewarganegaraan,
+ agama: data.agama,
+ alamatKTP: data.alamatKTP,
+ alamatDomisili: data.alamatDomisili,
+ noHp: data.noHp,
+ email: data.email,
+ statusPernikahan: data.statusPernikahan,
+ ukuranBaju: data.ukuranBaju,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading beasiswa pendaftar:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateBeasiswaPendaftar.safeParse(
+ beasiswaPendaftar.update.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ beasiswaPendaftar.update.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/beasiswa/beasiswapendaftar/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ namaLengkap: this.form.namaLengkap,
+ nik: this.form.nik,
+ tanggalLahir: this.form.tanggalLahir,
+ jenisKelamin: this.form.jenisKelamin,
+ kewarganegaraan: this.form.kewarganegaraan,
+ agama: this.form.agama,
+ alamatKTP: this.form.alamatKTP,
+ alamatDomisili: this.form.alamatDomisili,
+ noHp: this.form.noHp,
+ email: this.form.email,
+ statusPernikahan: this.form.statusPernikahan,
+ ukuranBaju: this.form.ukuranBaju,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update beasiswa pendaftar");
+ await beasiswaPendaftar.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update beasiswa pendaftar");
+ }
+ } catch (error) {
+ console.error("Error updating beasiswa pendaftar:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update beasiswa pendaftar"
+ );
+ return false;
+ } finally {
+ beasiswaPendaftar.update.loading = false;
+ }
+ },
+ reset() {
+ beasiswaPendaftar.update.id = "";
+ beasiswaPendaftar.update.form = { ...defaultBeasiswaPendaftar };
+ },
+ },
+});
+
+// ========================================= KEUNGGULAN PROGRAM ========================================= //
+const templateKeunggulanProgram = z.object({
+judul: z.string().min(1, "Judul harus diisi"),
+deskripsi: z.string().min(1, "Deskripsi harus diisi"),
+});
+
+const defaultKeunggulanProgram = {
+judul: "",
+deskripsi: "",
+};
+
+const keunggulanProgram = proxy({
+ create: {
+ form: { ...defaultKeunggulanProgram },
+ loading: false,
+ async create() {
+ const cek = templateKeunggulanProgram.safeParse(
+ keunggulanProgram.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ keunggulanProgram.create.loading = true;
+ const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram[
+ "create"
+ ].post(keunggulanProgram.create.form);
+ if (res.status === 200) {
+ keunggulanProgram.findMany.load();
+ return toast.success("Data Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ keunggulanProgram.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.KeunggulanProgramGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ keunggulanProgram.findMany.loading = true; // ✅ Akses langsung via nama path
+ keunggulanProgram.findMany.page = page;
+ keunggulanProgram.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram["findMany"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ keunggulanProgram.findMany.data = res.data.data ?? [];
+ keunggulanProgram.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ keunggulanProgram.findMany.data = [];
+ keunggulanProgram.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch keunggulan program paginated:", err);
+ keunggulanProgram.findMany.data = [];
+ keunggulanProgram.findMany.totalPages = 1;
+ } finally {
+ keunggulanProgram.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KeunggulanProgramGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/beasiswa/keunggulanprogram/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ keunggulanProgram.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ keunggulanProgram.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ keunggulanProgram.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ keunggulanProgram.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/beasiswa/keunggulanprogram/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Keunggulan Program berhasil dihapus");
+ await keunggulanProgram.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus keunggulan program");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus keunggulan program");
+ } finally {
+ keunggulanProgram.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultKeunggulanProgram },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/beasiswa/keunggulanprogram/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ deskripsi: data.deskripsi,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading keunggulan program:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateKeunggulanProgram.safeParse(
+ keunggulanProgram.update.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ keunggulanProgram.update.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/beasiswa/keunggulanprogram/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ deskripsi: this.form.deskripsi,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update keunggulan program");
+ await keunggulanProgram.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update keunggulan program");
+ }
+ } catch (error) {
+ console.error("Error updating keunggulan program:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update keunggulan program"
+ );
+ return false;
+ } finally {
+ keunggulanProgram.update.loading = false;
+ }
+ },
+ reset() {
+ keunggulanProgram.update.id = "";
+ keunggulanProgram.update.form = { ...defaultKeunggulanProgram };
+ },
+ },
+});
+
+
+const beasiswaDesaState = proxy({
+ beasiswaPendaftar,
+ keunggulanProgram
+});
+
+export default beasiswaDesaState;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa.ts b/src/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa.ts
new file mode 100644
index 00000000..2c29ee46
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa.ts
@@ -0,0 +1,260 @@
+import ApiFetch from "@/lib/api-fetch";
+import { Prisma } from "@prisma/client";
+import { toast } from "react-toastify";
+import { proxy } from "valtio";
+import { z } from "zod";
+
+// ========================================= TUJUAN PROGRAM ========================================= //
+
+const templateTujuanProgramForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type TujuanProgramForm = Prisma.TujuanBimbinganBelajarDesaGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateTujuanProgram = proxy({
+ findById: {
+ data: null as TujuanProgramForm | null,
+ loading: false,
+ initialize() {
+ stateTujuanProgram.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as TujuanProgramForm;
+ },
+ async load(id: string) {
+ try {
+ stateTujuanProgram.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.bimbinganbelajardesa.tujuanprogram[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateTujuanProgram.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data tujuan program");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data tujuan program");
+ } finally {
+ stateTujuanProgram.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: TujuanProgramForm) {
+ const cek = templateTujuanProgramForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateTujuanProgram.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.bimbinganbelajardesa.tujuanprogram[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data tujuan program berhasil diubah");
+ await stateTujuanProgram.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data tujuan program");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data tujuan program");
+ } finally {
+ stateTujuanProgram.update.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= LOKASI DAN JADWAL ========================================= //
+
+const templateLokasiDanJadwalForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type LokasiDanJadwalForm = Prisma.LokasiJadwalBimbinganBelajarDesaGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const lokasiDanJadwalState = proxy({
+ findById: {
+ data: null as LokasiDanJadwalForm | null,
+ loading: false,
+ initialize() {
+ lokasiDanJadwalState.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as LokasiDanJadwalForm;
+ },
+ async load(id: string) {
+ try {
+ lokasiDanJadwalState.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.bimbinganbelajardesa.lokasidanjadwal[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ lokasiDanJadwalState.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data lokasi dan jadwal");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data lokasi dan jadwal");
+ } finally {
+ lokasiDanJadwalState.findById.loading = false;
+ }
+ },
+ },
+ update: {
+ loading: false,
+ async save(data: LokasiDanJadwalForm) {
+ const cek = templateLokasiDanJadwalForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ lokasiDanJadwalState.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.bimbinganbelajardesa.lokasidanjadwal[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data lokasi dan jadwal berhasil diubah");
+ await lokasiDanJadwalState.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data lokasi dan jadwal");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data lokasi dan jadwal");
+ } finally {
+ lokasiDanJadwalState.update.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= FASILITAS YANG DISEDIAKAN ========================================= //
+
+const templateFasilitasYangDisediakanForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type FasilitasYangDisediakanForm = Prisma.FasilitasBimbinganBelajarDesaGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const fasilitasYangDisediakanState = proxy({
+ findById: {
+ data: null as FasilitasYangDisediakanForm | null,
+ loading: false,
+ initialize() {
+ fasilitasYangDisediakanState.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as FasilitasYangDisediakanForm;
+ },
+ async load(id: string) {
+ try {
+ fasilitasYangDisediakanState.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.bimbinganbelajardesa.fasilitasyangdisediakan[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ fasilitasYangDisediakanState.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data fasilitas yang disediakan");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data fasilitas yang disediakan");
+ } finally {
+ fasilitasYangDisediakanState.findById.loading = false;
+ }
+ },
+ },
+ update: {
+ loading: false,
+ async save(data: FasilitasYangDisediakanForm) {
+ const cek = templateFasilitasYangDisediakanForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ fasilitasYangDisediakanState.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.bimbinganbelajardesa.fasilitasyangdisediakan[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data fasilitas yang disediakan berhasil diubah");
+ await fasilitasYangDisediakanState.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data fasilitas yang disediakan");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data fasilitas yang disediakan");
+ } finally {
+ fasilitasYangDisediakanState.update.loading = false;
+ }
+ },
+ },
+});
+
+const stateBimbinganBelajarDesa = proxy({
+ stateTujuanProgram,
+ lokasiDanJadwalState,
+ fasilitasYangDisediakanState,
+});
+
+export default stateBimbinganBelajarDesa;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts b/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
new file mode 100644
index 00000000..08189a83
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
@@ -0,0 +1,178 @@
+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 templateDataPendidikan = z.object({
+ name: z.string().min(1, "Data nama harus diisi"),
+ jumlah: z.string().min(1, "Data jumlah harus diisi"),
+});
+
+type DataPendidikan = Prisma.DataPendidikanGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ jumlah: true;
+ };
+}>;
+
+const defaultForm: Omit & { id?: string } = {
+ name: "",
+ jumlah: "",
+};
+
+const dataPendidikan = proxy({
+ create: {
+ form: defaultForm,
+ loading: false,
+ async create() {
+ const cek = templateDataPendidikan.safeParse(dataPendidikan.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ dataPendidikan.create.loading = true;
+ const res = await ApiFetch.api.pendidikan.datapendidikan["create"].post(
+ dataPendidikan.create.form
+ );
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ dataPendidikan.create.form = {
+ name: "",
+ jumlah: "",
+ };
+ dataPendidikan.findMany.load();
+ return id;
+ }
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ dataPendidikan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.DataPendidikanGetPayload<{
+ select: { id: true; name: true; jumlah: true };
+ }>[]
+ | null,
+ loading: false,
+ async load() {
+ const res = await ApiFetch.api.pendidikan.datapendidikan[
+ "findMany"
+ ].get();
+ if (res.status === 200) {
+ dataPendidikan.findMany.data = res.data?.data ?? [];
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DataPendidikanGetPayload<{
+ select: { id: true; name: true; jumlah: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/pendidikan/datapendidikan/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ dataPendidikan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ dataPendidikan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading data pendidikan:", error);
+ dataPendidikan.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateDataPendidikan.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => (v.path as string[]).join("."))
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/pendidikan/datapendidikan/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await dataPendidikan.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data data pendidikan:", error);
+ toast.error("Gagal update data data pendidikan");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ dataPendidikan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/datapendidikan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Data berhasil dihapus");
+ await dataPendidikan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus data");
+ }
+ } catch (error) {
+ console.error("Gagal delete data pendidikan:", error);
+ toast.error("Terjadi kesalahan saat menghapus data pendidikan");
+ } finally {
+ dataPendidikan.delete.loading = false;
+ }
+ },
+ },
+});
+export default dataPendidikan;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud.ts b/src/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud.ts
new file mode 100644
index 00000000..b2504c91
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud.ts
@@ -0,0 +1,1160 @@
+/* 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";
+
+// ========================================= JENJANG PENDIDIKAN ========================================= //
+const jenjangPendidikanForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+});
+
+const jenjangPendidikanDefaultForm = {
+ nama: "",
+};
+
+const jenjangPendidikan = proxy({
+ create: {
+ form: { ...jenjangPendidikanDefaultForm },
+ loading: false,
+ async create() {
+ const cek = jenjangPendidikanForm.safeParse(
+ jenjangPendidikan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ jenjangPendidikan.create.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
+ "create"
+ ].post(jenjangPendidikan.create.form);
+ if (res.status === 200) {
+ jenjangPendidikan.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ jenjangPendidikan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<{
+ id: string;
+ nama: string;
+ }> | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ jenjangPendidikan.findMany.loading = true; // Use the full path to access the property
+ jenjangPendidikan.findMany.page = page;
+ jenjangPendidikan.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ const res =
+ await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ jenjangPendidikan.findMany.data = res.data.data || [];
+ jenjangPendidikan.findMany.total = res.data.total || 0;
+ jenjangPendidikan.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load jenjang pendidikan:",
+ res.data?.message
+ );
+ jenjangPendidikan.findMany.data = [];
+ jenjangPendidikan.findMany.total = 0;
+ jenjangPendidikan.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading jenjang pendidikan:", error);
+ jenjangPendidikan.findMany.data = [];
+ jenjangPendidikan.findMany.total = 0;
+ jenjangPendidikan.findMany.totalPages = 1;
+ } finally {
+ jenjangPendidikan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.JenjangPendidikanGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/infosekolahpaud/jenjangpendidikan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ jenjangPendidikan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ jenjangPendidikan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ jenjangPendidikan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ jenjangPendidikan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/jenjangpendidikan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "jenjang pendidikan berhasil dihapus"
+ );
+ await jenjangPendidikan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus jenjang pendidikan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus jenjang pendidikan");
+ } finally {
+ jenjangPendidikan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...jenjangPendidikanDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/jenjangpendidikan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading jenjang pendidikan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = jenjangPendidikanForm.safeParse(jenjangPendidikan.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ jenjangPendidikan.edit.loading = true;
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/jenjangpendidikan/${jenjangPendidikan.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: jenjangPendidikan.edit.form.nama,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate jenjang pendidikan (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui jenjang pendidikan"
+ );
+ await jenjangPendidikan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate jenjang pendidikan"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating jenjang pendidikan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate jenjang pendidikan"
+ );
+ return false;
+ } finally {
+ jenjangPendidikan.edit.loading = false;
+ }
+ },
+ reset() {
+ jenjangPendidikan.edit.id = "";
+ jenjangPendidikan.edit.form = { ...jenjangPendidikanDefaultForm };
+ },
+ },
+});
+
+// ========================================= LEMBAGA PENDIDIKAN ========================================= //
+
+const lembagaPendidikanForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+ jenjangId: z.string().min(1, "Jenjang pendidikan minimal 1"),
+});
+
+const lembagaPendidikanDefaultForm = {
+ nama: "",
+ jenjangId: "",
+};
+
+const lembagaPendidikan = proxy({
+ create: {
+ form: { ...lembagaPendidikanDefaultForm },
+ loading: false,
+ async create() {
+ const cek = lembagaPendidikanForm.safeParse(
+ lembagaPendidikan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ lembagaPendidikan.create.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
+ "create"
+ ].post(lembagaPendidikan.create.form);
+ if (res.status === 200) {
+ lembagaPendidikan.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ lembagaPendidikan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.LembagaGetPayload<{
+ include: {
+ jenjangPendidikan: true;
+ };
+ }> & {
+ siswa?: [];
+ pengajar?: [];
+ }
+ > | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
+ lembagaPendidikan.findMany.loading = true;
+ lembagaPendidikan.findMany.page = page;
+ lembagaPendidikan.findMany.search = search;
+
+ try {
+ const query: any = {
+ page,
+ limit,
+ ...(search && { search }),
+ ...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
+ };
+
+ console.log('Fetching lembaga with query:', query);
+
+ const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
+
+ console.log('API Response:', res);
+
+ if (res.status === 200 && res.data?.success) {
+ const data = Array.isArray(res.data.data) ? res.data.data : [];
+ const total = typeof res.data.total === 'number' ? res.data.total : 0;
+ const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
+
+ lembagaPendidikan.findMany.data = data;
+ lembagaPendidikan.findMany.total = total;
+ lembagaPendidikan.findMany.totalPages = totalPages;
+
+ console.log('Successfully loaded lembaga data:', {
+ count: data.length,
+ total,
+ totalPages
+ });
+ } else {
+ console.error(
+ "Failed to load lembaga pendidikan:",
+ res.data?.message || 'No error message provided'
+ );
+ throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
+ }
+ } catch (error) {
+ console.error("Error loading lembaga pendidikan:", error);
+ lembagaPendidikan.findMany.data = [];
+ lembagaPendidikan.findMany.total = 0;
+ lembagaPendidikan.findMany.totalPages = 1;
+ } finally {
+ lembagaPendidikan.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.LembagaGetPayload<{
+ include: {
+ jenjangPendidikan: true;
+ siswa: true;
+ pengajar: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/infosekolahpaud/lembagapendidikan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ lembagaPendidikan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ lembagaPendidikan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ lembagaPendidikan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ lembagaPendidikan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/lembagapendidikan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "lembaga pendidikan berhasil dihapus"
+ );
+ await lembagaPendidikan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus lembaga pendidikan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus lembaga pendidikan");
+ } finally {
+ lembagaPendidikan.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...lembagaPendidikanDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/lembagapendidikan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ jenjangId: data.jenjangId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading lembaga pendidikan:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = lembagaPendidikanForm.safeParse(lembagaPendidikan.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ lembagaPendidikan.edit.loading = true;
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/lembagapendidikan/${lembagaPendidikan.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: lembagaPendidikan.edit.form.nama,
+ jenjangId: lembagaPendidikan.edit.form.jenjangId,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate lembaga pendidikan (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(
+ result.message || "Berhasil memperbarui lembaga pendidikan"
+ );
+ await lembagaPendidikan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate lembaga pendidikan"
+ );
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating lembaga pendidikan:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate lembaga pendidikan"
+ );
+ return false;
+ } finally {
+ lembagaPendidikan.edit.loading = false;
+ }
+ },
+ reset() {
+ lembagaPendidikan.edit.id = "";
+ lembagaPendidikan.edit.form = { ...lembagaPendidikanDefaultForm };
+ },
+ },
+});
+
+// ========================================= SISWA ========================================= //
+
+const siswaForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+ lembagaId: z.string().min(1, "lembaga pendidikan minimal 1"),
+});
+
+const siswaDefaultForm = {
+ nama: "",
+ lembagaId: "",
+};
+
+const siswa = proxy({
+ create: {
+ form: { ...siswaDefaultForm },
+ loading: false,
+ async create() {
+ const cek = siswaForm.safeParse(siswa.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ siswa.create.loading = true;
+ const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
+ "create"
+ ].post(siswa.create.form);
+ if (res.status === 200) {
+ siswa.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ siswa.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.SiswaGetPayload<{
+ include: {
+ lembaga: {
+ include: {
+ jenjangPendidikan: true;
+ };
+ };
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ jenjangPendidikan: "",
+ load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
+ siswa.findMany.loading = true;
+ siswa.findMany.page = page;
+ siswa.findMany.search = search;
+ siswa.findMany.jenjangPendidikan = jenjangPendidikan;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
+ const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ siswa.findMany.data = res.data.data || [];
+ siswa.findMany.total = res.data.total || 0;
+ siswa.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load siswa:",
+ res.data?.message
+ );
+ siswa.findMany.data = [];
+ siswa.findMany.total = 0;
+ siswa.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading siswa:", error);
+ siswa.findMany.data = [];
+ siswa.findMany.total = 0;
+ siswa.findMany.totalPages = 1;
+ } finally {
+ siswa.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.SiswaGetPayload<{
+ include: {
+ lembaga: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/pendidikan/infosekolahpaud/siswa/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ siswa.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ siswa.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ siswa.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ siswa.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/siswa/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "siswa berhasil dihapus");
+ await siswa.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus siswa");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus siswa");
+ } finally {
+ siswa.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...siswaDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/siswa/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ lembagaId: data.lembagaId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading siswa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = siswaForm.safeParse(siswa.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ siswa.edit.loading = true;
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/siswa/${siswa.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: siswa.edit.form.nama,
+ lembagaId: siswa.edit.form.lembagaId,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message || `Gagal mengupdate siswa (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(result.message || "Berhasil memperbarui siswa");
+ await siswa.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate siswa");
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating siswa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate siswa"
+ );
+ return false;
+ } finally {
+ siswa.edit.loading = false;
+ }
+ },
+ reset() {
+ siswa.edit.id = "";
+ siswa.edit.form = { ...siswaDefaultForm };
+ },
+ },
+});
+
+// ========================================= PENGAJAR ========================================= //
+
+const pengajarForm = z.object({
+ nama: z.string().min(1, "Nama minimal 1 karakter"),
+ lembagaId: z.string().min(1, "lembaga pendidikan minimal 1"),
+});
+
+const pengajarDefaultForm = {
+ nama: "",
+ lembagaId: "",
+};
+
+const pengajar = proxy({
+ create: {
+ form: { ...pengajarDefaultForm },
+ loading: false,
+ async create() {
+ const cek = pengajarForm.safeParse(pengajar.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ pengajar.create.loading = true;
+ const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
+ "create"
+ ].post(pengajar.create.form);
+ if (res.status === 200) {
+ pengajar.findMany.load();
+ return toast.success("Data berhasil ditambahkan");
+ }
+ return toast.error("Gagal menambahkan data");
+ } catch (error) {
+ console.log(error);
+ toast.error("Gagal menambahkan data");
+ } finally {
+ pengajar.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as Array<
+ Prisma.PengajarGetPayload<{
+ include: {
+ lembaga: {
+ include: {
+ jenjangPendidikan: true
+ }
+ }
+ };
+ }>
+ > | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ jenjangPendidikan: "",
+ load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
+ // Change to arrow function
+ pengajar.findMany.loading = true; // Use the full path to access the property
+ pengajar.findMany.page = page;
+ pengajar.findMany.search = search;
+ pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
+ const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pengajar.findMany.data = res.data.data || [];
+ pengajar.findMany.total = res.data.total || 0;
+ pengajar.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load pengajar:",
+ res.data?.message
+ );
+ pengajar.findMany.data = [];
+ pengajar.findMany.total = 0;
+ pengajar.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pengajar:", error);
+ pengajar.findMany.data = [];
+ pengajar.findMany.total = 0;
+ pengajar.findMany.totalPages = 1;
+ } finally {
+ pengajar.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PengajarGetPayload<{
+ include: {
+ lembaga: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/infosekolahpaud/pengajar/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ pengajar.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ pengajar.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ pengajar.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ pengajar.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/pengajar/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "pengajar berhasil dihapus");
+ await pengajar.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus pengajar");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus pengajar");
+ } finally {
+ pengajar.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...pengajarDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/pengajar/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ lembagaId: data.lembagaId,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading siswa:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = pengajarForm.safeParse(pengajar.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ pengajar.edit.loading = true;
+ const response = await fetch(
+ `/api/pendidikan/infosekolahpaud/pengajar/${pengajar.edit.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: pengajar.edit.form.nama,
+ lembagaId: pengajar.edit.form.lembagaId,
+ }),
+ }
+ );
+
+ // Clone the response to avoid 'body already read' error
+ const responseClone = response.clone();
+
+ try {
+ const result = await response.json();
+
+ if (!response.ok) {
+ console.error(
+ "Update failed with status:",
+ response.status,
+ "Response:",
+ result
+ );
+ throw new Error(
+ result?.message ||
+ `Gagal mengupdate pengajar (${response.status})`
+ );
+ }
+
+ if (result.success) {
+ toast.success(result.message || "Berhasil memperbarui pengajar");
+ await pengajar.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal mengupdate pengajar");
+ }
+ } catch (error) {
+ // If JSON parsing fails, try to get the response text for better error messages
+ try {
+ const text = await responseClone.text();
+ console.error("Error response text:", text);
+ throw new Error(`Gagal memproses respons dari server: ${text}`);
+ } catch (textError) {
+ console.error("Error parsing response as text:", textError);
+ console.error("Original error:", error);
+ throw new Error("Gagal memproses respons dari server");
+ }
+ }
+ } catch (error) {
+ console.error("Error updating pengajar:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal mengupdate pengajar"
+ );
+ return false;
+ } finally {
+ pengajar.edit.loading = false;
+ }
+ },
+ reset() {
+ pengajar.edit.id = "";
+ pengajar.edit.form = { ...pengajarDefaultForm };
+ },
+ },
+});
+
+const infoSekolahPaud = proxy({
+ jenjangPendidikan,
+ lembagaPendidikan,
+ siswa,
+ pengajar,
+});
+
+export default infoSekolahPaud;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal.ts b/src/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal.ts
new file mode 100644
index 00000000..d9dbce3d
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal.ts
@@ -0,0 +1,267 @@
+import ApiFetch from "@/lib/api-fetch";
+import { Prisma } from "@prisma/client";
+import { toast } from "react-toastify";
+import { proxy } from "valtio";
+import { z } from "zod";
+
+// ========================================= TUJUAN PENDIDIKAN NON FORMAL ========================================= //
+
+const templateTujuanPendidikanNonFormalForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type TujuanPendidikanNonFormalForm =
+ Prisma.TujuanPendidikanNonFormalGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+ }>;
+
+const stateTujuanPendidikanNonFormal = proxy({
+ findById: {
+ data: null as TujuanPendidikanNonFormalForm | null,
+ loading: false,
+ initialize() {
+ stateTujuanPendidikanNonFormal.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as TujuanPendidikanNonFormalForm;
+ },
+ async load(id: string) {
+ try {
+ stateTujuanPendidikanNonFormal.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateTujuanPendidikanNonFormal.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data tujuan pendidikan non formal");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengambil data tujuan pendidikan non formal"
+ );
+ } finally {
+ stateTujuanPendidikanNonFormal.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: TujuanPendidikanNonFormalForm) {
+ const cek = templateTujuanPendidikanNonFormalForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateTujuanPendidikanNonFormal.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data tujuan pendidikan non formal berhasil diubah");
+ await stateTujuanPendidikanNonFormal.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data tujuan pendidikan non formal");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengubah data tujuan pendidikan non formal"
+ );
+ } finally {
+ stateTujuanPendidikanNonFormal.update.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= TEMPAT KEGIATAN ========================================= //
+
+const templateTempatKegiatanForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type TempatKegiatanForm = Prisma.TempatKegiatanGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateTempatKegiatan = proxy({
+ findById: {
+ data: null as TempatKegiatanForm | null,
+ loading: false,
+ initialize() {
+ stateTempatKegiatan.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as TempatKegiatanForm;
+ },
+ async load(id: string) {
+ try {
+ stateTempatKegiatan.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateTempatKegiatan.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data tempat kegiatan");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data tempat kegiatan");
+ } finally {
+ stateTempatKegiatan.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: TempatKegiatanForm) {
+ const cek = templateTempatKegiatanForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateTempatKegiatan.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data tempat kegiatan berhasil diubah");
+ await stateTempatKegiatan.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data tempat kegiatan");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data tempat kegiatan");
+ } finally {
+ stateTempatKegiatan.update.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= JENIS PROGRAM YANG DISELENGGARAKAN ========================================= //
+
+const templateJenisProgramForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type JenisProgramForm = Prisma.JenisProgramYangDiselenggarakanGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateJenisProgram = proxy({
+ findById: {
+ data: null as JenisProgramForm | null,
+ loading: false,
+ initialize() {
+ stateJenisProgram.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as JenisProgramForm;
+ },
+ async load(id: string) {
+ try {
+ stateJenisProgram.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateJenisProgram.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data jenis program");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data jenis program");
+ } finally {
+ stateJenisProgram.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: JenisProgramForm) {
+ const cek = templateJenisProgramForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateJenisProgram.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data jenis program berhasil diubah");
+ await stateJenisProgram.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data jenis program");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data jenis program");
+ } finally {
+ stateJenisProgram.update.loading = false;
+ }
+ },
+ },
+});
+
+const pendidikanNonFormalState = proxy({
+ stateTujuanPendidikanNonFormal,
+ stateTempatKegiatan,
+ stateJenisProgram,
+});
+
+export default pendidikanNonFormalState;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts b/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts
new file mode 100644
index 00000000..69234af2
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts
@@ -0,0 +1,884 @@
+/* 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 templateDataPerpustakaan = z.object({
+ judul: z.string().min(1, "Judul harus diisi"),
+ deskripsi: z.string().min(1, "Deskripsi harus diisi"),
+ imageId: z.string().min(1, "Image ID harus diisi"),
+ kategoriId: z.string().min(1, "Kategori ID harus diisi"),
+});
+
+const defaultDataPerpustakaan = {
+ judul: "",
+ deskripsi: "",
+ imageId: "",
+ kategoriId: "",
+};
+
+const dataPerpustakaan = proxy({
+ create: {
+ form: { ...defaultDataPerpustakaan },
+ loading: false,
+ async create() {
+ const cek = templateDataPerpustakaan.safeParse(
+ dataPerpustakaan.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ dataPerpustakaan.create.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
+ "create"
+ ].post(dataPerpustakaan.create.form);
+ if (res.status === 200) {
+ dataPerpustakaan.findMany.load();
+ return toast.success("Data Data Perpustakaan Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ dataPerpustakaan.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.DataPerpustakaanGetPayload<{
+ include: {
+ image: true;
+ kategori: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", kategori = "") => {
+ const startTime = Date.now();
+ dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
+ dataPerpustakaan.findMany.page = page;
+ dataPerpustakaan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ dataPerpustakaan.findMany.data = res.data.data ?? [];
+ dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ dataPerpustakaan.findMany.data = [];
+ dataPerpustakaan.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch data perpustakaan paginated:", err);
+ dataPerpustakaan.findMany.data = [];
+ dataPerpustakaan.findMany.totalPages = 1;
+ } finally {
+ // pastikan minimal 300ms sebelum loading = false (biar UX smooth)
+ const elapsed = Date.now() - startTime;
+ const minDelay = 300;
+ const delay = elapsed < minDelay ? minDelay - elapsed : 0;
+
+ setTimeout(() => {
+ dataPerpustakaan.findMany.loading = false;
+ }, delay);
+ }
+ },
+ },
+ findManyAll: {
+ data: null as
+ | Prisma.DataPerpustakaanGetPayload<{
+ include: {
+ image: true;
+ kategori: true;
+ };
+ }>[]
+ | null,
+ loading: false,
+ search: "",
+ load: async (search = "", kategori = "") => {
+ dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
+ dataPerpustakaan.findMany.search = search;
+
+ try {
+ const query: any = {};
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
+ "findManyAll"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ dataPerpustakaan.findManyAll.data = res.data.data ?? [];
+ } else {
+ dataPerpustakaan.findManyAll.data = [];
+ }
+ } catch (err) {
+ console.error("Gagal fetch data perpustakaan paginated:", err);
+ dataPerpustakaan.findManyAll.data = [];
+ } finally {
+ dataPerpustakaan.findManyAll.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DataPerpustakaanGetPayload<{
+ include: {
+ kategori: true;
+ image: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/perpustakaandigital/dataperpustakaan/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ dataPerpustakaan.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ dataPerpustakaan.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ dataPerpustakaan.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ dataPerpustakaan.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/dataperpustakaan/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Data Perpustakaan berhasil dihapus");
+ await dataPerpustakaan.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus Data Perpustakaan");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data Perpustakaan");
+ } finally {
+ dataPerpustakaan.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultDataPerpustakaan },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/dataperpustakaan/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ judul: data.judul,
+ deskripsi: data.deskripsi,
+ imageId: data.imageId,
+ kategoriId: data.kategoriId,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading perpustakaan digital:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateDataPerpustakaan.safeParse(
+ dataPerpustakaan.update.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ dataPerpustakaan.update.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/dataperpustakaan/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ judul: this.form.judul,
+ deskripsi: this.form.deskripsi,
+ imageId: this.form.imageId,
+ kategoriId: this.form.kategoriId,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data perpustakaan digital");
+ await dataPerpustakaan.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update data perpustakaan digital"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating data perpustakaan digital:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data perpustakaan digital"
+ );
+ return false;
+ } finally {
+ dataPerpustakaan.update.loading = false;
+ }
+ },
+ reset() {
+ dataPerpustakaan.update.id = "";
+ dataPerpustakaan.update.form = { ...defaultDataPerpustakaan };
+ },
+ },
+});
+
+const templateKategoriBuku = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+});
+
+const defaultKategoriBuku = {
+ name: "",
+};
+
+const kategoriBuku = proxy({
+ create: {
+ form: { ...defaultKategoriBuku },
+ loading: false,
+ async create() {
+ const cek = templateKategoriBuku.safeParse(kategoriBuku.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ kategoriBuku.create.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
+ "create"
+ ].post(kategoriBuku.create.form);
+ if (res.status === 200) {
+ kategoriBuku.findMany.load();
+ return toast.success("Data Kategori Buku Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ kategoriBuku.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.KategoriBukuGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
+ kategoriBuku.findMany.page = page;
+ kategoriBuku.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriBuku.findMany.data = res.data.data ?? [];
+ kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ kategoriBuku.findMany.data = [];
+ kategoriBuku.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch data kategori buku paginated:", err);
+ kategoriBuku.findMany.data = [];
+ kategoriBuku.findMany.totalPages = 1;
+ } finally {
+ kategoriBuku.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.KategoriBukuGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/perpustakaandigital/kategoribuku/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ kategoriBuku.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ kategoriBuku.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ kategoriBuku.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ kategoriBuku.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/kategoribuku/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Data Kategori Buku berhasil dihapus"
+ );
+ await kategoriBuku.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus Data Kategori Buku");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data Kategori Buku");
+ } finally {
+ kategoriBuku.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultKategoriBuku },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/kategoribuku/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading kategori buku:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateKategoriBuku.safeParse(kategoriBuku.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ kategoriBuku.update.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/kategoribuku/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data kategori buku");
+ await kategoriBuku.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update data kategori buku");
+ }
+ } catch (error) {
+ console.error("Error updating data kategori buku:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data kategori buku"
+ );
+ return false;
+ } finally {
+ kategoriBuku.update.loading = false;
+ }
+ },
+ reset() {
+ kategoriBuku.update.id = "";
+ kategoriBuku.update.form = { ...defaultKategoriBuku };
+ },
+ },
+});
+
+const templatePeminjamanBuku = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+ noTelp: z.string().min(1, "No Telp harus diisi"),
+ alamat: z.string().min(1, "Alamat harus diisi"),
+ bukuId: z.string().min(1, "Buku ID harus diisi"),
+ tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
+ batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
+ tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
+ catatan: z.string().min(1, "Catatan harus diisi"),
+});
+
+const defaultPeminjamanBuku = {
+ nama: "",
+ noTelp: "",
+ alamat: "",
+ bukuId: "",
+ tanggalPinjam: "",
+ batasKembali: "",
+ tanggalKembali: "",
+ catatan: "",
+};
+
+interface FormEditData {
+ nama: string;
+ noTelp: string;
+ alamat: string;
+ bukuId: string;
+ buku?: {
+ id: string;
+ judul: string;
+ };
+ tanggalPinjam: string;
+ batasKembali: string;
+ tanggalKembali: string;
+ catatan: string;
+ status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
+}
+
+const editForm: FormEditData = {
+ nama: "",
+ noTelp: "",
+ alamat: "",
+ bukuId: "",
+ tanggalPinjam: "",
+ batasKembali: "",
+ tanggalKembali: "",
+ catatan: "",
+ status: "Dipinjam",
+};
+
+const peminjamanBuku = proxy({
+ create: {
+ form: { ...defaultPeminjamanBuku },
+ loading: false,
+ async create() {
+ const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ peminjamanBuku.create.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
+ "create"
+ ].post(peminjamanBuku.create.form);
+ if (res.status === 200) {
+ peminjamanBuku.findMany.load();
+ return toast.success("Data Peminjaman Buku Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ peminjamanBuku.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.PeminjamanBukuGetPayload<{
+ include: {
+ buku: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
+ peminjamanBuku.findMany.page = page;
+ peminjamanBuku.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ peminjamanBuku.findMany.data = res.data.data ?? [];
+ peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ peminjamanBuku.findMany.data = [];
+ peminjamanBuku.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch data peminjaman buku paginated:", err);
+ peminjamanBuku.findMany.data = [];
+ peminjamanBuku.findMany.totalPages = 1;
+ } finally {
+ peminjamanBuku.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PeminjamanBukuGetPayload<{
+ include: {
+ buku: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ peminjamanBuku.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ peminjamanBuku.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ peminjamanBuku.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ peminjamanBuku.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Data Peminjaman Buku berhasil dihapus"
+ );
+ await peminjamanBuku.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus Data Peminjaman Buku"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data Peminjaman Buku");
+ } finally {
+ peminjamanBuku.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...editForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ noTelp: data.noTelp,
+ alamat: data.alamat,
+ bukuId: data.bukuId,
+ tanggalPinjam: data.tanggalPinjam,
+ batasKembali: data.batasKembali,
+ tanggalKembali: data.tanggalKembali,
+ catatan: data.catatan,
+ status: data.status,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading peminjaman buku:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ peminjamanBuku.update.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ noTelp: this.form.noTelp,
+ alamat: this.form.alamat,
+ bukuId: this.form.bukuId,
+ tanggalPinjam: this.form.tanggalPinjam,
+ batasKembali: this.form.batasKembali,
+ tanggalKembali: this.form.tanggalKembali,
+ catatan: this.form.catatan,
+ status: this.form.status,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data peminjaman buku");
+ await peminjamanBuku.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update data peminjaman buku"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating data peminjaman buku:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data peminjaman buku"
+ );
+ return false;
+ } finally {
+ peminjamanBuku.update.loading = false;
+ }
+ },
+ reset() {
+ peminjamanBuku.update.id = "";
+ peminjamanBuku.update.form = { ...editForm };
+ },
+ },
+});
+
+const perpustakaanDigitalState = proxy({
+ dataPerpustakaan,
+ kategoriBuku,
+ peminjamanBuku,
+});
+
+export default perpustakaanDigitalState;
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/program-pendidikan-anak.ts b/src/app/admin/(dashboard)/_state/pendidikan/program-pendidikan-anak.ts
new file mode 100644
index 00000000..7cc43719
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/pendidikan/program-pendidikan-anak.ts
@@ -0,0 +1,181 @@
+import ApiFetch from "@/lib/api-fetch";
+import { Prisma } from "@prisma/client";
+import { toast } from "react-toastify";
+import { proxy } from "valtio";
+import { z } from "zod";
+
+// ========================================= TUJUAN PROGRAM ========================================= //
+
+const templateTujuanProgramForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type TujuanProgramForm = Prisma.TujuanProgramGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const stateTujuanProgram = proxy({
+ findById: {
+ data: null as TujuanProgramForm | null,
+ loading: false,
+ initialize() {
+ stateTujuanProgram.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as TujuanProgramForm;
+ },
+ async load(id: string) {
+ try {
+ stateTujuanProgram.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.programpendidikananak.tujuanprogram[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateTujuanProgram.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data tujuan program");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data tujuan program");
+ } finally {
+ stateTujuanProgram.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: TujuanProgramForm) {
+ const cek = templateTujuanProgramForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateTujuanProgram.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.programpendidikananak.tujuanprogram[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data tujuan program berhasil diubah");
+ await stateTujuanProgram.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data tujuan program");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data tujuan program");
+ } finally {
+ stateTujuanProgram.update.loading = false;
+ }
+ },
+ },
+});
+
+// ========================================= PROGRAM UNGGULAN ========================================= //
+
+const templateProgramUnggulanForm = z.object({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+});
+
+type ProgramUnggulanForm = Prisma.ProgramUnggulanGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ deskripsi: true;
+ };
+}>;
+
+const programUnggulanState = proxy({
+ findById: {
+ data: null as ProgramUnggulanForm | null,
+ loading: false,
+ initialize() {
+ programUnggulanState.findById.data = {
+ id: "",
+ judul: "",
+ deskripsi: "",
+ } as ProgramUnggulanForm;
+ },
+ async load(id: string) {
+ try {
+ programUnggulanState.findById.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.programpendidikananak.programunggulan[
+ "find-by-id"
+ ].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ programUnggulanState.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data program pendidikan anak");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengambil data program pendidikan anak"
+ );
+ } finally {
+ programUnggulanState.findById.loading = false;
+ }
+ },
+ },
+ update: {
+ loading: false,
+ async save(data: ProgramUnggulanForm) {
+ const cek = templateProgramUnggulanForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ programUnggulanState.update.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.programpendidikananak.programunggulan[
+ "update"
+ ].post(data);
+ if (res.status === 200) {
+ toast.success("Data program pendidikan anak berhasil diubah");
+ await programUnggulanState.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data program pendidikan anak");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error(
+ "Terjadi kesalahan saat mengubah data program pendidikan anak"
+ );
+ } finally {
+ programUnggulanState.update.loading = false;
+ }
+ },
+ },
+});
+
+const stateProgramPendidikanAnak = proxy({
+ stateTujuanProgram,
+ programUnggulanState,
+});
+
+export default stateProgramPendidikanAnak;
diff --git a/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts
new file mode 100644
index 00000000..61f0f23b
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts
@@ -0,0 +1,264 @@
+/* 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 templateDaftarInformasi = z.object({
+ jenisInformasi: z.string().min(3, "Jenis Informasi minimal 3 karakter"),
+ deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
+ tanggal: z.string().min(3, "Tanggal minimal 3 karakter"),
+});
+
+const defaultForm = {
+ jenisInformasi: "",
+ deskripsi: "",
+ tanggal: "",
+};
+
+const daftarInformasiPublik = proxy({
+ create: {
+ form: {...defaultForm},
+ loading: false,
+ async create() {
+ const cek = templateDaftarInformasi.safeParse(
+ daftarInformasiPublik.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ daftarInformasiPublik.create.loading = true;
+ const res = await ApiFetch.api.ppid.daftarinformasipublik[
+ "create"
+ ].post(daftarInformasiPublik.create.form);
+ if (res.status === 200) {
+ daftarInformasiPublik.findMany.load();
+ return toast.success("success create");
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ daftarInformasiPublik.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.DaftarInformasiPublikGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ daftarInformasiPublik.findMany.loading = true; // ✅ Akses langsung via nama path
+ daftarInformasiPublik.findMany.page = page;
+ daftarInformasiPublik.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ daftarInformasiPublik.findMany.data = res.data.data ?? [];
+ daftarInformasiPublik.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ daftarInformasiPublik.findMany.data = [];
+ daftarInformasiPublik.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch daftar informasi publik paginated:", err);
+ daftarInformasiPublik.findMany.data = [];
+ daftarInformasiPublik.findMany.totalPages = 1;
+ } finally {
+ daftarInformasiPublik.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.DaftarInformasiPublikGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ppid/daftarinformasipublik/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ daftarInformasiPublik.findUnique.data = data.data ?? null;
+ } else {
+ console.error(
+ "Failed to fetch daftar informasi publik:",
+ res.statusText
+ );
+ daftarInformasiPublik.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching daftar informasi publik:", error);
+ daftarInformasiPublik.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ daftarInformasiPublik.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ppid/daftarinformasipublik/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Daftar Informasi Publik berhasil dihapus"
+ );
+ await daftarInformasiPublik.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus daftar informasi publik"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus daftar informasi publik");
+ } finally {
+ daftarInformasiPublik.delete.loading = false;
+ }
+ },
+ },
+ edit: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/ppid/daftarinformasipublik/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ jenisInformasi: data.jenisInformasi,
+ deskripsi: data.deskripsi,
+ tanggal: data.tanggal,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal mengambil data");
+ }
+ } catch (error) {
+ console.error("Error loading daftar informasi publik:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async update() {
+ const cek = templateDaftarInformasi.safeParse(
+ daftarInformasiPublik.edit.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ daftarInformasiPublik.edit.loading = true;
+ const formattedTanggal = this.form.tanggal
+ ? new Date(this.form.tanggal).toISOString()
+ : undefined;
+ const response = await fetch(
+ `/api/ppid/daftarinformasipublik/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ jenisInformasi: this.form.jenisInformasi,
+ deskripsi: this.form.deskripsi,
+ tanggal: formattedTanggal,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update daftar informasi publik");
+ await daftarInformasiPublik.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update daftar informasi publik"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating daftar informasi publik:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update daftar informasi publik"
+ );
+ return false;
+ } finally {
+ daftarInformasiPublik.edit.loading = false;
+ }
+ },
+
+ reset() {
+ daftarInformasiPublik.edit.id = "";
+ daftarInformasiPublik.edit.form = { ...defaultForm };
+ },
+ },
+});
+
+export default daftarInformasiPublik;
diff --git a/src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts b/src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts
new file mode 100644
index 00000000..f4223a98
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts
@@ -0,0 +1,82 @@
+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({
+ judul: z.string().min(3, "Judul minimal 3 karakter"),
+ content: z.string().min(3, "Content minimal 3 karakter"),
+});
+
+type DasarHukumForm = Prisma.DasarHukumPPIDGetPayload<{
+ select: {
+ id: true;
+ judul: true;
+ content: true;
+ };
+}>;
+
+const stateDasarHukumPPID = proxy({
+ findById: {
+ data: null as DasarHukumForm | null,
+ loading: false,
+ initialize() {
+ stateDasarHukumPPID.findById.data = {
+ id: '',
+ judul: '',
+ content: '',
+ } as DasarHukumForm;
+ },
+ async load(id: string) {
+ try {
+ stateDasarHukumPPID.findById.loading = true;
+ const res = await ApiFetch.api.ppid.dasarhukumppid["find-by-id"].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateDasarHukumPPID.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data dasar hukum");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data dasar hukum");
+ } finally {
+ stateDasarHukumPPID.findById.loading = false;
+ }
+ },
+ },
+
+ update: {
+ loading: false,
+ async save(data: DasarHukumForm) {
+ const cek = templateForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateDasarHukumPPID.update.loading = true;
+ const res = await ApiFetch.api.ppid.dasarhukumppid["update"].post(data);
+ if (res.status === 200) {
+ toast.success("Data dasar hukum berhasil diubah");
+ await stateDasarHukumPPID.findById.load(data.id);
+ } else {
+ toast.error("Gagal mengubah data dasar hukum");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengubah data dasar hukum");
+ } finally {
+ stateDasarHukumPPID.update.loading = false;
+ }
+ },
+ },
+});
+
+export default stateDasarHukumPPID;
diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin.ts
new file mode 100644
index 00000000..0e302bc6
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin.ts
@@ -0,0 +1,212 @@
+/* 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 templateGrafikJenisKelamin = z.object({
+ laki: z.string().min(1, "Data laki-laki harus diisi"),
+ perempuan: z.string().min(1, "Data perempuan harus diisi"),
+});
+
+const defaultForm = {
+ laki: "",
+ perempuan: "",
+};
+
+const grafikBerdasarkanJenisKelamin = proxy({
+ create: {
+ form: { ...defaultForm },
+ loading: false,
+ async create() {
+ const cek = templateGrafikJenisKelamin.safeParse(
+ grafikBerdasarkanJenisKelamin.create.form
+ );
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+
+ try {
+ grafikBerdasarkanJenisKelamin.create.loading = true;
+ const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
+ "create"
+ ].post(grafikBerdasarkanJenisKelamin.create.form);
+ if (res.status === 200) {
+ toast.success(
+ "Grafik berdasarkan jenis kelamin berhasil ditambahkan"
+ );
+ await grafikBerdasarkanJenisKelamin.findMany.load();
+ } else {
+ toast.error(
+ res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error(
+ "Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin"
+ );
+ } finally {
+ grafikBerdasarkanJenisKelamin.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => {
+ // Change to arrow function
+ grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
+ grafikBerdasarkanJenisKelamin.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
+ "find-many"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
+ grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
+ grafikBerdasarkanJenisKelamin.findMany.totalPages =
+ res.data.totalPages || 1;
+ } else {
+ console.error(
+ "Failed to load grafik berdasarkan jenis kelamin:",
+ res.data?.message
+ );
+ grafikBerdasarkanJenisKelamin.findMany.data = [];
+ grafikBerdasarkanJenisKelamin.findMany.total = 0;
+ grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan jenis kelamin:", error);
+ grafikBerdasarkanJenisKelamin.findMany.data = [];
+ grafikBerdasarkanJenisKelamin.findMany.total = 0;
+ grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
+ } finally {
+ grafikBerdasarkanJenisKelamin.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ppid/grafikberdasarkanjeniskelamin/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ grafikBerdasarkanJenisKelamin.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikBerdasarkanJenisKelamin.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan jenis kelamin:", error);
+ grafikBerdasarkanJenisKelamin.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateGrafikJenisKelamin.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(
+ `/api/ppid/grafikberdasarkanjeniskelamin/${id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ }
+ );
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await grafikBerdasarkanJenisKelamin.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik berdasarkan jenis kelamin");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikBerdasarkanJenisKelamin.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message ||
+ "Grafik berdasarkan jenis kelamin berhasil dihapus"
+ );
+ await grafikBerdasarkanJenisKelamin.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message ||
+ "Gagal menghapus grafik berdasarkan jenis kelamin"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error(
+ "Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin"
+ );
+ } finally {
+ grafikBerdasarkanJenisKelamin.delete.loading = false;
+ }
+ },
+ },
+});
+
+export default grafikBerdasarkanJenisKelamin;
diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts
new file mode 100644
index 00000000..f8caf999
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden.ts
@@ -0,0 +1,199 @@
+/* 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 templateGrafikResponden = z.object({
+ sangatbaik: z.string().min(1, "Data sangat baik harus diisi"),
+ baik: z.string().min(1, "Data baik harus diisi"),
+ kurangbaik: z.string().min(1, "Data kurang baik harus diisi"),
+ tidakbaik: z.string().min(1, "Data tidak baik harus diisi"),
+});
+
+const defaultForm = {
+ sangatbaik: "",
+ baik: "",
+ kurangbaik: "",
+ tidakbaik: "",
+};
+
+const grafikBerdasarkanResponden = proxy({
+ create: {
+ form: {...defaultForm},
+ loading: false,
+ async create() {
+ const cek = templateGrafikResponden.safeParse(
+ grafikBerdasarkanResponden.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ grafikBerdasarkanResponden.create.loading = true;
+ const res = await ApiFetch.api.ppid.grafikberdasarkanresponden[
+ "create"
+ ].post(grafikBerdasarkanResponden.create.form);
+ if (res.status === 200) {
+ toast.success("Grafik berdasarkan responden berhasil ditambahkan");
+ await grafikBerdasarkanResponden.findMany.load();
+ } else {
+ toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan responden");
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan responden");
+ } finally {
+ grafikBerdasarkanResponden.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => { // Change to arrow function
+ grafikBerdasarkanResponden.findMany.loading = true; // Use the full path to access the property
+ grafikBerdasarkanResponden.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.ppid.grafikberdasarkanresponden[
+ "find-many"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikBerdasarkanResponden.findMany.data = res.data.data || [];
+ grafikBerdasarkanResponden.findMany.total = res.data.total || 0;
+ grafikBerdasarkanResponden.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load grafik berdasarkan responden:", res.data?.message);
+ grafikBerdasarkanResponden.findMany.data = [];
+ grafikBerdasarkanResponden.findMany.total = 0;
+ grafikBerdasarkanResponden.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading grafikBerdasarkanResponden:", error);
+ grafikBerdasarkanResponden.findMany.data = [];
+ grafikBerdasarkanResponden.findMany.total = 0;
+ grafikBerdasarkanResponden.findMany.totalPages = 1;
+ } finally {
+ grafikBerdasarkanResponden.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikBerdasarkanRespondenGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ppid/grafikberdasarkanresponden/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ grafikBerdasarkanResponden.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikBerdasarkanResponden.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan responden:", error);
+ grafikBerdasarkanResponden.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: {...defaultForm},
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ const cek = templateGrafikResponden.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ this.loading = true;
+
+ try {
+ const response = await fetch(`/api/ppid/grafikberdasarkanresponden/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ const result = await response.json();
+
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+
+ await grafikBerdasarkanResponden.findMany.load();
+
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik berdasarkan responden");
+ } finally {
+ this.loading = false;
+ }
+ }
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikBerdasarkanResponden.delete.loading = true;
+
+ const response = await fetch(`/api/ppid/grafikberdasarkanresponden/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Grafik berdasarkan responden berhasil dihapus");
+ await grafikBerdasarkanResponden.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus grafik berdasarkan responden");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan responden");
+ } finally {
+ grafikBerdasarkanResponden.delete.loading = false;
+ }
+ }
+ }
+});
+
+export default grafikBerdasarkanResponden;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts
new file mode 100644
index 00000000..4eb184ad
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts
@@ -0,0 +1,197 @@
+/* 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 templateGrafikUmur = z.object({
+ remaja: z.string().min(1, "Data remaja harus diisi"),
+ dewasa: z.string().min(1, "Data dewasa harus diisi"),
+ orangtua: z.string().min(1, "Data orangtua harus diisi"),
+ lansia: z.string().min(1, "Data lansia harus diisi"),
+});
+
+const defaultForm = {
+ remaja: "",
+ dewasa: "",
+ orangtua: "",
+ lansia: "",
+};
+
+const grafikBerdasarkanUmur = proxy({
+ create: {
+ form: {...defaultForm},
+ loading: false,
+ async create() {
+ const cek = templateGrafikUmur.safeParse(
+ grafikBerdasarkanUmur.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ grafikBerdasarkanUmur.create.loading = true;
+ const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
+ "create"
+ ].post(grafikBerdasarkanUmur.create.form);
+ if (res.status === 200) {
+ const id = res.data?.data?.id;
+ if (id) {
+ toast.success("Success create");
+ grafikBerdasarkanUmur.create.form = {
+ remaja: "",
+ dewasa: "",
+ orangtua: "",
+ lansia: "",
+ };
+ grafikBerdasarkanUmur.findMany.load();
+ return id;
+ }
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ grafikBerdasarkanUmur.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => { // Change to arrow function
+ grafikBerdasarkanUmur.findMany.loading = true; // Use the full path to access the property
+ grafikBerdasarkanUmur.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
+ "find-many"
+ ].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikBerdasarkanUmur.findMany.data = res.data.data || [];
+ grafikBerdasarkanUmur.findMany.total = res.data.total || 0;
+ grafikBerdasarkanUmur.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load grafik berdasarkan umur:", res.data?.message);
+ grafikBerdasarkanUmur.findMany.data = [];
+ grafikBerdasarkanUmur.findMany.total = 0;
+ grafikBerdasarkanUmur.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan umur:", error);
+ grafikBerdasarkanUmur.findMany.data = [];
+ grafikBerdasarkanUmur.findMany.total = 0;
+ grafikBerdasarkanUmur.findMany.totalPages = 1;
+ } finally {
+ grafikBerdasarkanUmur.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.GrafikBerdasarkanUmurGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ppid/grafikberdasarkanumur/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ grafikBerdasarkanUmur.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikBerdasarkanUmur.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik berdasarkan umur:", error);
+ grafikBerdasarkanUmur.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ const cek = templateGrafikUmur.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+ this.loading = true;
+ try {
+ const response = await fetch(`/api/ppid/grafikberdasarkanumur/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+ const result = await response.json();
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+ toast.success("Berhasil update data!");
+ await grafikBerdasarkanUmur.findMany.load();
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik berdasarkan umur");
+ } finally {
+ this.loading = false;
+ }
+ }
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikBerdasarkanUmur.delete.loading = true;
+
+ const response = await fetch(`/api/ppid/grafikberdasarkanumur/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Grafik berdasarkan umur berhasil dihapus");
+ await grafikBerdasarkanUmur.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus grafik berdasarkan umur");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan umur");
+ } finally {
+ grafikBerdasarkanUmur.delete.loading = false;
+ }
+ }
+ }
+});
+
+export default grafikBerdasarkanUmur;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan.ts
new file mode 100644
index 00000000..2b669f43
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan.ts
@@ -0,0 +1,193 @@
+/* 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 templateGrafikHasilKepuasanMasyarakat = z.object({
+ label: z.string().min(2, "Label harus diisi"),
+ kepuasan: z.string().min(1, "Kepuasan harus diisi"),
+});
+
+const defaultForm = {
+ label: "",
+ kepuasan: "",
+};
+
+const grafikHasilKepuasanMasyarakat = proxy({
+ create: {
+ form: {...defaultForm},
+ loading: false,
+ async create() {
+ const cek = templateGrafikHasilKepuasanMasyarakat.safeParse(
+ grafikHasilKepuasanMasyarakat.create.form
+ );
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ grafikHasilKepuasanMasyarakat.create.loading = true;
+ const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["create"].post(grafikHasilKepuasanMasyarakat.create.form);
+ if (res.status === 200) {
+ toast.success("Grafik hasil kepuasan masyarakat berhasil ditambahkan");
+ await grafikHasilKepuasanMasyarakat.findMany.load();
+ } else {
+ toast.error(res.data?.message ?? "Gagal tambah grafik hasil kepuasan masyarakat");
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error("Terjadi kesalahan saat menambahkan grafik hasil kepuasan masyarakat");
+ } finally {
+ grafikHasilKepuasanMasyarakat.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as any[] | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ load: async (page = 1, limit = 10) => { // Change to arrow function
+ grafikHasilKepuasanMasyarakat.findMany.loading = true; // Use the full path to access the property
+ grafikHasilKepuasanMasyarakat.findMany.page = page;
+ try {
+ const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["find-many"].get({
+ query: { page, limit },
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ grafikHasilKepuasanMasyarakat.findMany.data = res.data.data || [];
+ grafikHasilKepuasanMasyarakat.findMany.total = res.data.total || 0;
+ grafikHasilKepuasanMasyarakat.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load grafik hasil kepuasan masyarakat:", res.data?.message);
+ grafikHasilKepuasanMasyarakat.findMany.data = [];
+ grafikHasilKepuasanMasyarakat.findMany.total = 0;
+ grafikHasilKepuasanMasyarakat.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading grafik hasil kepuasan masyarakat:", error);
+ grafikHasilKepuasanMasyarakat.findMany.data = [];
+ grafikHasilKepuasanMasyarakat.findMany.total = 0;
+ grafikHasilKepuasanMasyarakat.findMany.totalPages = 1;
+ } finally {
+ grafikHasilKepuasanMasyarakat.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.IndeksKepuasanMasyarakatGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ppid/grafikhasilkepuasamanmasyarakat/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ grafikHasilKepuasanMasyarakat.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ grafikHasilKepuasanMasyarakat.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error loading grafik hasil kepuasan masyarakat:", error);
+ grafikHasilKepuasanMasyarakat.findUnique.data = null;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ async byId() {
+ // Method implementation if needed
+ },
+ async submit() {
+ const id = this.id;
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ // ✅ Validasi pakai Zod
+ const cek = templateGrafikHasilKepuasanMasyarakat.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
+ toast.error(err);
+ return null;
+ }
+
+ this.loading = true;
+
+ try {
+ const response = await fetch(`/api/ppid/grafikhasilkepuasamanmasyarakat/${id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ const result = await response.json();
+
+ if (!response.ok || !result?.success) {
+ throw new Error(result?.message || "Gagal update data");
+ }
+
+ toast.success("Berhasil update data!");
+
+ // ✅ Optional: refresh list kalau kamu langsung ke halaman list
+ await grafikHasilKepuasanMasyarakat.findMany.load();
+
+ return result.data;
+ } catch (error) {
+ console.error("Error update data:", error);
+ toast.error("Gagal update data grafik hasil kepuasan masyarakat");
+ } finally {
+ this.loading = false;
+ }
+ }
+
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ grafikHasilKepuasanMasyarakat.delete.loading = true;
+
+ const response = await fetch(`/api/ppid/grafikhasilkepuasamanmasyarakat/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Grafik hasil kepuasan masyarakat berhasil dihapus");
+ await grafikHasilKepuasanMasyarakat.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus grafik hasil kepuasan masyarakat");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus grafik hasil kepuasan masyarakat");
+ } finally {
+ grafikHasilKepuasanMasyarakat.delete.loading = false;
+ }
+ },
+ }
+});
+
+export default grafikHasilKepuasanMasyarakat;
diff --git a/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts
new file mode 100644
index 00000000..56b734d9
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts
@@ -0,0 +1,150 @@
+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({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ nik: z.string().min(3, "NIK minimal 3 karakter"),
+ notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
+ alamat: z.string().min(3, "Alamat minimal 3 karakter"),
+ email: z.string().min(3, "Email minimal 3 karakter"),
+ jenisInformasiDimintaId: z.string().nonempty(),
+ caraMemperolehInformasiId: z.string().nonempty(),
+ caraMemperolehSalinanInformasiId: z.string().nonempty(),
+})
+
+const jenisInformasiDiminta = proxy({
+ findMany: {
+ data: null as
+ | null
+ | Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
+ async load(){
+ const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
+ if (res.status === 200) {
+ jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
+ }
+ }
+ }
+})
+
+const caraMemperolehInformasi = proxy({
+ findMany: {
+ data: null as
+ | null
+ | Prisma.CaraMemperolehInformasiGetPayload<{ omit: { isActive: true } }>[],
+ async load() {
+ const res = await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi["find-many"].get();
+ if (res.status === 200) {
+ caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
+ }
+ }
+ }
+})
+
+const caraMemperolehSalinanInformasi = proxy({
+ findMany: {
+ data: null as
+ | null
+ | Prisma.CaraMemperolehSalinanInformasiGetPayload<{ omit: { isActive: true } }>[],
+ async load() {
+ const res = await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi["find-many"].get();
+ if (res.status === 200) {
+ caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
+ }
+ }
+ }
+})
+console.log(caraMemperolehSalinanInformasi)
+
+type PermohonanInformasiPublikForm = Prisma.PermohonanInformasiPublikGetPayload<{
+ select: {
+ name: true;
+ nik: true;
+ notelp: true;
+ alamat: true;
+ email: true;
+ jenisInformasiDimintaId: true;
+ caraMemperolehInformasiId: true;
+ caraMemperolehSalinanInformasiId: true;
+ };
+}>;
+
+const statepermohonanInformasiPublik = proxy({
+ create: {
+ form: {} as PermohonanInformasiPublikForm,
+ loading: false,
+ async create(){
+ const cek = templateForm.safeParse(statepermohonanInformasiPublik.create.form);
+ if(!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ statepermohonanInformasiPublik.create.loading = true;
+ const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
+ if (res.status === 200) {
+ statepermohonanInformasiPublik.findMany.load();
+ return toast.success("success create");
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ statepermohonanInformasiPublik.create.loading = false;
+ }
+ }
+ },
+ findMany: {
+ data: null as
+ | Prisma.PermohonanInformasiPublikGetPayload<{ include: {
+ caraMemperolehSalinanInformasi: true,
+ jenisInformasiDiminta: true,
+ caraMemperolehInformasi: true,
+ } }>[]
+ | null,
+ async load() {
+ const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get();
+ if (res.status === 200) {
+ statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
+ }
+ }
+ },
+ findUnique: {
+ data: null as Prisma.PermohonanInformasiPublikGetPayload<{
+ include: {
+ jenisInformasiDiminta: true,
+ caraMemperolehInformasi: true,
+ caraMemperolehSalinanInformasi: true,
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch program inovasi:", res.statusText);
+ statepermohonanInformasiPublik.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching program inovasi:", error);
+ statepermohonanInformasiPublik.findUnique.data = null;
+ }
+ },
+ },
+
+})
+
+const statepermohonanInformasiPublikForm = proxy({
+ statepermohonanInformasiPublik,
+ jenisInformasiDiminta,
+ caraMemperolehInformasi,
+ caraMemperolehSalinanInformasi,
+})
+
+export default statepermohonanInformasiPublikForm;
diff --git a/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts
new file mode 100644
index 00000000..92373c3e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts
@@ -0,0 +1,86 @@
+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({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ email: z.string().min(3, "Email minimal 3 karakter"),
+ notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
+ alasan: z.string().min(3, "Alasan minimal 3 karakter"),
+})
+
+type PermohonanKeberatanInformasiForm = Prisma.FormulirPermohonanKeberatanGetPayload<{
+ select: {
+ name: true;
+ email: true;
+ notelp: true;
+ alasan: true;
+ };
+}>;
+
+const permohonanKeberatanInformasi = proxy({
+ create: {
+ form: {} as PermohonanKeberatanInformasiForm,
+ loading: false,
+ async create(){
+ const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
+ if(!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+ try {
+ permohonanKeberatanInformasi.create.loading = true;
+ const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
+ if (res.status === 200) {
+ permohonanKeberatanInformasi.findMany.load();
+ return toast.success("success create");
+ }
+ return toast.error("failed create");
+ } catch (error) {
+ console.log((error as Error).message);
+ } finally {
+ permohonanKeberatanInformasi.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: null as
+ | Prisma.FormulirPermohonanKeberatanGetPayload<{omit: {isActive: true}}>[]
+ | null,
+ async load() {
+ const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get();
+ if (res.status === 200) {
+ permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
+ }
+ }
+ },
+ findUnique: {
+ data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
+ permohonanKeberatanInformasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching permohonan keberatan informasi:", error);
+ permohonanKeberatanInformasi.findUnique.data = null;
+ }
+ },
+ }
+});
+
+export default permohonanKeberatanInformasi;
+
diff --git a/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts b/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts
new file mode 100644
index 00000000..0a6304b8
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts
@@ -0,0 +1,201 @@
+import { Prisma } from "@prisma/client";
+import { toast } from "react-toastify";
+import { proxy } from "valtio";
+import { z } from "zod";
+
+/**
+ * Schema validasi form ProfilePPID menggunakan Zod.
+ */
+const templateForm = z.object({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ biodata: z.string().min(3, "Biodata minimal 3 karakter"),
+ riwayat: z.string().min(3, "Riwayat minimal 3 karakter"),
+ pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"),
+ unggulan: z.string().min(3, "Unggulan minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+});
+
+const defaultForm = {
+ name: "",
+ biodata: "",
+ riwayat: "",
+ pengalaman: "",
+ unggulan: "",
+ imageId: "",
+};
+
+type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ biodata: true;
+ riwayat: true;
+ pengalaman: true;
+ unggulan: true;
+ imageId: true;
+ image?: {
+ select: {
+ link: true;
+ };
+ };
+ };
+}>;
+
+/**
+ * Improved State Management - Consolidated and more robust
+ */
+const stateProfilePPID = proxy({
+ // Consolidated data management
+ profile: {
+ data: null as ProfilePPIDForm | null,
+ loading: false,
+ error: null as string | null,
+
+ // Single method to load profile data
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ppid/profileppid/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(result.message || "Gagal mengambil data profile");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Load profile error:", errorMessage);
+ toast.error("Terjadi kesalahan saat mengambil data profile");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ // Reset profile data
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ }
+ },
+
+ // Edit form management
+ editForm: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false, // Flag untuk data yang tidak bisa diedit
+
+ // Initialize form with profile data
+ initialize(profileData: ProfilePPIDForm) {
+ this.id = profileData.id;
+ this.isReadOnly = false; // Semua data bisa diedit
+ this.form = {
+ name: profileData.name || "",
+ biodata: profileData.biodata || "",
+ riwayat: profileData.riwayat || "",
+ pengalaman: profileData.pengalaman || "",
+ unggulan: profileData.unggulan || "",
+ imageId: profileData.imageId || "",
+ };
+ },
+
+ // Update form field
+ updateField(field: keyof typeof defaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ // Submit form
+ async submit() {
+ // Validate form
+ const validation = templateForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ppid/profileppid/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update profile");
+ // Refresh profile data
+ await stateProfilePPID.profile.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update profile");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update profile error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update profile");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ // Reset form
+ reset() {
+ this.id = "";
+ this.form = { ...defaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ }
+ },
+
+ // Helper methods
+ async loadForEdit(id: string) {
+ const profileData = await this.profile.load(id);
+ if (profileData) {
+ this.editForm.initialize(profileData);
+ }
+ return profileData;
+ },
+
+ reset() {
+ this.profile.reset();
+ this.editForm.reset();
+ }
+});
+
+export default stateProfilePPID;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts b/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts
new file mode 100644
index 00000000..cc326aed
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts
@@ -0,0 +1,809 @@
+/* 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({
+ name: z.string().min(3, "Nama minimal 3 karakter"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+});
+
+const defaultForm = {
+ name: "",
+ imageId: "",
+};
+
+type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ imageId: true;
+ image?: {
+ select: {
+ link: true;
+ };
+ };
+ };
+}>;
+
+const stateStruktur = proxy({
+ struktur: {
+ data: null as StrukturPPIDForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ppid/strukturppid/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ this.data = result.data;
+ return result.data;
+ } else {
+ throw new Error(result.message || "Gagal mengambil data struktur");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Load struktur error:", errorMessage);
+ toast.error("Terjadi kesalahan saat mengambil data struktur");
+ return null;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.data = null;
+ this.error = null;
+ this.loading = false;
+ },
+ },
+
+ editStruktur: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(strukturData: StrukturPPIDForm) {
+ this.id = strukturData.id;
+ this.isReadOnly = false;
+ this.form = {
+ name: strukturData.name || "",
+ imageId: strukturData.imageId || "",
+ };
+ },
+
+ updateField(field: keyof typeof defaultForm, value: string) {
+ this.form[field] = value;
+ },
+
+ async submit() {
+ const validation = templateForm.safeParse(this.form);
+
+ if (!validation.success) {
+ const errors = validation.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return false;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ppid/strukturppid/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update struktur");
+ await stateStruktur.struktur.load(this.id);
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update struktur");
+ }
+ } catch (error) {
+ const errorMessage = (error as Error).message;
+ this.error = errorMessage;
+ console.error("Update struktur error:", errorMessage);
+ toast.error("Terjadi kesalahan saat update struktur");
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+
+ reset() {
+ this.id = "";
+ this.form = { ...defaultForm };
+ this.error = null;
+ this.loading = false;
+ this.isReadOnly = false;
+ },
+ },
+
+ async loadForEdit(id: string) {
+ const strukturData = await this.struktur.load(id);
+ if (strukturData) {
+ this.editStruktur.initialize(strukturData);
+ }
+ return strukturData;
+ },
+
+ reset() {
+ this.struktur.reset();
+ this.editStruktur.reset();
+ },
+});
+
+const templatePosisiOrganisasi = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+ deskripsi: z.string().optional(),
+ hierarki: z.number().int().positive("Hierarki harus angka positif"),
+});
+
+const posisiOrganisasiDefaultForm = {
+ nama: "",
+ deskripsi: "",
+ hierarki: 0,
+};
+
+const posisiOrganisasi = proxy({
+ create: {
+ form: { ...posisiOrganisasiDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templatePosisiOrganisasi.safeParse(this.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((v) => v.message).join("\n");
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
+ "create"
+ ].post(this.form);
+ if (res.status === 200) {
+ toast.success("Berhasil menambahkan posisi organisasi");
+ posisiOrganisasi.findMany.load();
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal menambahkan posisi");
+ }
+ } catch (error) {
+ console.error("Create error:", error);
+ toast.error("Terjadi kesalahan saat menambahkan posisi");
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.form = { ...posisiOrganisasiDefaultForm };
+ },
+ },
+
+ findUnique: {
+ data: null as Prisma.StrukturOrganisasiPPIDGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ppid/strukturppid/posisiorganisasi/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ posisiOrganisasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch posisiOrganisasi:", res.statusText);
+ posisiOrganisasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching posisiOrganisasi:", error);
+ posisiOrganisasi.findUnique.data = null;
+ }
+ },
+ },
+
+ edit: {
+ id: "",
+ form: { ...posisiOrganisasiDefaultForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+ try {
+ const response = await fetch(
+ `/api/ppid/strukturppid/posisiorganisasi/${id}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const result = await response.json();
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ nama: data.nama,
+ deskripsi: data.deskripsi,
+ hierarki: data.hierarki,
+ };
+ return data;
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading posisi organisasi:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templatePosisiOrganisasi.safeParse(this.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ this.loading = true;
+ const response = await fetch(
+ `/api/ppid/strukturppid/posisiorganisasi/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ deskripsi: this.form.deskripsi,
+ hierarki: this.form.hierarki,
+ }),
+ }
+ );
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ const result = await response.json();
+ if (result.success) {
+ toast.success("Berhasil update posisi organisasi");
+ await posisiOrganisasi.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal mengupdate posisi organisasi"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating posisi organisasi:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Gagal mengupdate posisi organisasi"
+ );
+ return false;
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.id = "";
+ this.form = { ...posisiOrganisasiDefaultForm };
+ },
+ },
+
+ findMany: {
+ data: [] as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string | null;
+ hierarki: number;
+ }>,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit?: number, search = "") => {
+ const appliedLimit = limit ?? 10;
+ posisiOrganisasi.findMany.page = page;
+ posisiOrganisasi.findMany.search = search;
+
+ try {
+ const query: any = { page, limit: appliedLimit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
+ "find-many"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ posisiOrganisasi.findMany.data = res.data.data ?? [];
+ posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ posisiOrganisasi.findMany.data = [];
+ posisiOrganisasi.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch posisi organisasi paginated:", err);
+ posisiOrganisasi.findMany.data = [];
+ posisiOrganisasi.findMany.totalPages = 1;
+ } finally {
+ posisiOrganisasi.findMany.loading = false;
+ }
+ },
+ },
+ findManyAll: {
+ data: [] as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string | null;
+ hierarki: number;
+ }>,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ // Change to arrow function
+ posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
+ posisiOrganisasi.findManyAll.search = search;
+ try {
+ const query: any = { search };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
+ "find-many-all"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ posisiOrganisasi.findManyAll.data = res.data.data || [];
+
+ } else {
+ console.error("Failed to load posisiOrganisasi:", res.data?.message);
+ posisiOrganisasi.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading posisiOrganisasi:", error);
+ posisiOrganisasi.findManyAll.data = [];
+ } finally {
+ posisiOrganisasi.findManyAll.loading = false;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ posisiOrganisasi.delete.loading = true;
+
+ const response = await fetch(
+ `/api/ppid/strukturppid/posisiorganisasi/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Posisi organisasi berhasil dihapus");
+ await posisiOrganisasi.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus posisi organisasi");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
+ } finally {
+ posisiOrganisasi.delete.loading = false;
+ }
+ },
+ },
+});
+
+const templatePegawai = z.object({
+ namaLengkap: z.string().min(1, "Nama wajib diisi"),
+ gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"),
+ imageId: z.string().min(1, "Gambar wajib dipilih"),
+ tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
+ email: z.string().email("Email tidak valid").optional(),
+ telepon: z.string().min(1, "Telepom wajib diisi"),
+ alamat: z.string().min(1, "Alamat wajib diisi"),
+ posisiId: z.string().min(1, "Posisi wajib diisi"),
+ isActive: z.boolean().default(true),
+});
+
+const pegawaiDefaultForm = {
+ namaLengkap: "",
+ gelarAkademik: "",
+ imageId: "",
+ tanggalMasuk: "",
+ email: "",
+ telepon: "",
+ alamat: "",
+ posisiId: "",
+ isActive: true,
+};
+
+const pegawai = proxy({
+ create: {
+ form: { ...pegawaiDefaultForm },
+ loading: false,
+ async submit() {
+ const cek = templatePegawai.safeParse(pegawai.create.form);
+ if (!cek.success) {
+ const err = cek.error.issues.map((i) => i.message).join("\n");
+ toast.error(err);
+ return;
+ }
+
+ try {
+ pegawai.create.loading = true;
+ const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(
+ pegawai.create.form
+ );
+ if (res.status === 200) {
+ toast.success("Pegawai berhasil ditambahkan");
+ await pegawai.findMany.load();
+ } else {
+ toast.error(res.data?.message ?? "Gagal tambah pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal create:", error);
+ toast.error("Terjadi kesalahan saat menambahkan pegawai");
+ } finally {
+ pegawai.create.loading = false;
+ }
+ },
+ },
+
+ // In struktur-organisasi.ts
+ findMany: {
+ data: null as
+ | Prisma.PegawaiPPIDGetPayload<{
+ include: {
+ image: true;
+ posisi: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ pegawai.findMany.loading = true; // Use the full path to access the property
+ pegawai.findMany.page = page;
+ pegawai.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ppid.strukturppid.pegawai[
+ "find-many"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pegawai.findMany.data = res.data.data || [];
+ pegawai.findMany.total = res.data.total || 0;
+ pegawai.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ pegawai.findMany.data = [];
+ pegawai.findMany.total = 0;
+ pegawai.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
+ pegawai.findMany.data = [];
+ pegawai.findMany.total = 0;
+ pegawai.findMany.totalPages = 1;
+ } finally {
+ pegawai.findMany.loading = false;
+ }
+ },
+ },
+ findManyAll: {
+ data: null as
+ | Prisma.PegawaiPPIDGetPayload<{
+ include: {
+ image: true;
+ posisi: true;
+ };
+ }>[]
+ | null,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ // Change to arrow function
+ pegawai.findManyAll.loading = true; // Use the full path to access the property
+ pegawai.findManyAll.search = search;
+ try {
+ const query: any = { search };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ppid.strukturppid.pegawai[
+ "find-many-all"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pegawai.findManyAll.data = res.data.data || [];
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ pegawai.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
+ pegawai.findManyAll.data = [];
+ } finally {
+ pegawai.findManyAll.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as
+ | (Prisma.PegawaiPPIDGetPayload<{
+ include: { posisi: true; image: true };
+ }> & { isActive: boolean })
+ | null,
+ async load(id: string) {
+ const res = await fetch(`/api/ppid/strukturppid/pegawai/${id}`);
+ if (res.ok) {
+ const json = await res.json();
+ pegawai.findUnique.data = json.data
+ ? {
+ ...json.data,
+ isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada data
+ }
+ : null;
+ } else {
+ pegawai.findUnique.data = null;
+ }
+ },
+ },
+
+ delete: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pegawai.delete.loading = true;
+ const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, {
+ method: "DELETE",
+ });
+ const json = await res.json();
+ if (res.ok) {
+ toast.success(json.message ?? "Berhasil hapus pegawai");
+ await pegawai.findMany.load();
+ } else {
+ toast.error(json.message ?? "Gagal hapus pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus");
+ } finally {
+ pegawai.delete.loading = false;
+ }
+ },
+ },
+
+ nonActive: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pegawai.nonActive.loading = true;
+ const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
+ method: "DELETE", // biasanya nonActive pakai PATCH
+ });
+ const json = await res.json();
+ if (res.ok) {
+ toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
+ await pegawai.findMany.load(); // refresh data
+ } else {
+ toast.error(json.message ?? "Gagal menonaktifkan pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal nonActive:", error);
+ toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
+ } finally {
+ pegawai.nonActive.loading = false;
+ }
+ },
+ },
+
+ edit: {
+ id: "",
+ form: { ...pegawaiDefaultForm },
+ loading: false,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/ppid/strukturppid/pegawai/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ namaLengkap: data.namaLengkap,
+ gelarAkademik: data.gelarAkademik,
+ imageId: data.imageId,
+ tanggalMasuk: data.tanggalMasuk,
+ email: data.email,
+ telepon: data.telepon,
+ alamat: data.alamat,
+ posisiId: data.posisiId,
+ isActive: data.isActive,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading berita:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+
+ async submit() {
+ const cek = templatePegawai.safeParse(pegawai.edit.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ pegawai.edit.loading = true;
+
+ // Format tanggalMasuk to ISO string if it exists
+ const formattedTanggalMasuk = this.form.tanggalMasuk
+ ? new Date(this.form.tanggalMasuk).toISOString()
+ : undefined;
+
+ const response = await fetch(
+ `/api/ppid/strukturppid/pegawai/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ id: this.id,
+ namaLengkap: this.form.namaLengkap,
+ gelarAkademik: this.form.gelarAkademik,
+ imageId: this.form.imageId || null,
+ tanggalMasuk: formattedTanggalMasuk,
+ email: this.form.email,
+ telepon: this.form.telepon,
+ alamat: this.form.alamat,
+ posisiId: this.form.posisiId,
+ isActive: this.form.isActive,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update pegawai");
+ await pegawai.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update pegawai");
+ }
+ } catch (error) {
+ console.error("Error updating pegawai:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update pegawai"
+ );
+ return false;
+ } finally {
+ pegawai.edit.loading = false;
+ }
+ },
+
+ reset() {
+ pegawai.edit.id = "";
+ pegawai.edit.form = { ...pegawaiDefaultForm };
+ },
+ },
+});
+
+const stateStrukturPPID = proxy({
+ stateStruktur,
+ posisiOrganisasi,
+ pegawai,
+});
+
+export default stateStrukturPPID;
diff --git a/src/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID.ts b/src/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID.ts
new file mode 100644
index 00000000..86e90bc7
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID.ts
@@ -0,0 +1,81 @@
+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({
+ misi: z.string().min(3, "Misi minimal 3 karakter"),
+ visi: z.string().min(3, "Visi minimal 3 karakter"),
+});
+
+type VisiMisiPPIDForm = Prisma.VisiMisiPPIDGetPayload<{
+ select: {
+ id: true;
+ misi: true;
+ visi: true;
+ };
+}>;
+
+const stateVisiMisiPPID = proxy({
+ findById: {
+ data: null as VisiMisiPPIDForm | null,
+ loading: false,
+ initialize() {
+ stateVisiMisiPPID.findById.data = {
+ id: "",
+ misi: "",
+ visi: "",
+ } as VisiMisiPPIDForm;
+ },
+ async load(id: string) {
+ try {
+ stateVisiMisiPPID.findById.loading = true;
+ const res = await ApiFetch.api.ppid.visimisippid["find-by-id"].get({
+ query: { id },
+ });
+ if (res.status === 200) {
+ stateVisiMisiPPID.findById.data = res.data?.data ?? null;
+ } else {
+ toast.error("Gagal mengambil data visi misi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat mengambil data visi misi");
+ } finally {
+ stateVisiMisiPPID.findById.loading = false;
+ }
+ },
+ },
+ update: {
+ loading: false,
+ async save(data: VisiMisiPPIDForm) {
+ const cek = templateForm.safeParse(data);
+ if (!cek.success) {
+ const errors = cek.error.issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+ toast.error(`Form tidak valid: ${errors}`);
+ return;
+ }
+
+ try {
+ stateVisiMisiPPID.update.loading = true;
+ const res = await ApiFetch.api.ppid.visimisippid["update"].post(data);
+ if (res.status === 200) {
+ toast.success("Berhasil update visi misi");
+ await stateVisiMisiPPID.findById.load(data.id);
+ } else {
+ toast.error("Gagal update visi misi");
+ }
+ } catch (error) {
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat update visi misi");
+ } finally {
+ stateVisiMisiPPID.update.loading = false;
+ }
+ },
+ },
+});
+
+export default stateVisiMisiPPID;
diff --git a/src/app/admin/(dashboard)/_state/state-file-storage.ts b/src/app/admin/(dashboard)/_state/state-file-storage.ts
new file mode 100644
index 00000000..98c0677e
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/state-file-storage.ts
@@ -0,0 +1,63 @@
+import ApiFetch from "@/lib/api-fetch";
+import { proxy } from "valtio";
+
+interface FileItem {
+ id: string;
+ name: string;
+ path: string;
+ link: string;
+ mimeType: string;
+ category: string;
+ realName: string;
+ isActive: boolean;
+ createdAt: string | Date;
+ updatedAt: string | Date;
+ deletedAt: string | Date | null;
+}
+
+const stateFileStorage = proxy<{
+ list: FileItem[] | null;
+ page: number;
+ limit: number;
+ total: number | undefined;
+ load: (params?: { search?: string }) => Promise;
+ del: (params: { id: string }) => Promise;
+}>({
+ list: null,
+ page: 1,
+ limit: 10,
+ total: undefined,
+ async load(params?: { search?: string }) {
+ const { search = "" } = params ?? {};
+ try {
+ const { data } = await ApiFetch.api.fileStorage.findMany.get({
+ query: {
+ page: this.page,
+ limit: this.limit,
+ search,
+ category: 'image'
+ },
+ });
+
+ if (data?.data) {
+ this.list = data.data as FileItem[];
+ this.total = data.meta?.totalPages;
+ }
+ } catch (error) {
+ console.error('Error loading files:', error);
+ this.list = [];
+ this.total = 0;
+ }
+ },
+ async del({ id }: { id: string }) {
+ try {
+ await ApiFetch.api.fileStorage.delete({ id });
+ await this.load();
+ } catch (error) {
+ console.error('Error deleting file:', error);
+ throw error;
+ }
+ },
+});
+
+export default stateFileStorage;
diff --git a/src/app/admin/(dashboard)/_state/user/user-state.ts b/src/app/admin/(dashboard)/_state/user/user-state.ts
new file mode 100644
index 00000000..93594956
--- /dev/null
+++ b/src/app/admin/(dashboard)/_state/user/user-state.ts
@@ -0,0 +1,337 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { proxy } from "valtio";
+import { toast } from "react-toastify";
+import ApiFetch from "@/lib/api-fetch";
+import { Prisma } from "@prisma/client";
+import { z } from "zod";
+
+// State Valtio
+const userState = proxy({
+ // Find Many
+ findMany: {
+ data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ userState.findMany.loading = true; // ✅ Akses langsung via nama path
+ userState.findMany.page = page;
+ userState.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.user["findMany"].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ userState.findMany.data = res.data.data ?? [];
+ userState.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ userState.findMany.data = [];
+ userState.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch user paginated:", err);
+ userState.findMany.data = [];
+ userState.findMany.totalPages = 1;
+ } finally {
+ userState.findMany.loading = false;
+ }
+ },
+ },
+
+ // Find Unique
+ findUnique: {
+ data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
+ loading: false,
+ async load(id: string) {
+ this.loading = true;
+ try {
+ const res = await fetch(`/api/user/findUnique/${id}`);
+ const data = await res.json();
+ if (res.status === 200) {
+ this.data = data.data;
+ } else {
+ toast.error(data.message);
+ }
+ } catch (e) {
+ console.error(e);
+ toast.error("Gagal ambil data user");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+
+ // Delete (Soft Delete)
+ delete: {
+ loading: false,
+ async submit(id: string) {
+ this.loading = true;
+ try {
+ const res = await fetch(`/api/user/del/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ });
+ const data = await res.json();
+ if (res.status === 200) {
+ toast.success("User dinonaktifkan");
+ userState.findMany.load();
+ } else {
+ toast.error(data.message || "Gagal hapus");
+ }
+ } catch (e) {
+ console.error(e);
+ toast.error("Gagal hapus user");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ updateActive: {
+ loading: false,
+ async submit(id: string, isActive: boolean) {
+ this.loading = true;
+ try {
+ const res = await fetch(`/api/user/updt`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ id, isActive }),
+ });
+
+ const data = await res.json();
+ if (res.status === 200 && data.success) {
+ toast.success(data.message);
+ userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
+ } else {
+ toast.error(data.message || "Gagal update status user");
+ }
+ } catch (e) {
+ console.error(e);
+ toast.error("Gagal update status user");
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+});
+
+const templateRole = z.object({
+ name: z.string().min(1, "Nama harus diisi"),
+ permissions: z.array(z.string()).min(1, "Permission harus diisi"),
+});
+
+const defaultRole = {
+ name: "",
+ permissions: [] as string[],
+};
+
+const roleState = proxy({
+ create: {
+ form: { ...defaultRole },
+ loading: false,
+ async create() {
+ const cek = templateRole.safeParse(roleState.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ roleState.create.loading = true;
+ const res = await ApiFetch.api.role["create"].post(
+ roleState.create.form
+ );
+ if (res.status === 200) {
+ roleState.findMany.load();
+ return toast.success("Data role Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ roleState.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.RoleGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }>[],
+ loading: false,
+ async load() {
+ const res = await ApiFetch.api.role["findMany"].get();
+ if (res.status === 200) {
+ roleState.findMany.data = res.data?.data ?? [];
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.RoleGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/role/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ roleState.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ roleState.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ roleState.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ roleState.delete.loading = true;
+
+ const response = await fetch(`/api/role/del/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "Data role berhasil dihapus");
+ await roleState.findMany.load(); // refresh list
+ } else {
+ toast.error(result?.message || "Gagal menghapus Data role");
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data role");
+ } finally {
+ roleState.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...defaultRole },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(`/api/role/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+
+ if (result?.success) {
+ const data = result.data;
+ this.id = data.id;
+ this.form = {
+ name: data.name,
+ permissions: data.permissions,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading role:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templateRole.safeParse(roleState.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ roleState.update.loading = true;
+
+ const response = await fetch(`/api/role/${this.id}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.form.name,
+ permissions: this.form.permissions,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success("Berhasil update data role");
+ await roleState.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(result.message || "Gagal update data role");
+ }
+ } catch (error) {
+ console.error("Error updating data role:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data role"
+ );
+ return false;
+ } finally {
+ roleState.update.loading = false;
+ }
+ },
+ reset() {
+ roleState.update.id = "";
+ roleState.update.form = { ...defaultRole };
+ },
+ },
+});
+
+const user = proxy({
+ userState,
+ roleState,
+});
+
+export default user;
diff --git a/src/app/admin/(dashboard)/auth/login-admin/page.tsx b/src/app/admin/(dashboard)/auth/login-admin/page.tsx
new file mode 100644
index 00000000..ab591207
--- /dev/null
+++ b/src/app/admin/(dashboard)/auth/login-admin/page.tsx
@@ -0,0 +1,111 @@
+'use client'
+import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
+import colors from '@/con/colors';
+import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { PhoneInput } from "react-international-phone";
+import "react-international-phone/style.css";
+import { toast } from 'react-toastify';
+
+
+
+function Login() {
+ const router = useRouter()
+ const [phone, setPhone] = useState("")
+ const [isError, setError] = useState(false)
+ const [loading, setLoading] = useState(false)
+
+ async function onLogin() {
+ const nomor = phone.substring(1);
+ if (nomor.length <= 4) return setError(true)
+
+
+ try {
+ setLoading(true);
+ const response = await apiFetchLogin({ nomor: nomor })
+ if (response && response.success) {
+ localStorage.setItem("hipmi_auth_code_id", response.kodeId);
+ toast.success(response.message);
+ router.push("/validasi", { scroll: false });
+ } else {
+ setLoading(false);
+ toast.error(response?.message);
+ }
+ } catch (error) {
+ setLoading(false)
+ console.log("Error Login", error)
+ toast.error("Terjadi kesalahan saat login")
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ Login
+
+
+
+
+
+
+ {/*
+ Masuk Untuk Akses Admin
+ setUsername(e.target.value)}
+ required
+ />
+ */}
+ {
+ setPhone(val);
+ }}
+ />
+
+ {isError ? (
+ toast.error("Masukan nomor telepon anda")
+ ) : (
+ ""
+ )}
+
+ Masuk
+
+
+
+ Belum punya akun?
+
+ Registrasi
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Login;
diff --git a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
new file mode 100644
index 00000000..62d2554b
--- /dev/null
+++ b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
@@ -0,0 +1,121 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+'use client'
+import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
+import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
+import colors from '@/con/colors';
+import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { PhoneInput } from "react-international-phone";
+import "react-international-phone/style.css";
+import { toast } from 'react-toastify';
+
+function Registrasi() {
+ const [phone, setPhone] = useState("")
+ const router = useRouter()
+ const [value, setValue] = useState("")
+ const [isValue, setIsValue] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ async function onRegistarsi() {
+ if (value.length < 5) {
+ toast.error("Username minimal 5 karakter!");
+ return;
+ }
+
+ if (value.includes(" ")) {
+ toast.error("Username tidak boleh ada spasi!");
+ return;
+ }
+
+ if (!phone) {
+ toast.error("Nomor telepon wajib diisi!");
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const respone = await apiFetchRegister({ nomor: phone, username: value });
+
+ if (respone.success) {
+ router.push("/login", { scroll: false });
+ toast.success(respone.message);
+
+ } else {
+ setLoading(false);
+ toast.error(respone.message);
+ }
+ } catch (error) {
+ setLoading(false);
+ console.log("Error Registrasi", error);
+ }
+ }
+ return (
+
+
+
+
+
+
+
+
+
+ Registrasi
+
+
+
+
+
+ 0 && value.length < 5
+ ? "Minimal 5 karakter !"
+ : value.includes(" ")
+ ? "Tidak boleh ada spasi"
+ : isValue
+ ? "Masukan username anda"
+ : ""
+ }
+ onChange={(val) => {
+ val.currentTarget.value.length > 0 ? setIsValue(false) : "";
+ setValue(val.currentTarget.value);
+ }}
+ required
+
+ />
+
+ Nomor Telepon
+ {
+ setPhone(val);
+ }}
+ />
+
+
+
+
+
+ Daftar
+
+
+
+
+
+
+
+ );
+}
+
+export default Registrasi;
diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
new file mode 100644
index 00000000..862edb33
--- /dev/null
+++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
@@ -0,0 +1,38 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
+import { useRouter } from 'next/navigation';
+
+function Validasi() {
+ const router = useRouter()
+ return (
+
+
+
+
+
+
+
+ Kode Verifikasi
+
+
+
+
+ Masukkan Kode Verifikasi
+
+
+
+ router.push("/admin/landing-page/profile/program-inovasi")}>
+ Page
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Validasi;
diff --git a/src/app/admin/(dashboard)/desa/_com/desaEditor.tsx b/src/app/admin/(dashboard)/desa/_com/desaEditor.tsx
new file mode 100644
index 00000000..de60355a
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/_com/desaEditor.tsx
@@ -0,0 +1,93 @@
+'use client'
+import colors from '@/con/colors';
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+const content =
+ 'Welcome to Mantine rich text editor RichTextEditor component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. RichTextEditor is based on Tiptap.dev and supports all of its features:
General text formatting: bold , italic , underline , strike-through Headings (h1-h6) Sub and super scripts (<sup /> and <sub /> tags) Ordered and bullet lists Text align And all other extensions ';
+
+export function DesaEditor({showSubmit = true} : {
+ showSubmit: boolean
+}) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+
+ Submit
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/_com/desaEditorText.tsx b/src/app/admin/(dashboard)/desa/_com/desaEditorText.tsx
new file mode 100644
index 00000000..3452d0a2
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/_com/desaEditorText.tsx
@@ -0,0 +1,95 @@
+'use client'
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+
+function DesaEditorText({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
+ onSubmit?: (val: string) => void,
+ onChange: (val: string) => void,
+ showSubmit?: boolean,
+ initialContent?: string }) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content: initialContent,
+ onUpdate : ({editor}) => {
+ onChange(editor.getHTML())
+ }
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+ {
+ if (!editor) return
+ onSubmit?.(editor?.getHTML())
+ }}>Submit
+ )}
+
+ );
+}
+
+export default DesaEditorText;
diff --git a/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx b/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx
new file mode 100644
index 00000000..4ec40e47
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx
@@ -0,0 +1,137 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
+
+function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const tabs = [
+ {
+ label: "Pelayanan Surat Keterangan",
+ value: "pelayanansuratketerangan",
+ href: "/admin/desa/layanan/pelayanan_surat_keterangan",
+ icon: ,
+ tooltip: "Layanan terkait surat keterangan resmi desa"
+ },
+ {
+ label: "Pelayanan Perizinan Berusaha",
+ value: "pelayananperizinanusaha",
+ href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
+ icon: ,
+ tooltip: "Layanan untuk izin usaha masyarakat"
+ },
+ {
+ label: "Pelayanan Telunjuk Sakti Desa",
+ value: "pelayanantelunjuksaktidesa",
+ href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
+ icon: ,
+ tooltip: "Layanan inovasi khusus desa"
+ },
+ {
+ label: "Pelayanan Penduduk Non-Permanent",
+ value: "pelayanannonpermanent",
+ href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
+ icon: ,
+ tooltip: "Pendataan penduduk non-permanent"
+ },
+ {
+ label: "Ajukan Permohonan",
+ value: "ajukanpermohonan",
+ href: "/admin/desa/layanan/ajukan_permohonan",
+ icon: ,
+ tooltip: "Ajukan permohonan"
+ }
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname)
+ const [activeTab, setActiveTab] = useState(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 (
+
+ Layanan
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsLayanan;
diff --git a/src/app/admin/(dashboard)/desa/berita/_com/BeritaEditor.tsx b/src/app/admin/(dashboard)/desa/berita/_com/BeritaEditor.tsx
new file mode 100644
index 00000000..3361c2bc
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/_com/BeritaEditor.tsx
@@ -0,0 +1,143 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import { RichTextEditor, Link } from '@mantine/tiptap';
+import { useEditor } from '@tiptap/react';
+import Highlight from '@tiptap/extension-highlight';
+import StarterKit from '@tiptap/starter-kit';
+import Underline from '@tiptap/extension-underline';
+import TextAlign from '@tiptap/extension-text-align';
+import Superscript from '@tiptap/extension-superscript';
+import SubScript from '@tiptap/extension-subscript';
+import { Button, Stack } from '@mantine/core';
+import { useEffect, useState } from 'react';
+
+// const content =
+// 'Welcome to Mantine rich text editor RichTextEditor component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. RichTextEditor is based on Tiptap.dev and supports all of its features:
General text formatting: bold , italic , underline , strike-through Headings (h1-h6) Sub and super scripts (<sup /> and <sub /> tags) Ordered and bullet lists Text align And all other extensions ';
+
+ export function BeritaEditor({
+ onEditorReady,
+ showSubmit = true,
+ onSubmit,
+ initialContent = '',
+ onUpdate,
+ }: {
+ onEditorReady?: (editor: any | null) => void;
+ onSubmit?: (val: string) => void;
+ showSubmit?: boolean;
+ initialContent?: string;
+ onUpdate?: (content: string) => void;
+ }) {
+ const [mounted, setMounted] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ content: initialContent || '
',
+ onUpdate: ({ editor }) => {
+ if (onUpdate) {
+ onUpdate(editor.getHTML());
+ }
+ },
+ editorProps: {
+ attributes: {
+ class: 'prose max-w-none',
+ },
+ },
+ onSelectionUpdate: () => {
+ if (!isReady && editor) {
+ setIsReady(true);
+ onEditorReady?.(editor);
+ }
+ },
+ immediatelyRender: false
+ });
+
+ useEffect(() => {
+ if (editor) {
+ // Set initial content when component mounts
+ editor.commands.setContent(initialContent || '
');
+
+ // Mark as mounted and notify parent
+ if (!mounted) {
+ setMounted(true);
+ onEditorReady?.(editor);
+ }
+ }
+
+ return () => {
+ if (editor) {
+ editor.destroy();
+ }
+ };
+ }, [editor, initialContent, mounted, onEditorReady]);
+
+ if (!editor) return Loading editor...
;
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+ {
+ if (!editor) return
+ onSubmit?.(editor?.getHTML())
+ }}>Submit
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx
new file mode 100644
index 00000000..d19825c5
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx
@@ -0,0 +1,117 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconNews, IconCategory } from '@tabler/icons-react';
+
+function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "List Berita",
+ value: "list_berita",
+ href: "/admin/desa/berita/list-berita",
+ icon: ,
+ tooltip: "Lihat dan kelola semua berita desa"
+ },
+ {
+ label: "Kategori Berita",
+ value: "kategori_berita",
+ href: "/admin/desa/berita/kategori-berita",
+ icon: ,
+ tooltip: "Kelola kategori berita desa"
+ },
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+ Berita Desa
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsBerita;
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
new file mode 100644
index 00000000..4ffdd64a
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
@@ -0,0 +1,134 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKategoriBerita() {
+ const editState = useProxy(stateDashboardBerita.kategoriBerita);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ });
+
+ useEffect(() => {
+ const loadKategori = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await editState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading kategori Berita:', error);
+ toast.error('Gagal memuat data kategori Berita');
+ }
+ };
+
+ loadKategori();
+ }, [params?.id]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // update global state hanya saat submit
+ editState.update.form = {
+ ...editState.update.form,
+ name: formData.name,
+ };
+
+ await editState.update.update();
+ toast.success('Kategori Berita berhasil diperbarui!');
+ router.push('/admin/desa/berita/kategori-berita');
+ } catch (error) {
+ console.error('Error updating kategori Berita:', error);
+ toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
+ }
+ };
+
+ return (
+
+ {/* Back Button + Title */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Kategori Berita
+
+
+
+ {/* Form Wrapper */}
+
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKategoriBerita;
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
new file mode 100644
index 00000000..db9e2b6a
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
@@ -0,0 +1,91 @@
+'use client';
+import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreateKategoriBerita() {
+ const createState = useProxy(stateDashboardBerita.kategoriBerita);
+ const router = useRouter();
+
+ const resetForm = () => {
+ createState.create.form = {
+ name: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await createState.create.create();
+ resetForm();
+ router.push('/admin/desa/berita/kategori-berita');
+ };
+
+ return (
+
+ {/* Header dengan back button */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kategori Berita
+
+
+
+ {/* Form utama */}
+
+
+ (createState.create.form.name = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKategoriBerita;
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
new file mode 100644
index 00000000..94dc8326
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
@@ -0,0 +1,199 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import stateDashboardBerita from '../../../_state/desa/berita';
+
+function KategoriBerita() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKategoriBerita({ search }: { search: string }) {
+ const listDataState = useProxy(stateDashboardBerita.kategoriBerita);
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const {
+ data,
+ loading,
+ load,
+ page,
+ totalPages,
+ } = listDataState.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ listDataState.delete.delete(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ load(page, 10, search);
+ }
+ };
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Kategori Berita
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/desa/berita/kategori-berita/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ No
+ Nama
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item, index) => (
+
+
+ {index + 1}
+
+
+
+ {item.name}
+
+
+
+
+
+ router.push(
+ `/admin/desa/berita/kategori-berita/${item.id}`
+ )
+ }
+ >
+
+
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data kategori berita yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus kategori berita ini?"
+ />
+
+ );
+}
+
+export default KategoriBerita;
diff --git a/src/app/admin/(dashboard)/desa/berita/layout.tsx b/src/app/admin/(dashboard)/desa/berita/layout.tsx
new file mode 100644
index 00000000..4c8e4901
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/layout.tsx
@@ -0,0 +1,13 @@
+'use client'
+import React from 'react';
+import LayoutTabsBerita from './_com/layoutTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
new file mode 100644
index 00000000..c34049cc
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
@@ -0,0 +1,279 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
+import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
+import colors from "@/con/colors";
+import ApiFetch from "@/lib/api-fetch";
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { Dropzone } from "@mantine/dropzone";
+import {
+ IconArrowBack,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+function EditBerita() {
+ const beritaState = useProxy(stateDashboardBerita);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ judul: "",
+ deskripsi: "",
+ kategoriBeritaId: "",
+ content: "",
+ imageId: "",
+ });
+
+ // Load kategori + berita
+ useEffect(() => {
+ beritaState.kategoriBerita.findMany.load();
+
+ const loadBerita = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateDashboardBerita.berita.edit.load(id);
+ if (data) {
+ setFormData({
+ judul: data.judul || "",
+ deskripsi: data.deskripsi || "",
+ kategoriBeritaId: data.kategoriBeritaId || "",
+ content: data.content || "",
+ imageId: data.imageId || "",
+ });
+
+ if (data?.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (error) {
+ console.error("Error loading berita:", error);
+ toast.error("Gagal memuat data berita");
+ }
+ };
+
+ loadBerita();
+ }, [params?.id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state hanya sekali di sini
+ beritaState.berita.edit.form = {
+ ...beritaState.berita.edit.form,
+ ...formData,
+ };
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+
+ beritaState.berita.edit.form.imageId = uploaded.id;
+ }
+
+ await beritaState.berita.edit.update();
+ toast.success("Berita berhasil diperbarui!");
+ router.push("/admin/desa/berita/list-berita");
+ } catch (error) {
+ console.error("Error updating berita:", error);
+ toast.error("Terjadi kesalahan saat memperbarui berita");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Berita
+
+
+
+ {/* Form */}
+
+
+ handleChange("judul", e.target.value)}
+ required
+ />
+
+ handleChange("kategoriBeritaId", val || "")}
+ label="Kategori"
+ placeholder="Pilih kategori"
+ data={
+ beritaState.kategoriBerita.findMany.data?.map((v) => ({
+ value: v.id,
+ label: v.name,
+ })) || []
+ }
+ clearable
+ searchable
+ required
+ error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
+ />
+
+
+
+ Deskripsi Singkat
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar Berita
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() =>
+ toast.error("File tidak valid, gunakan format gambar")
+ }
+ maxSize={5 * 1024 ** 2}
+ accept={{ "image/*": [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Konten */}
+
+
+ Konten
+
+
+ setFormData((prev) => ({ ...prev, content: htmlContent }))
+ }
+ />
+
+
+ {/* Action */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditBerita;
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx
new file mode 100644
index 00000000..2030f188
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx
@@ -0,0 +1,157 @@
+'use client'
+import { useProxy } from 'valtio/utils';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+import colors from '@/con/colors';
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
+
+function DetailBerita() {
+ const beritaState = useProxy(stateDashboardBerita);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ beritaState.berita.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ beritaState.berita.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/desa/berita/list-berita");
+ }
+ };
+
+ if (!beritaState.berita.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = beritaState.berita.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Detail Berita */}
+
+
+
+ Detail Berita
+
+
+
+
+
+ Kategori
+ {data.kategoriBerita?.name || '-'}
+
+
+
+ Judul
+ {data.judul || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+ Konten
+
+
+
+ {/* Action Button */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus berita ini?"
+ />
+
+ );
+}
+
+export default DetailBerita;
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
new file mode 100644
index 00000000..ab176266
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
@@ -0,0 +1,227 @@
+'use client'
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+export default function CreateBerita() {
+ const beritaState = useProxy(stateDashboardBerita);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ beritaState.kategoriBerita.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ beritaState.berita.create.form = {
+ judul: '',
+ deskripsi: '',
+ kategoriBeritaId: '',
+ imageId: '',
+ content: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ beritaState.berita.create.form.imageId = uploaded.id;
+
+ await beritaState.berita.create.create();
+
+ resetForm();
+ router.push('/admin/desa/berita/list-berita');
+ };
+
+ return (
+
+ {/* Header dengan tombol kembali */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Berita
+
+
+
+
+
+ (beritaState.berita.create.form.judul = e.target.value)}
+ required
+ />
+
+ ({
+ label: item.name,
+ value: item.id,
+ }))}
+ defaultValue={beritaState.berita.create.form.kategoriBeritaId || null}
+ onChange={(val: string | null) => {
+ if (val) {
+ const selected = beritaState.kategoriBerita.findMany.data?.find(
+ (item) => item.id === val
+ );
+ if (selected) {
+ beritaState.berita.create.form.kategoriBeritaId = selected.id;
+ }
+ } else {
+ beritaState.berita.create.form.kategoriBeritaId = '';
+ }
+ }}
+ searchable
+ clearable
+ nothingFoundMessage="Tidak ditemukan"
+ required
+ />
+
+
+
+ Deskripsi Singkat
+
+ {
+ beritaState.berita.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+
+
+ Gambar Berita
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Konten
+
+ {
+ beritaState.berita.create.form.content = htmlContent;
+ }}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
new file mode 100644
index 00000000..47b628d8
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
@@ -0,0 +1,156 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import stateDashboardBerita from '../../../_state/desa/berita';
+
+function Berita() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListBerita({ search }: { search: string }) {
+ const beritaState = useProxy(stateDashboardBerita);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ const filteredData = data || [];
+
+ return (
+
+
+
+ Daftar Berita
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/berita/list-berita/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Judul
+ Kategori
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.judul}
+
+
+
+
+
+ {item.kategoriBerita?.name || '-'}
+
+
+
+
+ router.push(`/admin/desa/berita/list-berita/${item.id}`)
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data berita yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default Berita;
diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx
new file mode 100644
index 00000000..3b08fbea
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx
@@ -0,0 +1,160 @@
+"use client";
+import stateFileStorage from "@/state/state-list-image";
+import {
+ ActionIcon,
+ Box,
+ Card,
+ Flex,
+ Group,
+ Image,
+ Pagination,
+ Paper,
+ SimpleGrid,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { useShallowEffect } from "@mantine/hooks";
+import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
+import { motion } from "framer-motion";
+import toast from "react-simple-toasts";
+import { useSnapshot } from "valtio";
+
+export default function ListImage() {
+ const { list, total } = useSnapshot(stateFileStorage);
+
+ useShallowEffect(() => {
+ stateFileStorage.load();
+ }, []);
+
+ let timeOut: NodeJS.Timer;
+
+ return (
+
+
+
+ Galeri Foto
+
+ }
+ rightSection={
+ stateFileStorage.load()}
+ >
+
+
+ }
+ onChange={(e) => {
+ if (timeOut) clearTimeout(timeOut);
+ timeOut = setTimeout(() => {
+ stateFileStorage.load({ search: e.target.value });
+ }, 300);
+ }}
+ />
+
+
+
+ {list && list.length > 0 ? (
+
+ {list.map((v, k) => (
+
+
+ {
+ navigator.clipboard.writeText(v.url);
+ toast("Tautan foto berhasil disalin");
+ }}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ style={{ cursor: "pointer" }}
+ >
+
+
+
+
+
+ {v.name}
+
+
+
+
+
+ {
+ stateFileStorage
+ .del({ id: v.id })
+ .finally(() => toast("Foto berhasil dihapus"));
+ }}
+ >
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+ Belum ada foto yang tersedia
+
+
+ )}
+
+
+ {total && total > 1 && (
+
+ {
+ stateFileStorage.load({ page });
+ }}
+ />
+
+
+ )}
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/desa/gallery/layout.tsx b/src/app/admin/(dashboard)/desa/gallery/layout.tsx
new file mode 100644
index 00000000..fbaf56c0
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/layout.tsx
@@ -0,0 +1,10 @@
+'use client'
+import LayoutTabsGallery from "./lib/layoutTabs"
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx
new file mode 100644
index 00000000..803a6884
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx
@@ -0,0 +1,115 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconPhoto, IconVideo } from '@tabler/icons-react';
+
+function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const tabs = [
+ {
+ label: "Foto",
+ value: "foto",
+ href: "/admin/desa/gallery/foto",
+ icon: ,
+ tooltip: "Kelola foto-foto galeri desa"
+ },
+ {
+ label: "Video",
+ value: "video",
+ href: "/admin/desa/gallery/video",
+ icon: ,
+ tooltip: "Kelola video galeri desa"
+ },
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname)
+ const [activeTab, setActiveTab] = useState(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 (
+
+ Gallery
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsGallery;
diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts b/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts
new file mode 100644
index 00000000..afbca78b
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts
@@ -0,0 +1,11 @@
+// export function convertYoutubeUrlToEmbed(url: string) {
+// const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
+// return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
+// }
+
+export function convertYoutubeUrlToEmbed(url: string) {
+ const videoIdMatch = url.match(
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
+ );
+ return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/youtubeEmbed.tsx b/src/app/admin/(dashboard)/desa/gallery/lib/youtubeEmbed.tsx
new file mode 100644
index 00000000..fcf4d266
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/lib/youtubeEmbed.tsx
@@ -0,0 +1,33 @@
+// components/YoutubeEmbed.tsx
+"use client";
+
+import { Box, Text } from "@mantine/core";
+
+type YoutubeEmbedProps = {
+ url?: string;
+ showRawUrl?: boolean; // opsional, buat nampilin URL mentahnya
+};
+
+export default function YoutubeEmbed({ url, showRawUrl = false }: YoutubeEmbedProps) {
+ if (!url || !url.includes("embed")) {
+ return Link embed Youtube tidak valid ;
+ }
+
+ return (
+
+
+ {showRawUrl && (
+
+ {url}
+
+ )}
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
new file mode 100644
index 00000000..1b4260fd
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
@@ -0,0 +1,176 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, useCallback } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
+
+function EditVideo() {
+ const router = useRouter();
+ const videoState = useProxy(stateGallery.video);
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ linkVideo: '',
+ });
+
+ // load data video sekali saat id ada
+ useEffect(() => {
+ const loadVideo = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await videoState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name ?? '',
+ deskripsi: data.deskripsi ?? '',
+ linkVideo: data.linkVideo ?? '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading video:', error);
+ toast.error('Gagal memuat data video');
+ }
+ };
+
+ loadVideo();
+ }, [params?.id]);
+
+ const handleChange = useCallback(
+ (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ const handleSubmit = async () => {
+ const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
+ if (!converted) {
+ toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
+ return;
+ }
+
+ try {
+ videoState.update.form = {
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ linkVideo: formData.linkVideo,
+ };
+ await videoState.update.update();
+ toast.success('Video berhasil diperbarui!');
+ router.push('/admin/desa/gallery/video');
+ } catch (error) {
+ console.error('Error updating video:', error);
+ toast.error('Terjadi kesalahan saat memperbarui video');
+ }
+ };
+
+ const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Video
+
+
+
+
+
+ handleChange('name', e.currentTarget.value)}
+ required
+ />
+
+
+ handleChange('linkVideo', e.currentTarget.value)}
+ required
+ />
+ {embedLink && (
+
+
+
+ )}
+
+
+
+
+ Deskripsi Video
+
+ handleChange('deskripsi', val)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditVideo;
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx
new file mode 100644
index 00000000..65a579a0
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx
@@ -0,0 +1,171 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailVideo() {
+ const videoState = useProxy(stateGallery.video);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ videoState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ videoState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/desa/gallery/video");
+ }
+ };
+
+ if (!videoState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = videoState.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Detail Video */}
+
+
+
+ Detail Video
+
+
+
+
+
+ Judul
+ {data?.name || '-'}
+
+
+
+ Video
+ {data?.linkVideo ? (
+
+ ) : (
+ Tidak ada video
+ )}
+
+
+
+ Tanggal Video
+
+ {data?.createdAt ? new Date(data.createdAt).toDateString() : '-'}
+
+
+
+
+ Deskripsi
+ {data?.deskripsi ? (
+
+ ) : (
+ Tidak ada deskripsi
+ )}
+
+
+ {/* Tombol Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/desa/gallery/video/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus video ini?"
+ />
+
+ );
+
+ function convertToEmbedUrl(youtubeUrl: string): string {
+ try {
+ const url = new URL(youtubeUrl);
+ const videoId = url.searchParams.get("v");
+ if (!videoId) return youtubeUrl;
+ return `https://www.youtube.com/embed/${videoId}`;
+ } catch (err) {
+ console.error("Error converting YouTube URL to embed:", err);
+ return youtubeUrl;
+ }
+ }
+}
+
+export default DetailVideo;
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
new file mode 100644
index 00000000..9badd230
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
@@ -0,0 +1,150 @@
+'use client';
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
+
+function CreateVideo() {
+ const videoState = useProxy(stateGallery.video);
+ const router = useRouter();
+ const [link, setLink] = useState('');
+ const embedLink = convertYoutubeUrlToEmbed(link);
+
+ const resetForm = () => {
+ videoState.create.form = {
+ name: '',
+ deskripsi: '',
+ linkVideo: '',
+ };
+ setLink('');
+ };
+
+ const handleSubmit = async () => {
+ if (!embedLink) {
+ toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
+ return;
+ }
+
+ videoState.create.form.linkVideo = embedLink;
+ await videoState.create.create();
+ resetForm();
+ router.push('/admin/desa/gallery/video');
+ };
+
+ return (
+
+ {/* Header Back Button + Title */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Video
+
+
+
+ {/* Card Form */}
+
+
+ {/* Judul */}
+ {
+ videoState.create.form.name = e.currentTarget.value;
+ }}
+ required
+ />
+
+ {/* Link YouTube */}
+ setLink(e.currentTarget.value)}
+ required
+ />
+
+ {/* Preview Video */}
+ {embedLink && (
+
+
+
+ )}
+
+ {/* Deskripsi */}
+
+
+ Deskripsi Video
+
+ {
+ videoState.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Button Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateVideo;
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx
new file mode 100644
index 00000000..232204ff
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx
@@ -0,0 +1,166 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 stateGallery from '../../../_state/desa/gallery';
+
+function Video() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListVideo({ search }: { search: string }) {
+ const videoState = useProxy(stateGallery.video)
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = videoState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ Daftar Video
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/gallery/video/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Judul Video
+ Tanggal
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+
+
+
+ {new Date(item.createdAt).toLocaleDateString('id-ID', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ })}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/desa/gallery/video/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada video yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10)
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default Video;
diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx
new file mode 100644
index 00000000..bd56c1c3
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx
@@ -0,0 +1,178 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Select,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditAjukanPermohonan() {
+ const router = useRouter();
+ const params = useParams();
+ const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
+
+ // State lokal form
+ const [formData, setFormData] = useState({
+ nama: '',
+ nik: '',
+ alamat: '',
+ nomorKk: '',
+ kategoriId: '',
+ });
+
+ // Load data awal
+ useEffect(() => {
+ stateLayananDesa.suratKeterangan.findManyAll.load();
+
+ const loadAjukan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateAjukan.edit.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || '',
+ nik: data.nik || '',
+ alamat: data.alamat || '',
+ nomorKk: data.nomorKk || '',
+ kategoriId: data.kategoriId || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading ajukan:', error);
+ toast.error('Gagal memuat data ajukan');
+ }
+ };
+
+ loadAjukan();
+ }, [params?.id]);
+
+ // Handler untuk input controlled
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ stateAjukan.edit.form = {
+ ...stateAjukan.edit.form,
+ ...formData,
+ };
+ toast.success('Ajukan berhasil diperbarui!');
+ router.push('/admin/desa/layanan/ajukan_permohonan');
+ } catch (error) {
+ console.error('Error updating ajukan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui ajukan');
+ }
+ };
+
+ return (
+
+ {/* Back Button */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Ajukan Permohonan
+
+
+
+
+
+ handleChange('nama', e.target.value)}
+ required
+ />
+
+ handleChange('nik', e.target.value)}
+ required
+ />
+
+ handleChange('alamat', e.target.value)}
+ required
+ />
+
+ handleChange('nomorKk', e.target.value)}
+ required
+ />
+
+ ({
+ label: item.name,
+ value: item.id,
+ }))}
+ value={formData.kategoriId || null}
+ onChange={(val) => handleChange('kategoriId', val || '')}
+ searchable
+ clearable
+ nothingFoundMessage="Tidak ditemukan"
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditAjukanPermohonan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx
new file mode 100644
index 00000000..7d10ba08
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx
@@ -0,0 +1,172 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailAjukanPermohonan() {
+ const ajukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ ajukanPermohonanState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ ajukanPermohonanState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/desa/layanan/ajukan_permohonan');
+ }
+ };
+
+ if (!ajukanPermohonanState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = ajukanPermohonanState.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Surat Keterangan
+
+
+
+
+
+
+ Nama
+
+
+ {data?.nama || '-'}
+
+
+
+
+
+ NIK
+
+
+ {data?.nik || '-'}
+
+
+
+
+
+ Alamat
+
+
+ {data?.alamat || '-'}
+
+
+
+
+
+ Nomor KK
+
+
+ {data?.nomorKk || '-'}
+
+
+
+
+
+ Kategori
+
+
+ {data?.kategori.name || '-'}
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={ajukanPermohonanState.delete.loading}
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/desa/layanan/ajukan_permohonan/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus ajukan permohonan ini?"
+ />
+
+ );
+}
+
+export default DetailAjukanPermohonan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/page.tsx
new file mode 100644
index 00000000..b13d2a44
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/page.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title
+} from '@mantine/core';
+import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import stateLayananDesa from '../../../_state/desa/layananDesa';
+
+function AjukanPermohonan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListAjukanPermohonan({ search }: { search: string }) {
+ const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = AjukanPermohonanState.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ // Loading state
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ List Ajukan Permohonan
+
+
+
+
+ Nama
+ Alamat
+ NIK
+ Aksi
+
+
+
+ {data.length > 0 ? (
+ data.map((item) => (
+
+
+
+
+ {item.nama}
+
+
+
+
+
+
+ {item.alamat}
+
+
+
+
+
+
+ {item.nik}
+
+
+
+
+ }
+ onClick={() =>
+ router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data ajukan permohonan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default AjukanPermohonan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/layout.tsx b/src/app/admin/(dashboard)/desa/layanan/layout.tsx
new file mode 100644
index 00000000..7113ca7e
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/layout.tsx
@@ -0,0 +1,10 @@
+'use client'
+import LayoutTabsLayanan from "../_com/layoutTabLayanan";
+
+export default function Layout({children} : {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
new file mode 100644
index 00000000..7f26b666
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
@@ -0,0 +1,160 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPelayananPendudukNonPermanent() {
+ const router = useRouter();
+ const params = useParams();
+ const statePendudukNonPermanent = useProxy(
+ stateLayananDesa.pelayananPendudukNonPermanen
+ );
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ });
+
+ // Load data sekali dari backend
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await statePendudukNonPermanent.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data pelayanan penduduk non permanent');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ if (!statePendudukNonPermanent.findById.data) return;
+
+ // Update global state hanya di submit
+ const updated = {
+ ...statePendudukNonPermanent.findById.data,
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ };
+
+ await statePendudukNonPermanent.update.update(updated);
+ router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
+ };
+
+ return (
+
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Pelayanan Penduduk Non Permanent
+
+
+
+
+
+ Edit Pelayanan Penduduk Non Permanent
+
+ {/* Nama Field */}
+ handleChange('name')(e.target.value)}
+ required
+ />
+
+ {/* Deskripsi Field */}
+
+
+ Deskripsi
+
+
+
+
+ {/* Submit Button */}
+
+
+ {statePendudukNonPermanent.update.loading
+ ? 'Menyimpan...'
+ : 'Simpan Perubahan'}
+
+
+ router.back()}
+ disabled={statePendudukNonPermanent.update.loading}
+ >
+ Batal
+
+
+
+
+
+
+ );
+}
+
+export default EditPelayananPendudukNonPermanent;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx
new file mode 100644
index 00000000..e5ce63bd
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx
@@ -0,0 +1,104 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Divider,
+ Grid,
+ GridCol,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import stateLayananDesa from '../../../_state/desa/layananDesa';
+
+function PelayananPendudukNonPermanent() {
+ const router = useRouter();
+ const pelayananPendudukNonPermanen = useProxy(
+ stateLayananDesa.pelayananPendudukNonPermanen
+ );
+
+ useShallowEffect(() => {
+ pelayananPendudukNonPermanen.findById.load('edit');
+ }, []);
+
+ if (!pelayananPendudukNonPermanen.findById.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = pelayananPendudukNonPermanen.findById.data;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Preview Pelayanan Penduduk Non Permanen
+
+
+
+
+ }
+ radius="md"
+ onClick={() =>
+ router.push(
+ `/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
+ )
+ }
+ >
+ Edit
+
+
+
+
+
+ {/* Content */}
+
+
+
+
+ {data.name}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default PelayananPendudukNonPermanent;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
new file mode 100644
index 00000000..e8f4a8b6
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
@@ -0,0 +1,178 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPelayananPerizinanBerusaha() {
+ const router = useRouter();
+ const params = useParams<{ id: string }>();
+ const id = params?.id; // ini langsung string
+ const state = useProxy(stateLayananDesa.pelayananPerizinanBerusaha);
+
+ const [loading, setLoading] = useState(true);
+ const [formData, setFormData] = useState({
+ id: '',
+ name: '',
+ deskripsi: '',
+ link: '',
+ });
+
+ // Load data detail
+ useEffect(() => {
+ if (!id) {
+ toast.error("ID tidak valid");
+ return;
+ }
+
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ const data = await state.findById.load(id);
+ if (data) {
+ setFormData({
+ id: data.id,
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ link: data.link || "",
+ });
+ } else {
+ toast.error("Data tidak ditemukan");
+ }
+ } catch (error) {
+ console.error("Error loading data:", error);
+ toast.error("Gagal memuat data");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, [id]);
+
+
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ await state.update.update(formData);
+ router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
+ } catch (error) {
+ console.error('Error updating pelayanan perizinan berusaha:', error);
+ toast.error('Terjadi kesalahan saat update data');
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Pelayanan Perizinan Berusaha
+
+
+
+ {/* Form */}
+
+
+ Edit Pelayanan Perizinan Berusaha
+
+ handleChange('name')(e.target.value)}
+ required
+ />
+
+ handleChange('link')(e.target.value)}
+ />
+
+
+ Deskripsi
+
+
+
+
+
+ {state.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
+
+
+ router.back()}
+ disabled={state.update.loading}
+ >
+ Batal
+
+
+
+
+
+
+ );
+}
+
+export default EditPelayananPerizinanBerusaha;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx
new file mode 100644
index 00000000..3edb28cd
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx
@@ -0,0 +1,210 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Divider,
+ Grid,
+ GridCol,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Stepper,
+ StepperCompleted,
+ StepperStep,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconEdit } from '@tabler/icons-react';
+import { useEffect, useState } from 'react';
+import stateLayananDesa from '../../../_state/desa/layananDesa';
+import { useProxy } from 'valtio/utils';
+import { useRouter } from 'next/navigation';
+
+function PerizinanBerusaha() {
+ const router = useRouter();
+ const pelayananPerizinanBerusaha = useProxy(
+ stateLayananDesa.pelayananPerizinanBerusaha
+ );
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [active, setActive] = useState(1);
+ const nextStep = () =>
+ setActive((current) => (current < 6 ? current + 1 : current));
+ const prevStep = () =>
+ setActive((current) => (current > 0 ? current - 1 : current));
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ // You should get the ID from your router query or params
+ const id = 'edit'; // Replace with actual ID or get from URL params
+ await pelayananPerizinanBerusaha.findById.load(id);
+ } catch (err) {
+ setError('Gagal memuat data');
+ console.error('Error:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !pelayananPerizinanBerusaha.findById.data) {
+ return (
+
+ {error || 'Data tidak ditemukan'}
+
+ );
+ }
+
+ const data = pelayananPerizinanBerusaha.findById.data;
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Preview Pelayanan Perizinan Berusaha
+
+
+
+
+ }
+ radius="md"
+ onClick={() =>
+ router.push(
+ `/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
+ )
+ }
+ >
+ Edit
+
+
+
+
+
+ {/* Content */}
+
+
+
+
+ {data.name}
+
+
+
+
+
+
+
+
+
+ Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
+ umum:
+
+
+
+
+
+ Pendaftaran akun pada portal OSS
+
+
+ Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
+
+
+ Memilih KBLI dengan jenis usaha yang akan didaftarkan
+
+
+ Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
+
+
+ Proses verifikasi dan persetujuan oleh instansi terkait
+
+
+ Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
+
+
+ Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
+
+
+
+
+
+ Back
+
+ Next step
+
+
+
+
+ Penting untuk diingat bahwa prosedur dan persyaratan dapat
+ berubah seiring waktu. Untuk informasi yang lebih akurat dan
+ terkini, silakan kunjungi situs resmi OSS{' '}
+
+ {data.link}
+ {' '}
+ atau hubungi instansi terkait di pemerintah Indonesia yang
+ bertanggung jawab atas urusan perizinan usaha.
+
+
+
+
+
+
+ );
+}
+
+export default PerizinanBerusaha;
+
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx
new file mode 100644
index 00000000..092eadcd
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx
@@ -0,0 +1,290 @@
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, useCallback } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditSuratKeterangan() {
+ const router = useRouter();
+ const params = useParams();
+ const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
+
+ // state lokal untuk form
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ image2Id: '',
+ });
+
+ // state file upload
+ const [file, setFile] = useState(null);
+ const [file2, setFile2] = useState(null);
+
+ // state preview gambar
+ const [previewImage, setPreviewImage] = useState(null);
+ const [previewImage2, setPreviewImage2] = useState(null);
+
+ // load data awal
+ useEffect(() => {
+ const loadSurat = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateSurat.edit.load(id);
+ if (!data) return;
+
+ setFormData((prev) => ({
+ ...prev,
+ ...{
+ name: prev.name || data.name || "",
+ deskripsi: prev.deskripsi || data.deskripsi || "",
+ imageId: prev.imageId || data.imageId || "",
+ image2Id: prev.image2Id || data.image2Id || "",
+ },
+ }));
+
+ if (data.image?.link && !previewImage) setPreviewImage(data.image.link);
+ if (data.image2?.link && !previewImage2) setPreviewImage2(data.image2.link);
+ } catch (error) {
+ console.error("Error loading surat:", error);
+ toast.error("Gagal memuat data surat");
+ }
+ };
+
+ loadSurat();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [params?.id]);
+
+
+
+ // handler untuk submit
+ const handleSubmit = useCallback(async () => {
+ try {
+ // update form global hanya saat submit
+ stateSurat.edit.form = { ...stateSurat.edit.form, ...formData };
+
+ // upload file 1
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ stateSurat.edit.form.imageId = uploaded.id;
+ }
+
+ // upload file 2
+ if (file2) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ stateSurat.edit.form.image2Id = uploaded.id;
+ }
+
+ await stateSurat.edit.update();
+ toast.success('Surat berhasil diperbarui!');
+ router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
+ } catch (error) {
+ console.error('Error updating surat:', error);
+ toast.error('Terjadi kesalahan saat memperbarui surat');
+ }
+ }, [formData, file, file2, router, stateSurat.edit]);
+
+ return (
+
+ {/* Back Button */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Surat Keterangan
+
+
+
+
+
+ {/* Input nama */}
+ setFormData((prev) => ({ ...prev, name: e.target.value }))}
+ required
+ />
+
+ {/* Input deskripsi */}
+
+
+ Konten
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+ {/* Upload Gambar 1 */}
+
+
+ Gambar Konten Pelayanan
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Upload Gambar 2 */}
+
+
+ Gambar Alur Pelayanan Surat
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile2(selectedFile);
+ setPreviewImage2(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage2 && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditSuratKeterangan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx
new file mode 100644
index 00000000..59d992ac
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx
@@ -0,0 +1,193 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailSuratKeterangan() {
+ const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ suratKeteranganState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ suratKeteranganState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
+ }
+ };
+
+ if (!suratKeteranganState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = suratKeteranganState.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Surat Keterangan
+
+
+
+
+
+
+ Nama
+
+
+ {data?.name || '-'}
+
+
+
+
+
+ Deskripsi
+
+
+
+
+
+
+ Gambar Konten Pelayanan
+
+ {data?.image?.link ? (
+
+ ) : (
+
+ Tidak ada gambar
+
+ )}
+
+
+
+
+ Gambar Alur Pelayanan Surat
+
+ {data?.image2?.link ? (
+
+ ) : (
+
+ Tidak ada gambar
+
+ )}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={suratKeteranganState.delete.loading}
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/desa/layanan/pelayanan_surat_keterangan/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus surat keterangan ini?"
+ />
+
+ );
+}
+
+export default DetailSuratKeterangan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx
new file mode 100644
index 00000000..ba52a647
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx
@@ -0,0 +1,254 @@
+'use client';
+
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreateSuratKeterangan() {
+ const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
+ const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
+ const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateSurat.create.form = {
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ image2Id: '',
+ };
+ setPreviewImage(null);
+ setPreviewImage2(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!previewImage) {
+ return toast.warn('Pilih file gambar utama terlebih dahulu');
+ }
+
+ try {
+ // Upload gambar utama
+ const res1 = await ApiFetch.api.fileStorage.create.post({
+ file: previewImage.file,
+ name: `main_${previewImage.file.name}`,
+ });
+
+ const uploadedImage1 = res1.data?.data;
+ if (!uploadedImage1?.id) {
+ return toast.error('Gagal upload gambar utama');
+ }
+
+ let uploadedImage2 = null;
+ if (previewImage2) {
+ const res2 = await ApiFetch.api.fileStorage.create.post({
+ file: previewImage2.file,
+ name: `secondary_${previewImage2.file.name}`,
+ });
+ uploadedImage2 = res2.data?.data;
+ }
+
+ stateSurat.create.form.imageId = uploadedImage1.id;
+ if (uploadedImage2?.id) {
+ stateSurat.create.form.image2Id = uploadedImage2.id;
+ }
+
+ await stateSurat.create.create();
+ resetForm();
+ toast.success('Data surat keterangan berhasil ditambahkan');
+ router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
+ } catch (error) {
+ console.error('Error creating surat keterangan:', error);
+ toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Surat Keterangan
+
+
+
+
+
+ {/* Nama Surat */}
+ (stateSurat.create.form.name = val.target.value)}
+ label="Nama Surat Keterangan"
+ placeholder="Masukkan nama surat keterangan"
+ required
+ />
+
+ {/* Konten */}
+
+
+ Konten
+
+ {
+ stateSurat.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+ {/* Gambar Konten Pelayanan */}
+
+
+ Gambar Konten Pelayanan
+
+ {
+ const file = files[0];
+ if (file) {
+ setPreviewImage({
+ file,
+ preview: URL.createObjectURL(file),
+ });
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Gambar Alur Pelayanan Surat */}
+
+
+ Gambar Alur Pelayanan Surat
+
+ {
+ const file = files[0];
+ if (file) {
+ setPreviewImage2({
+ file,
+ preview: URL.createObjectURL(file),
+ });
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage2 ? (
+
+
+
+ ) : (
+
+ Kosongkan jika tidak ada gambar tambahan
+
+ )}
+
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateSuratKeterangan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx
new file mode 100644
index 00000000..e4948edf
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx
@@ -0,0 +1,173 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useMemo, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import stateLayananDesa from '../../../_state/desa/layananDesa';
+
+function SuratKeterangan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListSuratKeterangan({ search }: { search: string }) {
+ const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = suratKeteranganState.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = useMemo(() => {
+ if (!data) return [];
+ const keyword = search.toLowerCase();
+ return data.filter(item =>
+ item.name?.toLowerCase().includes(keyword) ||
+ item.deskripsi?.toLowerCase().includes(keyword)
+ );
+ }, [data, search]);
+
+ // Loading state
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ List Surat Keterangan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+ }
+ onClick={() =>
+ router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data surat keterangan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default SuratKeterangan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
new file mode 100644
index 00000000..27b89bdb
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
@@ -0,0 +1,150 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useCallback, useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPelayananTelunjukSakti() {
+ const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ link: '',
+ });
+
+ // Load data awal hanya sekali (pas ada id)
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateTelunjukDesa.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name ?? '',
+ deskripsi: data.deskripsi ?? '',
+ link: data.link ?? '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading pelayanan telunjuk sakti:', error);
+ toast.error('Gagal memuat data pelayanan telunjuk sakti');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ // Handler input controlled
+ const handleChange = useCallback(
+ (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ // Submit: update global state hanya saat simpan
+ const handleSubmit = async () => {
+ try {
+ stateTelunjukDesa.edit.form = {
+ ...stateTelunjukDesa.edit.form,
+ ...formData,
+ };
+ await stateTelunjukDesa.edit.update();
+ toast.success('Pelayanan telunjuk sakti berhasil diperbarui!');
+ router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
+ } catch (error) {
+ console.error('Error updating pelayanan telunjuk sakti:', error);
+ toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
+ }
+ };
+
+ return (
+
+ {/* Back Button + Title */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Pelayanan Telunjuk Sakti Desa
+
+
+
+
+
+ {/* Nama */}
+ handleChange('name', e.target.value)}
+ required
+ />
+
+ {/* Deskripsi */}
+ handleChange('deskripsi', e.target.value)}
+ label="Judul Link"
+ placeholder="Masukkan judul link"
+ required
+ />
+
+ {/* Link */}
+ handleChange('link', e.target.value)}
+ />
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPelayananTelunjukSakti;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx
new file mode 100644
index 00000000..04b7a37f
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx
@@ -0,0 +1,181 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailPelayananTelunjukSakti() {
+ const telunjukSaktiState = useProxy(
+ stateLayananDesa.pelayananTelunjukSaktiDesa
+ );
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ telunjukSaktiState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ telunjukSaktiState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
+ }
+ };
+
+ if (!telunjukSaktiState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = telunjukSaktiState.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Pelayanan Telunjuk Sakti Desa
+
+
+
+
+
+
+ Nama
+
+
+ {data?.name || '-'}
+
+
+
+
+
+ Link
+
+ {data?.link ? (
+
+ {data.link}
+
+ ) : (
+
+ Tidak ada link
+
+ )}
+
+
+
+
+ Deskripsi
+
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={telunjukSaktiState.delete.loading}
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus layanan ini?"
+ />
+
+ );
+}
+
+export default DetailPelayananTelunjukSakti;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
new file mode 100644
index 00000000..2fd23ace
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreatePelayananTelunjukDesa() {
+ const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateTelunjukDesa.create.form = {
+ name: '',
+ deskripsi: '',
+ link: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ try {
+ await stateTelunjukDesa.create.create();
+ resetForm();
+ toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
+ router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
+ } catch (error) {
+ console.error('Error create pelayanan telunjuk sakti:', error);
+ toast.error('Terjadi kesalahan saat menambahkan data');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Pelayanan Telunjuk Sakti Desa
+
+
+
+ {/* Form */}
+
+
+ {/* Nama */}
+ {
+ stateTelunjukDesa.create.form.name = val.target.value;
+ }}
+ label="Nama Pelayanan"
+ placeholder="Masukkan nama pelayanan telunjuk sakti desa"
+ required
+ />
+
+ {/* Deskripsi */}
+ {
+ stateTelunjukDesa.create.form.deskripsi = val.target.value;
+ }}
+ label="Judul Link"
+ placeholder="Masukkan judul link"
+ required
+ />
+
+ {/* Link */}
+ {
+ stateTelunjukDesa.create.form.link = val.target.value;
+ }}
+ label="Link"
+ placeholder="Masukkan link pelayanan"
+ required
+ />
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePelayananTelunjukDesa;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx
new file mode 100644
index 00000000..03752f68
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx
@@ -0,0 +1,318 @@
+// /* eslint-disable react-hooks/exhaustive-deps */
+// 'use client'
+// import colors from '@/con/colors';
+// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
+// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
+// import { useRouter } from 'next/navigation';
+// import { useEffect, useMemo, useState } from 'react';
+// import { useProxy } from 'valtio/utils';
+// import HeaderSearch from '../../../_com/header';
+// import JudulList from '../../../_com/judulList';
+// import stateLayananDesa from '../../../_state/desa/layananDesa';
+
+// function PelayananTelunjukSakti() {
+// const [search, setSearch] = useState("");
+// return (
+//
+// }
+// value={search}
+// onChange={(e) => setSearch(e.currentTarget.value)}
+// />
+//
+//
+// );
+// }
+
+// function ListPelayananTelunjukSakti({ search }: { search: string }) {
+// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
+// const router = useRouter()
+
+// const {
+// data,
+// page,
+// totalPages,
+// loading,
+// load,
+// } = telunjukSaktiState.findMany;
+
+// useEffect(() => {
+// load(page, 10)
+// }, [])
+
+// const filteredData = useMemo(() => {
+// if (!data) return [];
+// return data.filter(item => {
+// const keyword = search.toLowerCase();
+// return (
+// item.name?.toLowerCase().includes(keyword) ||
+// item.link?.toLowerCase().includes(keyword) ||
+// item.deskripsi?.toLowerCase().includes(keyword)
+// );
+// })
+// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
+// }, [data, search]);
+
+// if (loading || !data) {
+// return (
+//
+//
+//
+// );
+// }
+
+// if (data.length === 0) {
+// return (
+//
+//
+//
+//
+//
+//
+// Nama
+// Link
+// Detail
+//
+//
+//
+//
+//
+//
+// Tidak ada data
+//
+//
+//
+//
+//
+//
+//
+// );
+// }
+
+// return (
+//
+//
+//
+//
+//
+//
+// Nama
+// Link
+// Detail
+//
+//
+//
+// {filteredData.map((item) => (
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+// router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
+//
+//
+//
+//
+//
+// ))}
+//
+//
+//
+//
+// {
+// load(newPage, 10);
+// window.scrollTo(0, 0);
+// }}
+// total={totalPages}
+// mt="md"
+// mb="md"
+// />
+//
+//
+// );
+// }
+
+// export default PelayananTelunjukSakti;
+
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import stateLayananDesa from '../../../_state/desa/layananDesa';
+
+function PelayananTelunjukSakti() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPelayananTelunjukSakti({ search }: { search: string }) {
+ const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Pelayanan Telunjuk Sakti
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama
+ Link
+ Detail
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+
+
+
+ router.push(
+ `/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data layanan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PelayananTelunjukSakti;
+
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
new file mode 100644
index 00000000..2f68bc4e
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
@@ -0,0 +1,234 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPenghargaan() {
+ const statePenghargaan = useProxy(penghargaanState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ // Lokal formData
+ const [formData, setFormData] = useState({
+ name: '',
+ juara: '',
+ deskripsi: '',
+ imageId: '',
+ });
+
+ // Load data pertama kali
+ useEffect(() => {
+ const loadPenghargaan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await statePenghargaan.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ juara: data.juara || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data?.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (error) {
+ console.error('Error loading penghargaan:', error);
+ toast.error('Gagal memuat data penghargaan');
+ }
+ };
+
+ loadPenghargaan();
+ }, [params?.id]);
+
+ // Submit
+ const handleSubmit = async () => {
+ try {
+ // Sync ke global state saat submit
+ statePenghargaan.edit.form = {
+ ...statePenghargaan.edit.form,
+ ...formData,
+ };
+
+ // Upload file baru (kalau ada)
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ statePenghargaan.edit.form.imageId = uploaded.id;
+ }
+
+ await statePenghargaan.edit.update();
+ toast.success('Penghargaan berhasil diperbarui!');
+ router.push('/admin/desa/penghargaan');
+ } catch (error) {
+ console.error('Error updating penghargaan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui penghargaan');
+ }
+ };
+
+ return (
+
+ {/* Tombol Back + Title */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Penghargaan
+
+
+
+ {/* Card Form */}
+
+
+ {/* Input Judul */}
+ setFormData((prev) => ({ ...prev, name: e.target.value }))}
+ required
+ />
+
+ {/* Input Juara */}
+ setFormData((prev) => ({ ...prev, juara: e.target.value }))}
+ required
+ />
+
+ {/* Upload Gambar */}
+
+
+ Gambar Penghargaan
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Deskripsi */}
+
+
+ Deskripsi
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPenghargaan;
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx
new file mode 100644
index 00000000..05a5774a
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx
@@ -0,0 +1,174 @@
+'use client'
+import React, { useState } from 'react';
+import penghargaanState from '../../../_state/desa/penghargaan';
+import { useProxy } from 'valtio/utils';
+import { useParams, useRouter } from 'next/navigation';
+import { useShallowEffect } from '@mantine/hooks';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import colors from '@/con/colors';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function DetailPenghargaan() {
+ const statePenghargaan = useProxy(penghargaanState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ statePenghargaan.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ statePenghargaan.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/desa/penghargaan');
+ }
+ };
+
+ if (!statePenghargaan.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = statePenghargaan.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Penghargaan
+
+
+
+
+
+
+ Gambar
+
+ {data.image?.link ? (
+
+ ) : (
+
+ Tidak ada gambar
+
+ )}
+
+
+
+
+ Judul
+
+
+ {data.name || '-'}
+
+
+
+
+
+ Juara
+
+
+ {data.juara || '-'}
+
+
+
+
+
+ Deskripsi
+
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/desa/penghargaan/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus penghargaan ini?"
+ />
+
+ );
+}
+
+export default DetailPenghargaan;
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
new file mode 100644
index 00000000..dbfb717c
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
@@ -0,0 +1,182 @@
+'use client';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import penghargaanState from '../../../_state/desa/penghargaan';
+
+function CreatePenghargaan() {
+ const statePenghargaan = useProxy(penghargaanState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ statePenghargaan.create.form = {
+ name: '',
+ juara: '',
+ deskripsi: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ statePenghargaan.create.form.imageId = uploaded.id;
+
+ await statePenghargaan.create.create();
+ resetForm();
+ router.push('/admin/desa/penghargaan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Penghargaan
+
+
+
+ {/* Form */}
+
+
+ (statePenghargaan.create.form.name = val.target.value)}
+ label="Nama Penghargaan"
+ placeholder="Masukkan nama penghargaan"
+ required
+ />
+
+ (statePenghargaan.create.form.juara = val.target.value)}
+ label="Juara"
+ placeholder="Masukkan juara"
+ required
+ />
+
+
+ Deskripsi
+ {
+ statePenghargaan.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+ {/* Dropzone Upload */}
+
+ Gambar
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Button Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePenghargaan;
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx
new file mode 100644
index 00000000..ac4de300
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx
@@ -0,0 +1,164 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconDeviceImacCog, IconPlus, IconSearch } 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 Penghargaan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPenghargaan({ search }: { search: string }) {
+ const state = useProxy(penghargaanState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ // Loading state
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ List Penghargaan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/penghargaan/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+ }
+ onClick={() =>
+ router.push(`/admin/desa/penghargaan/${item.id}`)
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data penghargaan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default Penghargaan;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx
new file mode 100644
index 00000000..e8702d32
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx
@@ -0,0 +1,110 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconListDetails, IconCategory } from '@tabler/icons-react';
+
+function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const tabs = [
+ {
+ label: "List Pengumuman",
+ value: "listpengumuman",
+ href: "/admin/desa/pengumuman/list-pengumuman",
+ icon: ,
+ tooltip: "Lihat semua daftar pengumuman"
+ },
+ {
+ label: "Kategori Pengumuman",
+ value: "kategoripengumuman",
+ href: "/admin/desa/pengumuman/kategori-pengumuman",
+ icon: ,
+ tooltip: "Kelola kategori pengumuman"
+ },
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname)
+ const [activeTab, setActiveTab] = useState(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 (
+
+ Pengumuman
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsLayanan;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
new file mode 100644
index 00000000..c2eeb624
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
@@ -0,0 +1,130 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKategoriPengumuman() {
+ const editState = useProxy(stateDesaPengumuman.category);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({ name: '' });
+
+ // Load data awal sekali aja
+ useEffect(() => {
+ const loadKategori = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await editState.update.load(id);
+ if (data) {
+ setFormData({ name: data.name || '' });
+ }
+ } catch (error) {
+ console.error('Error loading kategori Pengumuman:', error);
+ toast.error('Gagal memuat data kategori Pengumuman');
+ }
+ };
+
+ loadKategori();
+ }, [params?.id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state hanya di sini
+ editState.update.form = {
+ ...editState.update.form,
+ name: formData.name,
+ };
+
+ await editState.update.update();
+ toast.success('Kategori Pengumuman berhasil diperbarui!');
+ router.push('/admin/desa/pengumuman/kategori-pengumuman');
+ } catch (error) {
+ console.error('Error updating kategori Pengumuman:', error);
+ toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Kategori Pengumuman
+
+
+
+ {/* Form */}
+
+
+ handleChange('name', e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKategoriPengumuman;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
new file mode 100644
index 00000000..c5e4bac0
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
@@ -0,0 +1,91 @@
+'use client'
+import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreateKategoriPengumuman() {
+ const createState = useProxy(stateDesaPengumuman.category);
+ const router = useRouter();
+
+ const resetForm = () => {
+ createState.create.form = {
+ name: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await createState.create.create();
+ resetForm();
+ router.push('/admin/desa/pengumuman/kategori-pengumuman');
+ };
+
+ return (
+
+ {/* Header dengan back button */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kategori Pengumuman
+
+
+
+ {/* Form utama */}
+
+
+ (createState.create.form.name = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKategoriPengumuman;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx
new file mode 100644
index 00000000..541c0cae
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx
@@ -0,0 +1,163 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box, Button, Center, Paper, Skeleton, Stack,
+ Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
+ Text, Title, Tooltip, Pagination
+} from '@mantine/core';
+import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import stateDesaPengumuman from '../../../_state/desa/pengumuman';
+
+function KategoriPengumuman() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKategoriPengumuman({ search }: { search: string }) {
+ const listDataState = useProxy(stateDesaPengumuman.category)
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const { data, page, totalPages, loading, load } = listDataState.findMany;
+
+ useEffect(() => {
+ load(1, 10, search)
+ }, [search])
+
+ const handleDelete = () => {
+ if (selectedId) {
+ listDataState.delete.delete(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ load(page, 10, search)
+ }
+ }
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ List Kategori Pengumuman
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ No
+ Nama
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item, index) => (
+
+
+ {(page - 1) * 10 + index + 1}
+
+
+ {item.name}
+
+
+
+ router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
+ >
+
+
+
+
+
+
+ {
+ setSelectedId(item.id)
+ setModalHapus(true)
+ }}>
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data kategori pengumuman yang cocok
+
+
+
+ )}
+
+
+
+
+
+
+
+ load(newPage, 10, search)}
+ total={totalPages}
+ color="blue"
+ radius="md"
+ />
+
+
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text='Apakah anda yakin ingin menghapus kategori Pengumuman ini?'
+ />
+
+ )
+}
+
+export default KategoriPengumuman;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/layout.tsx b/src/app/admin/(dashboard)/desa/pengumuman/layout.tsx
new file mode 100644
index 00000000..4a4acdbd
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import LayoutTabs from './_com/layoutTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
new file mode 100644
index 00000000..9e7a622f
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
@@ -0,0 +1,177 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
+import stateDesaPengumuman from "@/app/admin/(dashboard)/_state/desa/pengumuman";
+import colors from "@/con/colors";
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { IconArrowBack } from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+function EditPengumuman() {
+ const editState = useProxy(stateDesaPengumuman);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ judul: "",
+ deskripsi: "",
+ categoryPengumumanId: "",
+ content: "",
+ });
+
+ // Load kategori & pengumuman by id saat pertama kali
+ useEffect(() => {
+ editState.category.findMany.load();
+
+ const loadpengumuman = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateDesaPengumuman.pengumuman.edit.load(id);
+ if (data) {
+ setFormData({
+ judul: data.judul || "",
+ deskripsi: data.deskripsi || "",
+ categoryPengumumanId: data.categoryPengumumanId || "",
+ content: data.content || "",
+ });
+ }
+ } catch (error) {
+ console.error("Error loading pengumuman:", error);
+ toast.error("Gagal memuat data pengumuman");
+ }
+ };
+
+ loadpengumuman();
+ }, [params?.id]);
+
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // update global state hanya sekali pas submit
+ editState.pengumuman.edit.form = {
+ ...editState.pengumuman.edit.form,
+ ...formData,
+ };
+
+ await editState.pengumuman.edit.update();
+ toast.success("Pengumuman berhasil diperbarui!");
+ router.push("/admin/desa/pengumuman/list-pengumuman");
+ } catch (error) {
+ console.error("Error updating pengumuman:", error);
+ toast.error("Terjadi kesalahan saat memperbarui pengumuman");
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Pengumuman
+
+
+
+
+
+ handleChange("judul", e.target.value)}
+ required
+ />
+
+ handleChange("deskripsi", e.target.value)}
+ required
+ />
+
+ handleChange("categoryPengumumanId", val || "")}
+ label="Kategori"
+ placeholder="Pilih kategori"
+ data={
+ editState.category.findMany.data?.map((v) => ({
+ value: v.id,
+ label: v.name,
+ })) || []
+ }
+ clearable
+ searchable
+ required
+ error={
+ !formData.categoryPengumumanId ? "Pilih kategori" : undefined
+ }
+ />
+
+
+
+ Konten Lengkap
+
+ handleChange("content", htmlContent)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPengumuman;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx
new file mode 100644
index 00000000..e1847094
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx
@@ -0,0 +1,164 @@
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useRouter, useParams } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
+
+export default function DetailPengumuman() {
+ const pengumumanState = useProxy(stateDesaPengumuman);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ pengumumanState.pengumuman.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ pengumumanState.pengumuman.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/desa/pengumuman/list-pengumuman');
+ }
+ };
+
+ if (!pengumumanState.pengumuman.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = pengumumanState.pengumuman.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Pengumuman
+
+
+
+
+
+
+ Kategori
+
+
+ {data?.CategoryPengumuman?.name || '-'}
+
+
+
+
+
+ Judul
+
+
+ {data?.judul || '-'}
+
+
+
+
+
+ Deskripsi
+
+
+ {data?.deskripsi || '-'}
+
+
+
+
+
+ Konten
+
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/desa/pengumuman/list-pengumuman/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus pengumuman ini?"
+ />
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
new file mode 100644
index 00000000..d2d62314
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
@@ -0,0 +1,137 @@
+'use client';
+
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreatePengumuman() {
+ const pengumumanState = useProxy(stateDesaPengumuman);
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ pengumumanState.category.findMany.load();
+ }, []);
+
+ const handleSubmit = async () => {
+ await pengumumanState.pengumuman.create.create();
+ resetForm();
+ router.push('/admin/desa/pengumuman/list-pengumuman');
+ };
+
+ const resetForm = () => {
+ pengumumanState.pengumuman.create.form = {
+ judul: '',
+ deskripsi: '',
+ content: '',
+ categoryPengumumanId: '',
+ };
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Pengumuman
+
+
+
+
+
+ {/* Judul */}
+ (pengumumanState.pengumuman.create.form.judul = val.target.value)}
+ label="Judul"
+ placeholder="Masukkan judul pengumuman"
+ required
+ />
+
+ {/* Kategori */}
+ {
+ pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
+ }}
+ data={pengumumanState.category.findMany.data?.map((item) => ({
+ label: item.name,
+ value: item.id,
+ }))}
+ searchable
+ nothingFoundMessage="Tidak ditemukan"
+ />
+
+ {/* Deskripsi Singkat */}
+ (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
+ label="Deskripsi Singkat"
+ placeholder="Masukkan deskripsi singkat"
+ required
+ />
+
+ {/* Konten Editor */}
+
+
+ Konten Lengkap
+
+ {
+ pengumumanState.pengumuman.create.form.content = htmlContent;
+ }}
+ />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePengumuman;
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx
new file mode 100644
index 00000000..e4c819e4
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx
@@ -0,0 +1,153 @@
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import stateDesaPengumuman from '../../../_state/desa/pengumuman';
+
+function Pengumuman() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPengumuman({ search }: { search: string }) {
+ const pengumumanState = useProxy(stateDesaPengumuman);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Pengumuman
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Judul
+ Kategori
+ Detail
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.judul}
+
+
+
+
+
+ {item.CategoryPengumuman?.name || '-'}
+
+
+
+
+ router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada pengumuman yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default Pengumuman;
diff --git a/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..dc981aff
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
@@ -0,0 +1,110 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { IconCategory, IconListCheck } from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+
+function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const tabs = [
+ {
+ label: "List Potensi",
+ value: "list_potensi",
+ href: "/admin/desa/potensi/list-potensi",
+ icon: ,
+ tooltip: "Lihat semua potensi desa"
+ },
+ {
+ label: "Kategori Potensi",
+ value: "kategori_potensi",
+ href: "/admin/desa/potensi/kategori-potensi",
+ icon: ,
+ tooltip: "Kelola kategori potensi"
+ },
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname)
+ const [activeTab, setActiveTab] = useState(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 (
+
+ Potensi
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsPotensi;
diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
new file mode 100644
index 00000000..16d0d951
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKategoriPotensi() {
+ const editState = useProxy(potensiDesaState.kategoriPotensi);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ });
+
+ // Load data dari backend -> isi ke formData lokal
+ useEffect(() => {
+ const loadKategori = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await editState.update.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading kategori potensi:', error);
+ toast.error('Gagal memuat data kategori potensi');
+ }
+ };
+
+ loadKategori();
+ }, [params?.id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state hanya pas submit
+ editState.update.form = {
+ ...editState.update.form,
+ nama: formData.nama,
+ };
+
+ await editState.update.update();
+ toast.success('Kategori Potensi berhasil diperbarui!');
+ router.push('/admin/desa/potensi/kategori-potensi');
+ } catch (error) {
+ console.error('Error updating kategori potensi:', error);
+ toast.error('Terjadi kesalahan saat memperbarui kategori potensi');
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Kategori Potensi
+
+
+
+
+
+ handleChange('nama', e.currentTarget.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKategoriPotensi;
diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
new file mode 100644
index 00000000..68f63d0a
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
@@ -0,0 +1,91 @@
+'use client';
+import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreateKategoriPotensi() {
+ const createState = useProxy(potensiDesaState.kategoriPotensi);
+ const router = useRouter();
+
+ const resetForm = () => {
+ createState.create.form = {
+ nama: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await createState.create.create();
+ resetForm();
+ router.push('/admin/desa/potensi/kategori-potensi');
+ };
+
+ return (
+
+ {/* Header dengan back button */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kategori Potensi
+
+
+
+ {/* Form utama */}
+
+
+ (createState.create.form.nama = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKategoriPotensi;
diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
new file mode 100644
index 00000000..3d408f8f
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
@@ -0,0 +1,151 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core';
+import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import potensiDesaState from '../../../_state/desa/potensi';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function KategoriPotensi() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKategoriPotensi({ search }: { search: string }) {
+ const listDataState = useProxy(potensiDesaState.kategoriPotensi)
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const { data, page, totalPages, loading, load } = listDataState.findMany;
+
+ useEffect(() => {
+ load(1, 10, search)
+ }, [search])
+
+ const handleDelete = () => {
+ if (selectedId) {
+ listDataState.delete.delete(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ load(page, 10, search)
+ }
+ }
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ List Kategori Potensi
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ No
+ Nama
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item, index) => (
+
+
+ {(page - 1) * 10 + index + 1}
+
+
+ {item.nama}
+
+
+ router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
+
+
+
+
+ {
+ setSelectedId(item.id)
+ setModalHapus(true)
+ }}>
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data kategori potensi yang cocok
+
+
+
+ )}
+
+
+
+
+
+
+
+ load(newPage, 10, search)}
+ total={totalPages}
+ color="blue"
+ radius="md"
+ />
+
+
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text='Apakah anda yakin ingin menghapus kategori Potensi ini?'
+ />
+
+ )
+}
+
+export default KategoriPotensi;
diff --git a/src/app/admin/(dashboard)/desa/potensi/layout.tsx b/src/app/admin/(dashboard)/desa/potensi/layout.tsx
new file mode 100644
index 00000000..b677970c
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/layout.tsx
@@ -0,0 +1,14 @@
+'use client'
+import React from 'react';
+import LayoutTabsPotensi from './_lib/layoutTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Layout;
+
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
new file mode 100644
index 00000000..68a453b3
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
@@ -0,0 +1,280 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
+import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
+import colors from "@/con/colors";
+import ApiFetch from "@/lib/api-fetch";
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { Dropzone } from "@mantine/dropzone";
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+function EditPotensi() {
+ const potensiState = useProxy(potensiDesaState.potensiDesa);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: "",
+ deskripsi: "",
+ kategoriId: "",
+ content: "",
+ imageId: "",
+ });
+
+ // handle input changes
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ useEffect(() => {
+ potensiDesaState.kategoriPotensi.findMany.load();
+
+ const loadPotensi = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await potensiState.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ kategoriId: data.kategoriId || "",
+ content: data.content || "",
+ imageId: data.imageId || "",
+ });
+
+ // // merge, bukan replace
+ // setFormData((prev) => ({
+ // ...prev,
+ // name: data.name ?? prev.name,
+ // deskripsi: data.deskripsi ?? prev.deskripsi,
+ // kategoriId: data.kategoriId ?? prev.kategoriId,
+ // content: data.content ?? prev.content,
+ // imageId: data.imageId ?? prev.imageId,
+ // }));
+
+ if (data?.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (error) {
+ console.error("Error loading potensi:", error);
+ toast.error("Gagal memuat data potensi");
+ }
+ };
+
+ loadPotensi();
+ }, [params?.id]);
+
+
+ const handleSubmit = async () => {
+ try {
+ let imageId = formData.imageId;
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+
+ imageId = uploaded.id;
+ }
+
+ potensiState.edit.form = {
+ ...formData,
+ imageId,
+ };
+
+ await potensiState.edit.update();
+ toast.success("Potensi berhasil diperbarui!");
+ router.push("/admin/desa/potensi/list-potensi");
+ } catch (error) {
+ console.error("Error updating potensi:", error);
+ toast.error("Terjadi kesalahan saat memperbarui potensi");
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Potensi Desa
+
+
+
+
+
+ handleChange("name", e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi Singkat
+
+
+ handleChange("deskripsi", htmlContent)
+ }
+ />
+
+
+ handleChange("kategoriId", val || "")}
+ label="Kategori"
+ placeholder="Pilih kategori"
+ data={
+ potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
+ value: v.id,
+ label: v.nama,
+ })) || []
+ }
+ clearable
+ searchable
+ required
+ error={!formData.kategoriId ? "Pilih kategori" : undefined}
+ />
+
+
+
+ Gambar Potensi
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() =>
+ toast.error("File tidak valid, gunakan format gambar")
+ }
+ maxSize={5 * 1024 ** 2}
+ accept={{ "image/*": [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Konten Lengkap
+
+
+ handleChange("content", htmlContent)
+ }
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPotensi;
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
new file mode 100644
index 00000000..73531d4e
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
@@ -0,0 +1,153 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useRouter, useParams } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
+
+export default function DetailPotensi() {
+ const router = useRouter();
+ const params = useParams();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const potensiState = useProxy(potensiDesaState.potensiDesa);
+
+ useShallowEffect(() => {
+ potensiState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ potensiState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/desa/potensi/list-potensi");
+ }
+ };
+
+ if (!potensiState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = potensiState.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Potensi
+
+
+
+
+
+ Judul
+ {data.name || '-'}
+
+
+
+ Kategori
+ {data.kategori?.nama || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+ Konten
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/desa/potensi/list-potensi/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus potensi ini?"
+ />
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
new file mode 100644
index 00000000..6eeb29b4
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
@@ -0,0 +1,212 @@
+'use client';
+
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreatePotensi() {
+ const potensiState = useProxy(potensiDesaState.potensiDesa);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ potensiDesaState.kategoriPotensi.findMany.load();
+ }, []);
+
+ const handleSubmit = async () => {
+ if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ potensiState.create.form.imageId = uploaded.id;
+
+ await potensiState.create.create();
+
+ resetForm();
+ router.push('/admin/desa/potensi/list-potensi');
+ };
+
+ const resetForm = () => {
+ potensiState.create.form = {
+ name: '',
+ deskripsi: '',
+ kategoriId: '',
+ imageId: '',
+ content: '',
+ };
+
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Potensi Desa
+
+
+
+
+
+ {/* Judul */}
+ (potensiState.create.form.name = val.target.value)}
+ label="Judul"
+ placeholder="Masukkan judul potensi"
+ required
+ />
+
+ {/* Deskripsi */}
+
+
+ Deskripsi Singkat
+
+ {
+ potensiState.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+ {/* Kategori */}
+ {
+ potensiState.create.form.kategoriId = val ?? "";
+ }}
+ data={potensiDesaState.kategoriPotensi.findMany.data?.map((item) => ({
+ value: item.id,
+ label: item.nama,
+ }))}
+ />
+
+ {/* Upload Gambar */}
+
+
+ Gambar Potensi
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Konten Editor */}
+
+
+ Konten Lengkap
+
+ {
+ potensiState.create.form.content = htmlContent;
+ }}
+ />
+
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePotensi;
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
new file mode 100644
index 00000000..0599c55b
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
@@ -0,0 +1,172 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import potensiDesaState from '../../../_state/desa/potensi';
+
+function Potensi() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPotensi({ search }: { search: string }) {
+ const potensiState = useProxy(potensiDesaState);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = potensiState.potensiDesa.findMany;
+
+ useEffect(() => {
+ potensiState.kategoriPotensi.findMany.load();
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Potensi Desa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Judul
+ Kategori
+ Deskripsi
+ Detail
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+ {item.kategori?.nama || '-'}
+
+
+
+
+
+
+
+
+ }
+ onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data potensi yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default Potensi;
diff --git a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx
new file mode 100644
index 00000000..7554a1c8
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx
@@ -0,0 +1,117 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
+
+function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const tabs = [
+ {
+ label: "Profile Desa",
+ value: "profiledesa",
+ href: "/admin/desa/profile/profile-desa",
+ icon: ,
+ tooltip: "Lihat dan kelola profil desa"
+ },
+ {
+ label: "Profile Perbekel",
+ value: "profileperbekel",
+ href: "/admin/desa/profile/profile-perbekel",
+ icon: ,
+ tooltip: "Kelola data Perbekel"
+ },
+ {
+ label: "Profile Perbekel Dari Masa Ke Masa",
+ value: "profile-perbekel-dari-masa-ke-masa",
+ href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
+ icon: ,
+ tooltip: "Riwayat Perbekel dari masa ke masa"
+ }
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname)
+ const [activeTab, setActiveTab] = useState(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 (
+
+ Profile Desa
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+ export default LayoutTabsDetail;
diff --git a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsEdit.tsx b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsEdit.tsx
new file mode 100644
index 00000000..75201d3d
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsEdit.tsx
@@ -0,0 +1,71 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+
+function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const tabs = [
+ {
+ label: "Sejarah Desa",
+ value: "sejarahdesa",
+ href: "/admin/desa/profile/edit/sejarah_desa"
+ },
+ {
+ label: "Visi Misi Desa",
+ value: "visimisidesa",
+ href: "/admin/desa/profile/edit/visi_misi_desa"
+ },
+ {
+ label: "Lambang Desa",
+ value: "lambangdesa",
+ href: "/admin/desa/profile/edit/lambang_desa"
+ },
+ {
+ label: "Maskot Desa",
+ value: "maskotdesa",
+ href: "/admin/desa/profile/edit/maskot_desa"
+ },
+ ];
+ const curentTab = tabs.find(tab => tab.href === pathname)
+ const [activeTab, setActiveTab] = useState(curentTab?.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 (
+
+
+
+ {tabs.map((e, i) => (
+ {e.label}
+ ))}
+
+ {tabs.map((e, i) => (
+
+ {/* Konten dummy, bisa diganti tergantung routing */}
+ <>>
+
+ ))}
+
+ {children}
+
+ );
+}
+
+export default LayoutTabsEdit;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/profile/layout.tsx b/src/app/admin/(dashboard)/desa/profile/layout.tsx
new file mode 100644
index 00000000..f82687f5
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/layout.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import LayoutTabsDetail from "./_lib/layoutTabsDetail"
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx
new file mode 100644
index 00000000..03660631
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx
@@ -0,0 +1,154 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function Page() {
+ const lambangState = useProxy(stateProfileDesa.lambangDesa)
+ const router = useRouter()
+ const params = useParams()
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ // Load data
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error("ID tidak valid");
+ router.push("/admin/desa/profile/profile-desa");
+ return;
+ }
+
+ try {
+ const data = await lambangState.findUnique.load(id);
+ lambangState.update.initialize(data);
+ } catch (error) {
+ console.error("Error loading lambang:", error);
+ toast.error("Gagal memuat data lambang desa");
+ }
+ };
+
+ loadData();
+
+ return () => {
+ lambangState.update.reset();
+ lambangState.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ const handleSubmit = async () => {
+ if (isSubmitting || !lambangState.update.form.judul.trim()) {
+ toast.error("Judul wajib diisi");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const success = await lambangState.update.submit();
+
+ if (success) {
+ toast.success("Data berhasil disimpan");
+ router.push("/admin/desa/profile/profile-desa");
+ }
+ } catch (error) {
+ console.error("Error update lambang desa:", error);
+ toast.error("Terjadi kesalahan saat update lambang desa");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleBack = () => router.back();
+
+ // Loading state
+ if (lambangState.findUnique.loading || lambangState.update.loading) {
+ return (
+
+
+ Memuat data...
+
+
+ );
+ }
+
+ // Error state
+ if (lambangState.findUnique.error) {
+ return (
+
+
+
+
+
+ } color="red">
+ Error
+ {lambangState.findUnique.error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Edit Lambang Desa
+
+
+
+
+ Edit Lambang Desa
+
+ {/* Judul */}
+ Judul}
+ placeholder="Judul lambang"
+ defaultValue={lambangState.update.form.judul}
+ onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
+ error={!lambangState.update.form.judul && "Judul wajib diisi"}
+ />
+
+ {/* Deskripsi */}
+
+ Deskripsi
+ lambangState.update.form.deskripsi = val}
+ />
+
+
+ {/* Buttons */}
+
+
+ {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
+
+
+
+ Batal
+
+
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx
new file mode 100644
index 00000000..8ead63e3
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx
@@ -0,0 +1,285 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function Page() {
+ const maskotState = useProxy(stateProfileDesa.maskotDesa);
+ const router = useRouter();
+ const params = useParams();
+
+ const [images, setImages] = useState<
+ Array<{ file: File | null; preview: string; label: string; imageId?: string }>
+>([]);
+ const [formData, setFormData] = useState({
+ judul: '',
+ deskripsi: '',
+ images: [] as Array<{ label: string; imageId: string }>,
+ });
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Load data
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error("ID tidak valid");
+ router.push("/admin/desa/profile/profile-desa");
+ return;
+ }
+
+ try {
+ const data = await maskotState.findUnique.load(id);
+ if (data) {
+ maskotState.update.initialize(data);
+
+ setFormData({
+ judul: data.judul || '',
+ deskripsi: data.deskripsi || '',
+ images: (data.images || []).map((img: any) => ({
+ label: img.label,
+ imageId: img.image?.id ?? '',
+ })),
+ });
+
+ if (data?.images?.length > 0 && data.images[0].image?.link) {
+ setImages(data.images.map((img: any) => ({
+ file: null,
+ preview: img.image.link,
+ label: img.label,
+ imageId: img.image.id, // simpan id lama
+ })));
+ }
+ }
+ } catch (error) {
+ console.error("Error loading maskot:", error);
+ toast.error("Gagal memuat data maskot");
+ }
+ };
+
+ loadData();
+
+ return () => {
+ maskotState.update.reset();
+ maskotState.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ const handleBack = () => router.back();
+
+ const handleSubmit = async () => {
+ if (isSubmitting || !formData.judul.trim()) {
+ toast.error("Judul wajib diisi");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const uploadedImages = [];
+
+ // Upload semua gambar baru
+ for (const img of images) {
+ if (!img.file) {
+ if (!img.imageId) continue; // kalau benar2 kosong, skip
+ uploadedImages.push({ imageId: img.imageId, label: img.label });
+ continue;
+ }
+
+ // upload baru
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file: img.file,
+ name: img.file.name,
+ });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ toast.error("Gagal upload salah satu gambar");
+ return;
+ }
+ uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
+ }
+
+
+ // Update ke global state
+ maskotState.update.updateField("judul", formData.judul);
+ maskotState.update.updateField("deskripsi", formData.deskripsi);
+ maskotState.update.updateField("images", uploadedImages);
+
+ const success = await maskotState.update.submit();
+
+ if (success) {
+ toast.success("Maskot berhasil diperbarui!");
+ router.push("/admin/desa/profile/profile-desa");
+ }
+ } catch (error) {
+ console.error("Error update maskot:", error);
+ toast.error("Gagal update maskot");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // Loading state
+ if (maskotState.findUnique.loading || maskotState.update.loading) {
+ return (
+
+
+ Memuat data...
+
+
+ );
+ }
+
+ // Error state
+ if (maskotState.findUnique.error) {
+ return (
+
+
+
+
+
+ } color="red">
+ Error
+ {maskotState.findUnique.error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Edit Maskot Desa
+
+
+
+
+ Edit Maskot Desa
+
+ {/* Judul */}
+ Judul}
+ placeholder="Masukkan judul maskot"
+ defaultValue={formData.judul}
+ onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })}
+ error={!formData.judul && "Judul wajib diisi"}
+ />
+
+ {/* Deskripsi */}
+
+ Deskripsi
+ setFormData({ ...formData, deskripsi: val })}
+ />
+
+
+ {/* Upload Gambar */}
+
+ Gambar
+ {
+ const newImages = files.map((file) => ({
+ file,
+ preview: URL.createObjectURL(file),
+ label: '',
+ }));
+ setImages((prev) => [...prev, ...newImages]);
+ }}
+ >
+
+
+
+
+
+ Drag images here or click to select files
+ Attach as many files as you like, each file max 5mb
+
+
+
+
+
+ {/* Preview Gambar */}
+
+ {images.map((img, index) => (
+
+
+
+
+ {
+ const updated = [...images];
+ updated.splice(index, 1);
+ setImages(updated);
+ }}
+ >
+ Hapus
+
+
+
+ {
+ const updated = [...images];
+ updated[index].label = e.currentTarget.value;
+ setImages(updated);
+ }}
+ />
+
+
+
+ ))}
+
+
+ {/* Buttons */}
+
+
+ {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
+
+
+ Batal
+
+
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx
new file mode 100644
index 00000000..c941dd4f
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function Page() {
+ const sejarahState = useProxy(stateProfileDesa.sejarahDesa)
+ const router = useRouter()
+ const params = useParams()
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Load data
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error("ID tidak valid");
+ router.push("/admin/desa/profile/profile-desa");
+ return;
+ }
+
+ try {
+ const data = await sejarahState.findUnique.load(id);
+ if (data) {
+ sejarahState.update.initialize(data);
+ }
+ } catch (error) {
+ console.error("Error loading sejarah:", error);
+ toast.error("Gagal memuat data sejarah desa");
+ }
+ };
+
+ loadData();
+
+ return () => {
+ sejarahState.update.reset();
+ sejarahState.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ const handleSubmit = async () => {
+ if (isSubmitting || !sejarahState.update.form.judul.trim()) {
+ toast.error("Judul wajib diisi");
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const success = await sejarahState.update.submit();
+ if (success) {
+ toast.success("Data berhasil disimpan");
+ router.push("/admin/desa/profile/profile-desa");
+ }
+ } catch (error) {
+ console.error("Error update sejarah desa:", error);
+ toast.error("Terjadi kesalahan saat update sejarah desa");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleBack = () => router.back();
+
+ // Loading state
+ if (sejarahState.findUnique.loading || sejarahState.update.loading) {
+ return (
+
+
+ Memuat data...
+
+
+ );
+ }
+
+ // Error state
+ if (sejarahState.findUnique.error) {
+ return (
+
+
+
+
+
+ } color="red">
+ Error
+ {sejarahState.findUnique.error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Edit Sejarah Desa
+
+
+
+
+ Edit Sejarah Desa
+
+ {/* Judul */}
+ Judul}
+ placeholder="Judul sejarah"
+ defaultValue={sejarahState.update.form.judul}
+ onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
+ error={!sejarahState.update.form.judul && "Judul wajib diisi"}
+ />
+
+ {/* Deskripsi */}
+
+ Deskripsi
+ sejarahState.update.form.deskripsi = val}
+ />
+
+
+ {/* Buttons */}
+
+
+ {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
+
+
+
+ Batal
+
+
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx
new file mode 100644
index 00000000..0a06bc88
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function Page() {
+ const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa)
+ const router = useRouter()
+ const params = useParams()
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Load data
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error("ID tidak valid");
+ router.push("/admin/desa/profile/profile-desa");
+ return;
+ }
+
+ try {
+ const data = await visiMisiState.findUnique.load(id);
+ visiMisiState.update.initialize(data);
+ } catch (error) {
+ console.error("Error loading visi misi:", error);
+ toast.error("Gagal memuat data visi misi desa");
+ }
+ };
+
+ loadData();
+
+ return () => {
+ visiMisiState.update.reset();
+ visiMisiState.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ const handleSubmit = async () => {
+ if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
+ toast.error("Visi wajib diisi");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const success = await visiMisiState.update.submit();
+
+ if (success) {
+ toast.success("Data berhasil disimpan");
+ router.push("/admin/desa/profile/profile-desa");
+ }
+ } catch (error) {
+ console.error("Error update visi misi desa:", error);
+ toast.error("Terjadi kesalahan saat update visi misi desa");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleBack = () => router.back();
+
+ // Loading state
+ if (visiMisiState.findUnique.loading || visiMisiState.update.loading) {
+ return (
+
+
+ Memuat data...
+
+
+ );
+ }
+
+ // Error state
+ if (visiMisiState.findUnique.error) {
+ return (
+
+
+
+
+
+ } color="red">
+ Error
+ {visiMisiState.findUnique.error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Edit Visi Misi Desa
+
+
+
+
+ Edit Visi Misi Desa
+
+ {/* Visi */}
+
+ Visi
+ visiMisiState.update.form.visi = val}
+ />
+
+
+ {/* Misi */}
+
+ Misi
+ visiMisiState.update.form.misi = val}
+ />
+
+
+ {/* Buttons */}
+
+
+ {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
+
+
+
+ Batal
+
+
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx
new file mode 100644
index 00000000..0dee1b80
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx
@@ -0,0 +1,248 @@
+'use client'
+
+import colors from '@/con/colors';
+import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { useSnapshot } from 'valtio';
+import stateProfileDesa from '../../../_state/desa/profile';
+import { useEffect } from 'react';
+import { IconEdit } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+
+function Page() {
+ const router = useRouter();
+ const snap = useSnapshot(stateProfileDesa);
+
+ useEffect(() => {
+ stateProfileDesa.sejarahDesa.findUnique.load("edit");
+ stateProfileDesa.visiMisiDesa.findUnique.load("edit");
+ stateProfileDesa.lambangDesa.findUnique.load("edit");
+ stateProfileDesa.maskotDesa.findUnique.load("edit");
+ }, []);
+
+ const sejarah = snap.sejarahDesa.findUnique.data;
+ const visiMisi = snap.visiMisiDesa.findUnique.data;
+ const lambang = snap.lambangDesa.findUnique.data;
+ const maskot = snap.maskotDesa.findUnique.data;
+
+ return (
+
+
+ Preview Profile Desa
+
+ {/* Sejarah Desa */}
+ {sejarah && (
+
+
+
+ Preview Sejarah Desa
+
+
+
+ }
+ radius="md"
+ onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
+ >
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {sejarah.judul}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Visi Misi Desa */}
+ {visiMisi && (
+
+
+
+ Preview Visi Misi Desa
+
+
+
+ }
+ radius="md"
+ onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
+ >
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Visi Misi Desa
+
+
+
+
+ Visi Desa
+
+ Misi Desa
+
+
+
+
+ )}
+
+ {/* Lambang Desa */}
+ {lambang && (
+
+
+
+ Preview Lambang Desa
+
+
+
+ }
+ radius="md"
+ onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
+ >
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {lambang.judul}
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Maskot Desa */}
+ {maskot && (
+
+
+
+ Preview Maskot Desa
+
+
+
+ }
+ radius="md"
+ onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
+ >
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Maskot Desa
+
+
+
+
+
+
+
+ {maskot.images.map((img, idx) => (
+
+
+
+
+ {img.label}
+
+ ))}
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx
new file mode 100644
index 00000000..56f2c2e2
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx
@@ -0,0 +1,219 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPerbekelDariMasaKeMasa() {
+ const state = useProxy(stateProfileDesa.mantanPerbekel);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ nama: '',
+ daerah: '',
+ periode: '',
+ imageId: ''
+ });
+
+ // load data pertama kali
+ useEffect(() => {
+ const loadFoto = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+ try {
+ const data = await state.update.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || '',
+ daerah: data.daerah || '',
+ periode: data.periode || '',
+ imageId: data.imageId || ''
+ });
+ if (data?.imageGalleryFoto?.link) {
+ setPreviewImage(data.imageGalleryFoto.link);
+ }
+ }
+ } catch (error) {
+ console.error('Error loading foto:', error);
+ toast.error('Gagal memuat data foto');
+ }
+ };
+ loadFoto();
+ }, [params?.id]);
+
+ // helper ubah state formData
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // update global state hanya sekali pas submit
+ state.update.form = { ...state.update.form, ...formData };
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error("Gagal upload gambar");
+ state.update.form.imageId = uploaded.id;
+ }
+
+ await state.update.update();
+ toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
+ router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
+ } catch (error) {
+ console.error('Error updating perbekel dari masa ke masa:', error);
+ toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Perbekel Dari Masa Ke Masa
+
+
+
+
+
+ handleChange('nama', e.target.value)}
+ required
+ />
+
+
+
+ Foto Perbekel
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ handleChange('daerah', e.target.value)}
+ required
+ />
+
+ handleChange('periode', e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPerbekelDariMasaKeMasa;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx
new file mode 100644
index 00000000..048d8494
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx
@@ -0,0 +1,143 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailPerbekelDariMasa() {
+ const state = useProxy(stateProfileDesa.mantanPerbekel);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Perbekel Dari Masa Ke Masa
+
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+ Nama Perbekel
+ {data.nama || '-'}
+
+
+
+ Daerah
+ {data.daerah || '-'}
+
+
+
+ Periode
+ {data.periode || '-'}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus perbekel dari masa ke masa ini?"
+ />
+
+ );
+}
+
+export default DetailPerbekelDariMasa;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx
new file mode 100644
index 00000000..d2c5ca5e
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx
@@ -0,0 +1,161 @@
+'use client';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreatePerbekelDariMasaKeMasa() {
+ const state = useProxy(stateProfileDesa.mantanPerbekel);
+ const router = useRouter();
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const resetForm = () => {
+ state.create.form = {
+ nama: '',
+ daerah: '',
+ periode: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+
+ state.create.form.imageId = uploaded.id;
+ await state.create.create();
+ resetForm();
+ router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
+ };
+
+ return (
+
+ {/* Back button + Title */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Create Perbekel Dari Masa Ke Masa
+
+
+
+
+
+ (state.create.form.nama = e.target.value)}
+ required
+ />
+ (state.create.form.daerah = e.target.value)}
+ required
+ />
+ (state.create.form.periode = e.target.value)}
+ required
+ />
+
+ {/* Dropzone */}
+
+ Gambar Perbekel
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePerbekelDariMasaKeMasa;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx
new file mode 100644
index 00000000..6957d8f5
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx
@@ -0,0 +1,135 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 stateProfileDesa from '../../../_state/desa/profile';
+
+function PerbekelDariMasaKeMasa() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
+ const state = useProxy(stateProfileDesa.mantanPerbekel)
+ const router = useRouter();
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ List Perbekel Dari Masa Ke Masa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Perbekel
+ Periode
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+
+ {item.periode}
+
+
+
+
+ }
+ onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
+ >
+ Detail
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data perbekel yang cocok
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PerbekelDariMasaKeMasa;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx
new file mode 100644
index 00000000..43eefc8a
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx
@@ -0,0 +1,209 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function ProfilePerbekel() {
+ const perbekelState = useProxy(stateProfileDesa.profilPerbekel)
+ const router = useRouter()
+ const params = useParams()
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error("ID tidak valid");
+ router.push("/admin/desa/profile/profile-perbekel");
+ return;
+ }
+
+ const data = await perbekelState.findUnique.load(id);
+ if (data) perbekelState.edit.initialize(data);
+ if (data?.image?.link) setPreviewImage(data.image.link);
+ };
+
+ loadData();
+
+ return () => {
+ perbekelState.edit.reset();
+ perbekelState.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ const handleFileChange = (newFile: File | null) => {
+ if (!newFile) {
+ setFile(null);
+ setPreviewImage(null);
+ return;
+ }
+ setFile(newFile);
+ const reader = new FileReader();
+ reader.onload = (event) => setPreviewImage(event.target?.result as string);
+ reader.readAsDataURL(newFile);
+ }
+
+ const handleSubmit = async () => {
+ if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
+ toast.error("Biodata wajib diisi");
+ return;
+ }
+ setIsSubmitting(true)
+ try {
+ if (file) {
+ const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = uploadResponse.data?.data;
+
+ if (!uploaded?.id) {
+ toast.error("Gagal upload gambar");
+ return;
+ }
+ perbekelState.edit.form.imageId = uploaded.id;
+ }
+ const success = await perbekelState.edit.submit()
+ if (success) {
+ toast.success("Data berhasil disimpan");
+ router.push("/admin/desa/profile/profile-perbekel");
+ }
+ } catch (error) {
+ console.error("Error update sejarah desa:", error);
+ toast.error("Terjadi kesalahan saat update sejarah desa");
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ const handleBack = () => router.back();
+
+ if (perbekelState.findUnique.loading || perbekelState.edit.loading) {
+ return (
+
+
+ Memuat data...
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+ Edit Profil Perbekel
+
+
+
+
+
+ {/* Biodata */}
+
+ Biodata
+ perbekelState.edit.form.biodata = val}
+ />
+
+
+ {/* Gambar */}
+
+ Gambar
+ handleFileChange(files[0])}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2} // 5MB
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+ {/* Preview */}
+
+ {previewImage ? (
+
+ ) : (
+
+
+
+ Tidak ada gambar
+
+
+ )}
+
+
+
+ {/* Pengalaman */}
+
+ Pengalaman
+ perbekelState.edit.form.pengalaman = val}
+ />
+
+
+ {/* Pengalaman Organisasi */}
+
+ Pengalaman Organisasi
+ perbekelState.edit.form.pengalamanOrganisasi = val}
+ />
+
+
+ {/* Program Unggulan */}
+
+ Program Unggulan
+ perbekelState.edit.form.programUnggulan = val}
+ />
+
+
+ {/* Submit */}
+
+
+ {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
+
+
+
+ Batal
+
+
+
+
+
+
+ )
+}
+
+export default ProfilePerbekel;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx
new file mode 100644
index 00000000..73da958e
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx
@@ -0,0 +1,117 @@
+'use client'
+
+import colors from '@/con/colors';
+import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { IconEdit } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useSnapshot } from 'valtio';
+import stateProfileDesa from '../../../_state/desa/profile';
+
+function Page() {
+ const router = useRouter();
+ const snap = useSnapshot(stateProfileDesa);
+
+ // Load data saat mount
+ useEffect(() => {
+ stateProfileDesa.profilPerbekel.findUnique.load("edit");
+ }, []);
+
+ const perbekel = snap.profilPerbekel.findUnique.data;
+
+ if (!perbekel) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header + tombol edit */}
+
+
+ Preview Profil PPID
+
+
+
+ }
+ radius="md"
+ onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
+ >
+ Edit
+
+
+
+
+
+ {/* Card Profil */}
+
+
+
+
+
+
+
+
+
+
+ Profil Pimpinan Badan Publik Desa Darmasaba
+
+
+
+
+
+
+
+
+ { e.currentTarget.src = "/perbekel.png"; }}
+ loading='lazy'
+ />
+
+
+
+ I.B. Surya Prabhawa Manuaba, S.H., M.H.
+
+
+
+
+ {/* Biodata & Info */}
+
+ Biodata
+
+
+ Pengalaman
+
+
+ Pengalaman Organisasi
+
+
+ Program Kerja Unggulan
+
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..dca2dfe2
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx
@@ -0,0 +1,150 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Stack,
+ Tabs,
+ TabsList,
+ TabsPanel,
+ TabsTab,
+ Title,
+ Tooltip,
+ ScrollArea,
+} from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import {
+ IconFileAnalytics,
+ IconCoins,
+ IconShoppingCart,
+ IconWallet,
+} from '@tabler/icons-react';
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "APB Desa",
+ value: "apbdesa",
+ href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa",
+ icon: ,
+ tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa",
+ },
+ {
+ label: "Pendapatan",
+ value: "pendapatan",
+ href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan",
+ icon: ,
+ tooltip: "Kelola data pendapatan desa",
+ },
+ {
+ label: "Belanja",
+ value: "belanja",
+ href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja",
+ icon: ,
+ tooltip: "Atur data belanja desa",
+ },
+ {
+ label: "Pembiayaan",
+ value: "pembiayaan",
+ href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan",
+ icon: ,
+ tooltip: "Kelola data pembiayaan desa",
+ },
+ ];
+
+ const currentTab = tabs.find((tab) => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(
+ 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 (
+
+
+ Pendapatan Asli Desa
+
+
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
new file mode 100644
index 00000000..956f84e1
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
@@ -0,0 +1,267 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ MultiSelect,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+ Group,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditAPBDesa() {
+ const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ tahun: '',
+ pendapatanIds: [] as string[],
+ belanjaIds: [] as string[],
+ pembiayaanIds: [] as string[],
+ });
+
+ // Load APB desa by id → hanya update formData, bukan global state
+ useEffect(() => {
+ const loadAPBdesa = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await apbState.update.load(id);
+ if (data) {
+ setFormData({
+ tahun: String(data.tahun || ''),
+ pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
+ belanjaIds: data.belanja?.map((b: any) => b.id) || [],
+ pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
+ });
+ }
+ } catch (error) {
+ console.error("Error loading APBdesa:", error);
+ toast.error("Gagal memuat data APBdesa");
+ }
+ };
+
+ loadAPBdesa();
+ }, [params?.id]);
+
+ const handleChange = (field: keyof typeof formData, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // update global state cuma pas submit
+ apbState.update.form = {
+ ...apbState.update.form,
+ tahun: Number(formData.tahun),
+ pendapatanIds: formData.pendapatanIds,
+ belanjaIds: formData.belanjaIds,
+ pembiayaanIds: formData.pembiayaanIds,
+ };
+
+ await apbState.update.update();
+ toast.success("APB Desa berhasil diperbarui!");
+ router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa");
+ } catch (error) {
+ console.error("Error updating APBdesa:", error);
+ toast.error("Terjadi kesalahan saat memperbarui APBdesa");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit APB Desa
+
+
+
+ {/* Form Card */}
+
+
+ {/* Tahun */}
+ handleChange("tahun", e.target.value)}
+ label={Tahun }
+ placeholder="Masukkan tahun anggaran"
+ required
+ />
+
+ {/* Selects */}
+ handleChange("pendapatanIds", ids)}
+ />
+
+ handleChange("belanjaIds", ids)}
+ />
+
+ handleChange("pembiayaanIds", ids)}
+ />
+
+ {/* Save Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+
+ /* --- Sub Components --- */
+
+ function SelectPendapatan({
+ selectedIds,
+ onSelectionChange,
+ }: {
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+ }) {
+ const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
+
+ useShallowEffect(() => {
+ pendapatanState.findMany.load();
+ }, []);
+
+ if (!pendapatanState.findMany.data) {
+ return ;
+ }
+
+ return (
+ Pendapatan}
+ data={pendapatanState.findMany.data.map((p: any) => ({
+ value: p.id,
+ label: p.name,
+ }))}
+ value={selectedIds}
+ onChange={onSelectionChange}
+ searchable
+ clearable
+ placeholder="Pilih pendapatan..."
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
+ }
+
+ function SelectBelanja({
+ selectedIds,
+ onSelectionChange,
+ }: {
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+ }) {
+ const belanjaState = useProxy(PendapatanAsliDesa.belanja);
+
+ useShallowEffect(() => {
+ belanjaState.findMany.load();
+ }, []);
+
+ if (!belanjaState.findMany.data) {
+ return ;
+ }
+
+ return (
+ Belanja}
+ data={belanjaState.findMany.data.map((b: any) => ({
+ value: b.id,
+ label: b.name,
+ }))}
+ value={selectedIds}
+ onChange={onSelectionChange}
+ searchable
+ clearable
+ placeholder="Pilih belanja..."
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
+ }
+
+ function SelectPembiayaan({
+ selectedIds,
+ onSelectionChange,
+ }: {
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+ }) {
+ const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
+
+ useShallowEffect(() => {
+ pembiayaanState.findMany.load();
+ }, []);
+
+ if (!pembiayaanState.findMany.data) {
+ return ;
+ }
+
+ return (
+ Pembiayaan}
+ data={pembiayaanState.findMany.data.map((p: any) => ({
+ value: p.id,
+ label: p.name,
+ }))}
+ value={selectedIds}
+ onChange={onSelectionChange}
+ searchable
+ clearable
+ placeholder="Pilih pembiayaan..."
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
+ }
+}
+
+export default EditAPBDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx
new file mode 100644
index 00000000..ee57dc50
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx
@@ -0,0 +1,208 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailAPBDesa() {
+ const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ apbState.findUnique.load(params?.id as string);
+ }, []);
+
+ const formatRupiah = (value: number) =>
+ new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(value);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ apbState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push(
+ '/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'
+ );
+ }
+ };
+
+ if (!apbState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = apbState.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail APB Desa
+
+
+
+
+
+
+ Tahun
+
+
+ {data.tahun}
+
+
+
+
+
+
+ Detail Pembiayaan
+
+ {(data?.pembiayaan || []).map((item) => (
+
+ {item.name}: {formatRupiah(Number(item.value))}
+
+ ))}
+
+ Total:{' '}
+ {formatRupiah(
+ (data?.pembiayaan || []).reduce(
+ (sum, item) => sum + Number(item.value),
+ 0
+ )
+ )}
+
+
+
+
+
+
+
+ Detail Belanja
+
+ {(data?.belanja || []).map((item) => (
+
+ {item.name}: {formatRupiah(Number(item.value))}
+
+ ))}
+
+ Total:{' '}
+ {formatRupiah(
+ (data?.belanja || []).reduce(
+ (sum, item) => sum + Number(item.value),
+ 0
+ )
+ )}
+
+
+
+
+
+
+
+ Detail Pendapatan
+
+ {(data?.pendapatan || []).map((item) => (
+
+ {item.name}: {formatRupiah(Number(item.value))}
+
+ ))}
+
+ Total:{' '}
+ {formatRupiah(
+ (data?.pendapatan || []).reduce(
+ (sum, item) => sum + Number(item.value),
+ 0
+ )
+ )}
+
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus APB Desa ini?"
+ />
+
+ );
+}
+
+export default DetailAPBDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
new file mode 100644
index 00000000..797cc92f
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
@@ -0,0 +1,218 @@
+'use client';
+
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ MultiSelect,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreateAPBDesa() {
+ const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
+ const router = useRouter();
+
+ const resetForm = () => {
+ apbDesaState.create.form = {
+ tahun: 0,
+ pendapatanIds: [],
+ belanjaIds: [],
+ pembiayaanIds: [],
+ };
+ };
+
+ const handleSubmit = async () => {
+ await apbDesaState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah APB Desa
+
+
+
+ {/* Form */}
+
+
+ {
+ apbDesaState.create.form.tahun = Number(val.target.value);
+ }}
+ label={Tahun }
+ placeholder="Masukkan tahun anggaran"
+ required
+ />
+
+ {
+ apbDesaState.create.form.pendapatanIds = ids;
+ }}
+ />
+
+ {
+ apbDesaState.create.form.belanjaIds = ids;
+ }}
+ />
+
+ {
+ apbDesaState.create.form.pembiayaanIds = ids;
+ }}
+ />
+
+ {/* Action */}
+
+
+ Simpan
+
+
+
+
+
+ );
+
+ /* ---------- Select Pendapatan ---------- */
+ interface SelectPendapatanProps {
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+ }
+ function SelectPendapatan({ selectedIds = [], onSelectionChange }: SelectPendapatanProps) {
+ const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
+
+ useShallowEffect(() => {
+ pendapatanState.findMany.load();
+ }, []);
+
+ if (!pendapatanState.findMany.data) {
+ return ;
+ }
+
+ return (
+ Pendapatan}
+ data={pendapatanState.findMany.data.map((p) => ({
+ value: p.id,
+ label: p.name,
+ }))}
+ value={selectedIds}
+ onChange={onSelectionChange}
+ searchable
+ clearable
+ placeholder="Pilih pendapatan..."
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
+ }
+
+ /* ---------- Select Belanja ---------- */
+ interface SelectBelanjaProps {
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+ }
+ function SelectBelanja({ selectedIds = [], onSelectionChange }: SelectBelanjaProps) {
+ const belanjaState = useProxy(PendapatanAsliDesa.belanja);
+
+ useShallowEffect(() => {
+ belanjaState.findMany.load();
+ }, []);
+
+ if (!belanjaState.findMany.data) {
+ return ;
+ }
+
+ return (
+ Belanja}
+ data={belanjaState.findMany.data.map((b) => ({
+ value: b.id,
+ label: b.name,
+ }))}
+ value={selectedIds}
+ onChange={onSelectionChange}
+ searchable
+ clearable
+ placeholder="Pilih belanja..."
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
+ }
+
+ /* ---------- Select Pembiayaan ---------- */
+ interface SelectPembiayaanProps {
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+ }
+ function SelectPembiayaan({ selectedIds = [], onSelectionChange }: SelectPembiayaanProps) {
+ const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
+
+ useShallowEffect(() => {
+ pembiayaanState.findMany.load();
+ }, []);
+
+ if (!pembiayaanState.findMany.data) {
+ return ;
+ }
+
+ return (
+ Pembiayaan}
+ data={pembiayaanState.findMany.data.map((b) => ({
+ value: b.id,
+ label: b.name,
+ }))}
+ value={selectedIds}
+ onChange={onSelectionChange}
+ searchable
+ clearable
+ placeholder="Pilih pembiayaan..."
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
+ }
+}
+
+export default CreateAPBDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx
new file mode 100644
index 00000000..ff0c6464
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx
@@ -0,0 +1,191 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Pagination,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
+
+function APBDesa() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListAPBDesa({ search }: { search: string }) {
+ const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = apbDesaState.findMany;
+
+ const formatRupiah = (value: number) =>
+ new Intl.NumberFormat("id-ID", {
+ style: "currency",
+ currency: "IDR",
+ minimumFractionDigits: 0,
+ }).format(value);
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ List APB Desa
+
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create"
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Tahun
+ Pembiayaan
+ Belanja
+ Pendapatan
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+ {item.tahun}
+
+ {formatRupiah(
+ item.pembiayaan.reduce(
+ (sum, val) => sum + Number(val.value),
+ 0
+ )
+ )}
+
+
+ {formatRupiah(
+ item.belanja.reduce(
+ (sum, val) => sum + Number(val.value),
+ 0
+ )
+ )}
+
+
+ {formatRupiah(
+ item.pendapatan.reduce(
+ (sum, val) => sum + Number(val.value),
+ 0
+ )
+ )}
+
+
+
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data APB Desa yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: "smooth" });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default APBDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
new file mode 100644
index 00000000..7c03d4a8
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
@@ -0,0 +1,160 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditBelanja() {
+ const belanjaState = useProxy(PendapatanAsliDesa.belanja);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ value: '',
+ });
+
+ // format angka ke rupiah
+ const formatRupiah = (value: number | string) => {
+ const number =
+ typeof value === 'number'
+ ? value
+ : Number(value.replace(/\D/g, '')) || 0;
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(number);
+ };
+
+ // buang semua simbol jadi angka murni
+ const unformatRupiah = (value: string) => {
+ return Number(value.replace(/\D/g, '')) || 0;
+ };
+
+ useEffect(() => {
+ const loadBelanja = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await belanjaState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ value: String(data.value || ''),
+ });
+ }
+ } catch (error) {
+ console.error("Error loading belanja:", error);
+ toast.error("Gagal memuat data belanja");
+ }
+ };
+
+ loadBelanja();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ belanjaState.update.form = {
+ ...belanjaState.update.form,
+ name: formData.name,
+ value: Number(formData.value),
+ };
+
+ await belanjaState.update.update();
+ toast.success("Jenis Belanja berhasil diperbarui!");
+ router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja");
+ } catch (error) {
+ console.error("Error updating jenis belanja:", error);
+ toast.error("Terjadi kesalahan saat memperbarui jenis belanja");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Jenis Belanja
+
+
+
+ {/* Card */}
+
+
+
+ setFormData({ ...formData, name: e.target.value })
+ }
+ required
+ />
+
+ {
+ const raw = e.currentTarget.value;
+ const cleanValue = unformatRupiah(raw);
+ setFormData({ ...formData, value: String(cleanValue) });
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditBelanja;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
new file mode 100644
index 00000000..7cc80cf9
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
@@ -0,0 +1,125 @@
+'use client';
+
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import { toast } from 'react-toastify';
+
+function CreateBelanja() {
+ const belanjaState = useProxy(PendapatanAsliDesa.belanja);
+ const router = useRouter();
+
+ const formatRupiah = (value: number | string) => {
+ const number =
+ typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(number);
+ };
+
+ const unformatRupiah = (value: string) => {
+ return Number(value.replace(/\D/g, ''));
+ };
+
+ const resetForm = () => {
+ belanjaState.create.form = {
+ name: '',
+ value: 0,
+ };
+ };
+
+ const handleSubmit = async () => {
+ if (!belanjaState.create.form.name || !belanjaState.create.form.value) {
+ return toast.warn('Lengkapi semua field terlebih dahulu');
+ }
+
+ await belanjaState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
+ };
+
+ return (
+
+ {/* Header dengan back button */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Jenis Belanja
+
+
+
+ {/* Card Form */}
+
+
+ Nama Jenis Belanja}
+ placeholder="Masukkan nama jenis belanja"
+ defaultValue={belanjaState.create.form.name}
+ onChange={(e) => (belanjaState.create.form.name = e.target.value)}
+ required
+ />
+
+ Nilai}
+ placeholder="Masukkan nilai belanja"
+ defaultValue={formatRupiah(belanjaState.create.form.value)}
+ onChange={(e) => {
+ const raw = e.currentTarget.value;
+ belanjaState.create.form.value = unformatRupiah(raw);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateBelanja;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx
new file mode 100644
index 00000000..467c8142
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx
@@ -0,0 +1,223 @@
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
+
+function Belanja() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListBelanja({ search }: { search: string }) {
+ const belanjaState = useProxy(PendapatanAsliDesa.belanja);
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const {
+ data,
+ loading,
+ load,
+ page,
+ totalPages,
+ } = belanjaState.findMany;
+
+ const formatRupiah = (value: number) =>
+ new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(value);
+
+ const totalBelanja = data?.reduce((sum, item) => sum + item.value, 0) || 0;
+
+ const handleDelete = () => {
+ if (selectedId) {
+ belanjaState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ load(page, 10, search);
+ }
+ };
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Belanja
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama
+ Nilai
+ Persentase
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ <>
+ {filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+ {formatRupiah(item.value)}
+
+ {totalBelanja > 0
+ ? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
+ : '0%'}
+
+
+
+
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
+ )
+ }
+ >
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+
+ ))}
+
+
+ Total
+
+
+ {formatRupiah(totalBelanja)}
+
+
+ >
+ ) : (
+
+
+
+ Tidak ada data belanja yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus belanja ini?"
+ />
+
+ );
+}
+
+export default Belanja;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx
new file mode 100644
index 00000000..3a414c77
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import LayoutTabs from './_lib/layoutTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
new file mode 100644
index 00000000..25df9c5b
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
@@ -0,0 +1,157 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPembiayaan() {
+ const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ value: '',
+ });
+
+ const formatRupiah = (value: number | string) => {
+ const number =
+ typeof value === 'number'
+ ? value
+ : Number(value.toString().replace(/\D/g, '')) || 0;
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(number);
+ };
+
+ const unformatRupiah = (value: string) => {
+ return Number(value.replace(/\D/g, '')) || 0;
+ };
+
+ useEffect(() => {
+ const loadPembiayaan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await pembiayaanState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ value: String(data.value || ''),
+ });
+ }
+ } catch (error) {
+ console.error('Error loading pembiayaan:', error);
+ toast.error('Gagal memuat data pembiayaan');
+ }
+ };
+
+ loadPembiayaan();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ pembiayaanState.update.form = {
+ ...pembiayaanState.update.form,
+ name: formData.name,
+ value: unformatRupiah(formData.value),
+ };
+
+ await pembiayaanState.update.update();
+ toast.success('Jenis Pembiayaan berhasil diperbarui!');
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
+ } catch (error) {
+ console.error('Error updating jenis pembiayaan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Jenis Pembiayaan
+
+
+
+ {/* Card Form */}
+
+
+
+ setFormData((prev) => ({ ...prev, name: e.target.value }))
+ }
+ required
+ />
+
+ {
+ const raw = e.currentTarget.value;
+ const cleanValue = unformatRupiah(raw);
+ setFormData((prev) => ({ ...prev, value: String(cleanValue) }));
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPembiayaan;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
new file mode 100644
index 00000000..a432a431
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
@@ -0,0 +1,127 @@
+'use client';
+import React from 'react';
+import { useProxy } from 'valtio/utils';
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import { useRouter } from 'next/navigation';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Title,
+ TextInput,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { toast } from 'react-toastify';
+
+function CreatePembiayaan() {
+ const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
+ const router = useRouter();
+
+ const formatRupiah = (value: number | string) => {
+ const number =
+ typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(number);
+ };
+
+ const unformatRupiah = (value: string) => {
+ return Number(value.replace(/\D/g, ''));
+ };
+
+ const resetForm = () => {
+ pembiayaanState.create.form = {
+ name: '',
+ value: 0,
+ };
+ };
+
+ const handleSubmit = async () => {
+ if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
+ return toast.warn('Nama dan nilai wajib diisi');
+ }
+
+ await pembiayaanState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Jenis Pembiayaan
+
+
+
+ {/* Form Card */}
+
+
+ Nama Jenis Pembiayaan}
+ placeholder="Masukkan nama jenis pembiayaan"
+ defaultValue={pembiayaanState.create.form.name}
+ onChange={(e) => {
+ pembiayaanState.create.form.name = e.currentTarget.value;
+ }}
+ required
+ />
+
+ Nilai}
+ placeholder="Masukkan nilai"
+ defaultValue={formatRupiah(pembiayaanState.create.form.value)}
+ onChange={(e) => {
+ const raw = e.currentTarget.value;
+ pembiayaanState.create.form.value = unformatRupiah(raw);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePembiayaan;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx
new file mode 100644
index 00000000..627f5894
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx
@@ -0,0 +1,215 @@
+'use client'
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+ Pagination,
+} from '@mantine/core';
+import React, { useState } from 'react';
+import HeaderSearch from '../../../_com/header';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
+import { useProxy } from 'valtio/utils';
+import { useRouter } from 'next/navigation';
+import { useShallowEffect } from '@mantine/hooks';
+import colors from '@/con/colors';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function Pembiayaan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPembiayaan({ search }: { search: string }) {
+ const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = pembiayaanState.findMany;
+
+ const formatRupiah = (value: number) =>
+ new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(value);
+
+ const totalPembiayaan = (data || []).reduce((sum, item) => sum + item.value, 0);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ pembiayaanState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ load(page, 10, search);
+ }
+ };
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ const filteredData = data || [];
+
+ return (
+
+
+
+ Daftar Pembiayaan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama
+ Nilai
+ Persentase
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ <>
+ {filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+ {formatRupiah(item.value)}
+
+ {totalPembiayaan > 0
+ ? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
+ : '0%'}
+
+
+
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`
+ )
+ }
+ >
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+ ))}
+ {/* Total Row */}
+
+
+ Total
+
+ {formatRupiah(totalPembiayaan)}
+
+ >
+ ) : (
+
+
+
+ Tidak ada data pembiayaan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus pembiayaan ini?"
+ />
+
+ );
+}
+
+export default Pembiayaan;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
new file mode 100644
index 00000000..cf8e65f0
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
@@ -0,0 +1,162 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPendapatan() {
+ const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ value: '',
+ });
+
+ // helper format
+ const formatRupiah = (value: number | string) => {
+ const number = typeof value === 'number'
+ ? value
+ : Number(value.toString().replace(/\D/g, ''));
+
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(number);
+ };
+
+ const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
+
+ // load data once
+ useEffect(() => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ const loadPendapatan = async () => {
+ try {
+ const data = await pendapatanState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name ?? '',
+ value: data.value?.toString() ?? '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading pendapatan:', error);
+ toast.error('Gagal memuat data pendapatan');
+ }
+ };
+
+ loadPendapatan();
+ }, [params?.id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ pendapatanState.update.form = {
+ ...pendapatanState.update.form,
+ name: formData.name,
+ value: Number(formData.value),
+ };
+
+ await pendapatanState.update.update();
+ toast.success('Jenis Pendapatan berhasil diperbarui!');
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
+ } catch (error) {
+ console.error('Error updating jenis pendapatan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan');
+ }
+ };
+
+ return (
+
+ {/* Header with Back Button */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Jenis Pendapatan
+
+
+
+ {/* Card Form */}
+
+
+ handleChange('name', e.target.value)}
+ required
+ />
+
+ {
+ const raw = e.currentTarget.value;
+ const cleanValue = unformatRupiah(raw).toString();
+ handleChange('value', cleanValue);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPendapatan;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
new file mode 100644
index 00000000..663ae35b
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
@@ -0,0 +1,120 @@
+'use client';
+import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreatePendapatan() {
+ const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
+ const router = useRouter();
+
+ const formatRupiah = (value: number | string) => {
+ const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(number);
+ };
+
+ const unformatRupiah = (value: string) => {
+ return Number(value.replace(/\D/g, ''));
+ };
+
+ const resetForm = () => {
+ pendapatanState.create.form = {
+ name: '',
+ value: 0,
+ };
+ };
+
+ const handleSubmit = async () => {
+ await pendapatanState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
+ };
+
+ return (
+
+ {/* Header dengan tombol back + judul */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Jenis Pendapatan
+
+
+
+ {/* Card Form */}
+
+
+ {
+ pendapatanState.create.form.name = val.target.value;
+ }}
+ label="Nama Jenis Pendapatan"
+ placeholder="Masukkan nama jenis pendapatan"
+ required
+ />
+
+ {
+ const raw = val.currentTarget.value;
+ const cleanValue = unformatRupiah(raw);
+ pendapatanState.create.form.value = cleanValue;
+ }}
+ label="Nilai"
+ placeholder="Masukkan nilai"
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePendapatan;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx
new file mode 100644
index 00000000..47ba7b86
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx
@@ -0,0 +1,214 @@
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
+
+function Pendapatan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPendapatan({ search }: { search: string }) {
+ const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = pendapatanState.findMany;
+
+ const formatRupiah = (value: number) =>
+ new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(value);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ pendapatanState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ load(page, 10, search);
+ }
+ };
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ const totalValue = filteredData.reduce((total, item) => total + item.value, 0);
+
+ return (
+
+
+
+ Daftar Pendapatan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama
+ Nilai
+ Edit
+ Delete
+
+
+
+ {filteredData.length > 0 ? (
+ <>
+ {filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+ {formatRupiah(item.value)}
+
+
+ router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
+ }
+ >
+
+ Edit
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+ Hapus
+
+
+
+ ))}
+
+ {/* Row total */}
+
+
+ Total
+
+
+ {formatRupiah(totalValue)}
+
+
+ >
+ ) : (
+
+
+
+ Tidak ada data pendapatan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus pendapatan ini?"
+ />
+
+ );
+}
+
+export default Pendapatan;
diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
new file mode 100644
index 00000000..016cb96b
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
@@ -0,0 +1,168 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, useCallback } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
+
+interface FormData {
+ pekerjaan: string;
+ lakiLaki: number;
+ perempuan: number;
+}
+
+export default function EditDemografiPekerjaan() {
+ const router = useRouter();
+ const { id } = useParams() as { id: string };
+ const stateDemografi = useProxy(demografiPekerjaan);
+
+ const [formData, setFormData] = useState({
+ pekerjaan: '',
+ lakiLaki: 0,
+ perempuan: 0,
+ });
+
+ // ✅ Load data hanya sekali di awal (tidak reset form)
+ useEffect(() => {
+ if (!id) return;
+
+ const loadData = async () => {
+ try {
+ stateDemografi.update.id = id;
+ await stateDemografi.findUnique.load(id);
+
+ const data = stateDemografi.findUnique.data;
+ if (data) {
+ setFormData({
+ pekerjaan: data.pekerjaan ?? '',
+ lakiLaki: Number(data.lakiLaki ?? 0),
+ perempuan: Number(data.perempuan ?? 0),
+ });
+ }
+ } catch (error) {
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data');
+ }
+ };
+
+ loadData();
+ }, [id]);
+
+ // ✅ Handler input terkontrol (tidak buat re-render berlebihan)
+ const handleChange = useCallback(
+ (field: keyof FormData) =>
+ (e: React.ChangeEvent) => {
+ const value =
+ field === 'lakiLaki' || field === 'perempuan'
+ ? Number(e.currentTarget.value)
+ : e.currentTarget.value;
+
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ // ✅ Submit hanya update global state sekali
+ const handleSubmit = async () => {
+ try {
+ stateDemografi.update.id = id;
+ stateDemografi.update.form = { ...formData };
+
+ await stateDemografi.update.submit();
+
+ toast.success('Data berhasil diperbarui');
+ router.push('/admin/ekonomi/demografi-pekerjaan');
+ } catch (error) {
+ console.error('Error updating data:', error);
+ toast.error('Gagal memperbarui data');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Demografi Pekerjaan
+
+
+
+ {/* Form Card */}
+
+
+
+
+
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
new file mode 100644
index 00000000..d590b347
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
@@ -0,0 +1,128 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
+
+function CreateDemografiPekerjaan() {
+ const stateDemografi = useProxy(demografiPekerjaan);
+ const [chartData, setChartData] = useState([]);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateDemografi.create.form = {
+ pekerjaan: '',
+ lakiLaki: 0,
+ perempuan: 0,
+ };
+ };
+
+ const handleSubmit = async () => {
+ const id = await stateDemografi.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stateDemografi.findUnique.load(idStr);
+ if (stateDemografi.findUnique.data) {
+ setChartData([stateDemografi.findUnique.data]);
+ }
+ }
+ resetForm();
+ router.push('/admin/ekonomi/demografi-pekerjaan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Demografi Pekerjaan
+
+
+
+ {/* Form */}
+
+
+ {
+ stateDemografi.create.form.pekerjaan = val.currentTarget.value;
+ }}
+ required
+ />
+ {
+ stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
+ }}
+ required
+ />
+ {
+ stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateDemografiPekerjaan;
diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx
new file mode 100644
index 00000000..0b2d4a92
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx
@@ -0,0 +1,247 @@
+'use client'
+import colors from '@/con/colors';
+import { BarChart } from '@mantine/charts';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+ Pagination,
+ Flex,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../_com/header';
+import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
+import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan';
+
+function DemografiPekerjaan() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListDemografiPekerjaan({ search }: { search: string }) {
+ type DemografiPekerjaan = {
+ id: string;
+ pekerjaan: string;
+ lakiLaki: number;
+ perempuan: number;
+ };
+
+ const router = useRouter();
+ const stateDemografi = useProxy(demografiPekerjaan);
+ const [chartData, setChartData] = useState([]);
+ const [mounted, setMounted] = useState(false);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateDemografi.findMany;
+
+ const handleDelete = () => {
+ if (selectedId) {
+ stateDemografi.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ }
+ };
+
+ useShallowEffect(() => {
+ setMounted(true);
+ load(page, 10, search);
+ }, [page, search]);
+
+ useEffect(() => {
+ if (data) {
+ setChartData(
+ data.map((item) => ({
+ id: item.id,
+ pekerjaan: item.pekerjaan,
+ lakiLaki: Number(item.lakiLaki),
+ perempuan: Number(item.perempuan),
+ }))
+ );
+ }
+ }, [data]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ List Demografi Pekerjaan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Pekerjaan
+ Laki - Laki
+ Perempuan
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+ {item.pekerjaan}
+ {item.lakiLaki}
+ {item.perempuan}
+
+
+ router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
+ }
+ >
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data demografi pekerjaan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Chart */}
+
+
+
+
+ Grafik Demografi Pekerjaan
+
+ {mounted && chartData.length > 0 ? (
+
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
+
+
+
+
+ Laki - Laki
+
+
+
+ Perempuan
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus demografi pekerjaan ini?"
+ />
+
+ );
+}
+
+export default DemografiPekerjaan;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx
new file mode 100644
index 00000000..b7a217a4
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx
@@ -0,0 +1,149 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Group,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { toast } from 'react-toastify';
+
+function EditJumlahPendudukMiskin() {
+ const router = useRouter();
+ const params = useParams() as { id: string };
+ const stateJPM = useProxy(jumlahPendudukMiskin);
+
+ const id = params.id;
+
+ // 🔹 State lokal untuk form
+ const [formData, setFormData] = useState({
+ year: 0,
+ totalPoorPopulation: 0,
+ });
+
+ // 🔹 Load data awal dari backend
+ useEffect(() => {
+ if (!id) return;
+
+ const loadData = async () => {
+ try {
+ await stateJPM.findUnique.load(id);
+ const data = stateJPM.findUnique.data;
+ if (data) {
+ setFormData({
+ year: data.year || 0,
+ totalPoorPopulation: data.totalPoorPopulation || 0,
+ });
+ }
+ } catch (error) {
+ console.error('Gagal memuat data:', error);
+ toast.error('Gagal memuat data jumlah penduduk miskin');
+ }
+ };
+
+ loadData();
+ }, [id]);
+
+ // 🔹 Handler input controlled
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: Number(value),
+ }));
+ };
+
+ // 🔹 Submit form
+ const handleSubmit = async () => {
+ try {
+ stateJPM.update.id = id;
+ // update global state cuma saat submit
+ stateJPM.update.form = { ...formData };
+
+ await stateJPM.update.submit();
+ toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
+ router.push('/admin/ekonomi/jumlah-penduduk-miskin');
+ } catch (error) {
+ console.error('Gagal menyimpan data:', error);
+ toast.error('Terjadi kesalahan saat menyimpan data');
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Jumlah Penduduk Miskin
+
+
+
+
+
+ handleChange('year', e.currentTarget.value)}
+ />
+
+
+ handleChange('totalPoorPopulation', e.currentTarget.value)
+ }
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditJumlahPendudukMiskin;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
new file mode 100644
index 00000000..fdebedfd
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
@@ -0,0 +1,102 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import colors from '@/con/colors';
+import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
+
+export default function CreateJumlahPendudukMiskin() {
+ const stateJPM = useProxy(jumlahPendudukMiskin);
+ const [chartData, setChartData] = useState([]);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateJPM.create.form = {
+ year: new Date().getFullYear(),
+ totalPoorPopulation: 0,
+ };
+ };
+
+ const handleSubmit = async () => {
+ const id = await stateJPM.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stateJPM.findUnique.load(idStr);
+ if (stateJPM.findUnique.data) {
+ setChartData([stateJPM.findUnique.data]);
+ }
+ }
+ resetForm();
+ router.push('/admin/ekonomi/jumlah-penduduk-miskin');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Jumlah Penduduk Miskin
+
+
+
+ {/* Form Paper */}
+
+
+ {
+ const value = e.currentTarget.value;
+ stateJPM.create.form.year = value ? Number(value) : 0;
+ }}
+ required
+ />
+
+ {
+ stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx
new file mode 100644
index 00000000..57fde078
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx
@@ -0,0 +1,227 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../_com/header';
+import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
+import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
+
+// ✅ BarChart Mantine
+import { BarChart } from '@mantine/charts';
+
+function JumlahPendudukMiskin() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListJumlahPendudukMiskin({ search }: { search: string }) {
+ type JPMGrafik = { year: number; totalPoorPopulation: number };
+ const stateJPM = useProxy(jumlahPendudukMiskin);
+ const [chartData, setChartData] = useState([]);
+ const [mounted, setMounted] = useState(false);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+
+ const { data, page, loading, load, totalPages } = stateJPM.findMany;
+
+ // Load data awal
+ useShallowEffect(() => {
+ setMounted(true);
+ load(page, 10, search);
+ }, [page, search]);
+
+ // Update chart data
+ useEffect(() => {
+ if (stateJPM.findMany.data) {
+ setChartData(
+ stateJPM.findMany.data.map((item) => ({
+ year: Number(item.year),
+ totalPoorPopulation: Number(item.totalPoorPopulation),
+ }))
+ );
+ }
+ }, [stateJPM.findMany.data]);
+
+ const filteredData = data || [];
+
+ const handleDelete = () => {
+ if (selectedId) {
+ stateJPM.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ stateJPM.findMany.load();
+ }
+ };
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Tabel */}
+
+
+ Daftar Jumlah Penduduk Miskin
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Tahun
+ Jumlah Penduduk Miskin
+ Edit
+ Delete
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+ {item.year}
+ {item.totalPoorPopulation}
+
+
+ router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)
+ }
+ >
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Bar Chart */}
+
+
+
+ Grafik Jumlah Penduduk Miskin
+
+ {mounted && chartData.length > 0 ? (
+ ({
+ name: item.year.toString(),
+ value: item.totalPoorPopulation,
+ }))}
+ dataKey="name"
+ series={[
+ { name: 'value', color: colors['blue-button'] },
+ ]}
+ withTooltip
+ valueFormatter={(v) => `${v.toLocaleString()} jiwa`}
+ />
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus data ini?"
+ />
+
+ );
+}
+
+export default JumlahPendudukMiskin;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..8b884c50
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx
@@ -0,0 +1,124 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Stack,
+ Tabs,
+ TabsList,
+ TabsPanel,
+ TabsTab,
+ Title,
+ Tooltip,
+ ScrollArea,
+} from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconUsers, IconSchool } from '@tabler/icons-react';
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "Pengangguran Berdasarkan Usia",
+ value: "pengangguranberdasarkanusia",
+ href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia",
+ icon: ,
+ tooltip: "Data pengangguran menurut kelompok usia",
+ },
+ {
+ label: "Pengangguran Berdasarkan Pendidikan",
+ value: "pengangguranberdasarkanpendidikan",
+ href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan",
+ icon: ,
+ tooltip: "Data pengangguran menurut tingkat pendidikan",
+ },
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+
+ Jumlah Penduduk Usia Kerja yang Menganggur
+
+
+
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/layout.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/layout.tsx
new file mode 100644
index 00000000..285fc16e
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/layout.tsx
@@ -0,0 +1,9 @@
+import LayoutTabs from "./_lib/layoutTabs";
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
new file mode 100644
index 00000000..7e069310
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
@@ -0,0 +1,143 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function EditGrafikBerdasarkanPendidikan() {
+ const router = useRouter();
+ const params = useParams() as { id: string };
+ const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
+ const id = params.id;
+
+ // state lokal untuk form
+ const [formData, setFormData] = useState({
+ SD: '',
+ SMP: '',
+ SMA: '',
+ D3: '',
+ S1: '',
+ });
+
+ useEffect(() => {
+ if (id) {
+ stategrafik.findUnique.load(id).then(() => {
+ const data = stategrafik.findUnique.data;
+ if (data) {
+ setFormData({
+ SD: data.SD || '',
+ SMP: data.SMP || '',
+ SMA: data.SMA || '',
+ D3: data.D3 || '',
+ S1: data.S1 || '',
+ });
+ }
+ });
+ }
+ }, [id]);
+
+ const handleChange = (field: keyof typeof formData) =>
+ (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: e.currentTarget.value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ stategrafik.update.id = id;
+ stategrafik.update.form = { ...formData }; // update global state pas submit aja
+ await stategrafik.update.submit();
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
+ );
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Grafik Pengangguran Berdasarkan Pendidikan
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditGrafikBerdasarkanPendidikan;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
new file mode 100644
index 00000000..e57a797a
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
@@ -0,0 +1,127 @@
+'use client';
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import React from 'react';
+import { useRouter } from 'next/navigation';
+import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
+import { useProxy } from 'valtio/utils';
+import { useState } from 'react';
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+function CreateGrafikBerdasarkanPendidikan() {
+ const router = useRouter();
+ const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
+ const [donutData, setDonutData] = useState([]);
+
+ const resetForm = () => {
+ stategrafik.create.form = {
+ ...stategrafik.create.form,
+ SD: '',
+ SMP: '',
+ SMA: '',
+ D3: '',
+ S1: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ const id = await stategrafik.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stategrafik.findUnique.load(idStr);
+ if (stategrafik.findUnique.data) {
+ setDonutData([stategrafik.findUnique.data]);
+ }
+ }
+ resetForm();
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
+ );
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Data Pengangguran Berdasarkan Pendidikan
+
+
+
+
+
+ (stategrafik.create.form.SD = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.SMP = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.SMA = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.D3 = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.S1 = val.currentTarget.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateGrafikBerdasarkanPendidikan;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx
new file mode 100644
index 00000000..74137342
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx
@@ -0,0 +1,255 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Flex,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { DonutChart } from '@mantine/charts';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
+
+function GrafikBerdasarkanPendidikan() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
+ const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
+ const [donutData, setDonutData] = useState([]);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+
+ const handleDelete = async () => {
+ if (selectedId) {
+ await stategrafik.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ stategrafik.findMany.load();
+ }
+ };
+
+ const { data, page, totalPages, loading, load } = stategrafik.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ useEffect(() => {
+ if (stategrafik.findMany.data) {
+ const SD = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.SD || 0),
+ 0,
+ );
+ const SMP = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.SMP || 0),
+ 0,
+ );
+ const SMA = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.SMA || 0),
+ 0,
+ );
+ const D3 = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.D3 || 0),
+ 0,
+ );
+ const S1 = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.S1 || 0),
+ 0,
+ );
+ setDonutData([
+ { name: 'SD', value: SD, color: '#4b6Ef5' },
+ { name: 'SMP', value: SMP, color: '#14b885' },
+ { name: 'SMA', value: SMA, color: '#E6A03B' },
+ { name: 'D3', value: D3, color: '#DB524D' },
+ { name: 'S1', value: S1, color: '#1018A8FF' },
+ ]);
+ }
+ }, [stategrafik.findMany.data]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Table Data */}
+
+
+ List Pengangguran Berdasarkan Pendidikan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create',
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ SD
+ SMP
+ SMA
+ D3
+ S1
+ Edit
+ Delete
+
+
+
+ {filteredData.length === 0 ? (
+
+
+
+
+ Belum ada data grafik responden
+
+
+
+
+ ) : (
+ filteredData.map((item) => (
+
+ {item.SD}
+ {item.SMP}
+ {item.SMA}
+ {item.D3}
+ {item.S1}
+
+
+
+ router.push(
+ `/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`,
+ )
+ }
+ >
+
+
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Donut Chart */}
+
+
+
+ Grafik Pengangguran Berdasarkan Pendidikan
+
+ {donutData.length > 0 ? (
+
+ ) : (
+
+ Belum ada data untuk ditampilkan dalam grafik
+
+ )}
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?"
+ />
+
+ );
+}
+
+export default GrafikBerdasarkanPendidikan;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
new file mode 100644
index 00000000..f5e77711
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
@@ -0,0 +1,159 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Group,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { toast } from 'react-toastify';
+
+function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
+ const router = useRouter();
+ const params = useParams() as { id: string };
+ const stategrafik = useProxy(
+ grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur
+ );
+ const id = params.id;
+
+ // ✅ state lokal, controlled
+ const [formData, setFormData] = useState({
+ usia18_25: '',
+ usia26_35: '',
+ usia36_45: '',
+ usia46_keatas: '',
+ });
+
+ // load data dari global state -> masukin ke local state
+ useEffect(() => {
+ if (id) {
+ stategrafik.findUnique.load(id).then(() => {
+ const data = stategrafik.findUnique.data;
+ if (data) {
+ setFormData({
+ usia18_25: data.usia18_25 || '',
+ usia26_35: data.usia26_35 || '',
+ usia36_45: data.usia36_45 || '',
+ usia46_keatas: data.usia46_keatas || '',
+ });
+ }
+ });
+ }
+ }, [id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // ✅ baru update global state pas submit
+ stategrafik.update.id = id;
+ stategrafik.update.form = { ...formData };
+
+ await stategrafik.update.submit();
+
+ toast.success('Data grafik berhasil diperbarui!');
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
+ );
+ } catch (error) {
+ console.error(error);
+ toast.error('Terjadi kesalahan saat memperbarui data grafik');
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Grafik Pengangguran Berdasarkan Usia Kerja
+
+
+
+
+
+ handleChange('usia18_25', e.currentTarget.value)}
+ required
+ />
+ handleChange('usia26_35', e.currentTarget.value)}
+ required
+ />
+ handleChange('usia36_45', e.currentTarget.value)}
+ required
+ />
+ handleChange('usia46_keatas', e.currentTarget.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditGrafikBerdasarkanUsiaKerjaYangMenganggur;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
new file mode 100644
index 00000000..77c362fd
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
@@ -0,0 +1,119 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import React, { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+
+function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
+ const router = useRouter();
+ const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
+ const [donutData, setDonutData] = useState([]);
+
+ const resetForm = () => {
+ stategrafik.create.form = {
+ ...stategrafik.create.form,
+ usia18_25: '',
+ usia26_35: '',
+ usia36_45: '',
+ usia46_keatas: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ const id = await stategrafik.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stategrafik.findUnique.load(idStr);
+ if (stategrafik.findUnique.data) {
+ setDonutData([stategrafik.findUnique.data]);
+ }
+ }
+ resetForm();
+ router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Data Pengangguran Berdasarkan Usia
+
+
+
+ {/* Form Paper */}
+
+
+ (stategrafik.create.form.usia18_25 = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.usia26_35 = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.usia36_45 = val.currentTarget.value)}
+ required
+ />
+ (stategrafik.create.form.usia46_keatas = val.currentTarget.value)}
+ required
+ />
+
+ {/* Submit Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateGrafikBerdasarkanUsiaKerjaYangMenganggur;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx
new file mode 100644
index 00000000..3c0fcfd6
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx
@@ -0,0 +1,224 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Flex,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { DonutChart } from '@mantine/charts';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
+
+function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
+ const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
+ const [donutData, setDonutData] = useState([]);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+
+ const handleDelete = async () => {
+ if (selectedId) {
+ await stategrafik.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ stategrafik.findMany.load();
+ }
+ };
+
+ const { data, page, totalPages, loading, load } = stategrafik.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ useEffect(() => {
+ if (stategrafik.findMany.data) {
+ const totalUsia18_25 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia18_25 || 0), 0);
+ const totalUsia26_35 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0);
+ const totalUsia36_45 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0);
+ const totalUsia46_keatas = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0);
+
+ setDonutData([
+ { name: 'Usia 18-25', value: totalUsia18_25, color: colors['blue-button'] },
+ { name: 'Usia 26-35', value: totalUsia26_35, color: '#10A85AFF' },
+ { name: 'Usia 36-45', value: totalUsia36_45, color: '#C07B13FF' },
+ { name: 'Usia 46+', value: totalUsia46_keatas, color: '#1094A8FF' },
+ ]);
+ }
+ }, [stategrafik.findMany.data]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Table */}
+
+
+
+ List Pengangguran Berdasarkan Usia Kerja
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Usia 18-25
+ Usia 26-35
+ Usia 36-45
+ Usia 46+
+ Edit
+ Delete
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+ {item.usia18_25}
+ {item.usia26_35}
+ {item.usia36_45}
+ {item.usia46_keatas}
+
+
+ router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)
+ }
+ >
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Belum ada data grafik responden
+
+
+
+ )}
+
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Donut Chart */}
+
+
+
+ Grafik Pengangguran Berdasarkan Usia Kerja
+
+ {donutData.length > 0 ? (
+
+
+
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?"
+ />
+
+ );
+}
+
+export default GrafikBerdasarkanUsiaKerjaYangMenganggur;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx
new file mode 100644
index 00000000..22ae288d
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx
@@ -0,0 +1,227 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Select,
+ NumberInput,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, useCallback } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+// --- Helper konstanta
+const MONTHS = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
+ 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des',
+];
+
+function EditDetailDataPengangguran() {
+ const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
+ const router = useRouter();
+ const params = useParams();
+
+ // --- state lokal form
+ const [formData, setFormData] = useState({
+ month: '',
+ year: new Date().getFullYear(),
+ educatedUnemployment: 0,
+ uneducatedUnemployment: 0,
+ totalUnemployment: 0,
+ percentageChange: 0,
+ });
+
+ // --- hitung total + persentase perubahan
+ const calculateTotalAndChange = useCallback(
+ async (data: typeof formData) => {
+ const total = data.educatedUnemployment + data.uneducatedUnemployment;
+ let percentageChange = 0;
+
+ const currentMonthIndex = MONTHS.indexOf(data.month);
+ if (currentMonthIndex !== -1) {
+ let prevMonthIndex = currentMonthIndex - 1;
+ let prevYear = data.year;
+
+ if (prevMonthIndex < 0) {
+ prevMonthIndex = 11;
+ prevYear--;
+ }
+
+ const prevData = await stateDetail.findByMonthYear.load({
+ month: MONTHS[prevMonthIndex],
+ year: prevYear,
+ });
+
+ if (prevData && prevData.totalUnemployment > 0) {
+ const change =
+ ((total - prevData.totalUnemployment) /
+ prevData.totalUnemployment) * 100;
+ percentageChange = parseFloat(change.toFixed(1));
+ }
+ }
+
+ return { total, percentageChange };
+ },
+ [stateDetail.findByMonthYear]
+ );
+
+ // --- update state lokal
+ const updateFormData = async (updates: Partial) => {
+ const newData = { ...formData, ...updates };
+ const { total, percentageChange } = await calculateTotalAndChange(newData);
+ setFormData({ ...newData, totalUnemployment: total, percentageChange });
+ };
+
+ // --- load detail by ID (sekali)
+ useEffect(() => {
+ const loadDetail = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ await stateDetail.findUnique.load(id);
+ const data = stateDetail.findUnique.data;
+ if (!data) return;
+
+ const yearValue =
+ data.year && typeof data.year === 'object' && 'getFullYear' in data.year
+ ? (data.year as Date).getFullYear()
+ : Number(data.year);
+
+ stateDetail.update.id = id; // simpan id untuk update
+
+ setFormData({
+ month: data.month,
+ year: yearValue,
+ educatedUnemployment: data.educatedUnemployment,
+ uneducatedUnemployment: data.uneducatedUnemployment,
+ totalUnemployment: data.totalUnemployment,
+ percentageChange: data.percentageChange || 0,
+ });
+ } catch (err) {
+ console.error('Error loading detail:', err);
+ toast.error('Gagal memuat data detail');
+ }
+ };
+
+ loadDetail();
+ }, [params?.id]);
+
+ // --- submit form
+ const handleSubmit = async () => {
+ try {
+ const { total, percentageChange } = await calculateTotalAndChange(formData);
+
+ stateDetail.update.form = {
+ ...formData,
+ totalUnemployment: total,
+ percentageChange,
+ };
+
+ const success = await stateDetail.update.submit();
+ if (success) {
+ toast.success('Detail data pengangguran berhasil diperbarui!');
+ router.push('/admin/ekonomi/jumlah-pengangguran');
+ }
+ } catch (err) {
+ console.error('Error updating:', err);
+ toast.error('Terjadi kesalahan saat memperbarui data');
+ }
+ };
+
+ return (
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+ Edit Detail Data Pengangguran
+
+
+
+
+
+ updateFormData({ month: val || '' })}
+ />
+ updateFormData({ year: Number(val) })}
+ required
+ />
+
+ updateFormData({
+ educatedUnemployment: Number(e.currentTarget.value) || 0,
+ })
+ }
+ required
+ />
+
+ updateFormData({
+ uneducatedUnemployment: Number(e.currentTarget.value) || 0,
+ })
+ }
+ required
+ />
+
+ Total Otomatis: {formData.totalUnemployment}
+
+
+ Perubahan Otomatis:{' '}
+ {formData.percentageChange !== null
+ ? `${formData.percentageChange}%`
+ : '-'}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditDetailDataPengangguran;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx
new file mode 100644
index 00000000..a4043b22
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx
@@ -0,0 +1,149 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
+import colors from '@/con/colors';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailJumlahPengangguran() {
+ const router = useRouter();
+ const params = useParams();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
+
+ useShallowEffect(() => {
+ stateDetail.findUnique.load(params?.id as string);
+ }, [params?.id]);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateDetail.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/jumlah-pengangguran");
+ }
+ };
+
+ if (!stateDetail.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = stateDetail.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Paper Detail */}
+
+
+
+ Detail Data Pengangguran
+
+
+
+
+
+ Pengangguran Terdidik
+ {data.educatedUnemployment || '-'}
+
+
+
+ Pengangguran Tidak Terdidik
+ {data.uneducatedUnemployment || '-'}
+
+
+
+ Perubahan
+
+ {data.percentageChange !== null && data.percentageChange !== undefined
+ ? `${data.percentageChange}%`
+ : 'Tidak ada data perubahan'}
+
+
+
+
+ Tahun
+ {data.year || '-'}
+
+
+
+ Bulan
+ {data.month || '-'}
+
+
+
+ Total Pengangguran
+ {data.totalUnemployment || '-'}
+
+
+ {/* Tombol Edit & Hapus */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ color="red"
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
+ color="green"
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus data ini?"
+ />
+
+ );
+}
+
+export default DetailJumlahPengangguran;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
new file mode 100644
index 00000000..df0cb26a
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
@@ -0,0 +1,203 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ NumberInput,
+ Title,
+ Select,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function CreateJumlahPengangguran() {
+ const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
+ const [chartData, setChartData] = useState([]);
+ const router = useRouter();
+
+ const monthOptions = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
+ 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
+ ];
+
+ const resetForm = () => {
+ stateDetail.create.form = {
+ month: monthOptions[new Date().getMonth()], // default bulan sekarang
+ year: new Date().getFullYear(), // default tahun sekarang
+ totalUnemployment: 0,
+ educatedUnemployment: 0,
+ uneducatedUnemployment: 0,
+ percentageChange: 0,
+ };
+ };
+
+ const calculateTotalAndChange = async () => {
+ const total =
+ stateDetail.create.form.educatedUnemployment +
+ stateDetail.create.form.uneducatedUnemployment;
+
+ stateDetail.create.form.totalUnemployment = total;
+
+ // hitung perubahan dibanding bulan sebelumnya
+ const monthOrder = monthOptions;
+ const currentIndex = monthOrder.findIndex(
+ (m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase()
+ );
+
+ if (currentIndex > 0) {
+ const prevMonth = monthOrder[currentIndex - 1];
+ const prev = await stateDetail.findByMonthYear.load({
+ month: prevMonth,
+ year: stateDetail.create.form.year,
+ });
+
+ if (prev?.totalUnemployment) {
+ const change = ((total - prev.totalUnemployment) / prev.totalUnemployment) * 100;
+ stateDetail.create.form.percentageChange = Number(change.toFixed(1));
+ } else {
+ stateDetail.create.form.percentageChange = 0;
+ }
+ } else {
+ stateDetail.create.form.percentageChange = 0;
+ }
+ };
+
+ const handleSubmit = async () => {
+ await calculateTotalAndChange();
+ const id = await stateDetail.create.create();
+ if (id) {
+ await stateDetail.findUnique.load(String(id));
+ if (stateDetail.findUnique.data) {
+ setChartData([stateDetail.findUnique.data]);
+ }
+ resetForm();
+ router.push('/admin/ekonomi/jumlah-pengangguran');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Data Pengangguran
+
+
+
+ {/* Form Card */}
+
+
+ {
+ stateDetail.create.form.month = value || '';
+ calculateTotalAndChange();
+ }}
+ required
+ />
+
+ {
+ stateDetail.create.form.year = Number(value) || new Date().getFullYear();
+ calculateTotalAndChange();
+ }}
+ min={2000}
+ max={2100}
+ required
+ />
+
+ {
+ stateDetail.create.form.educatedUnemployment = Number(value) || 0;
+ calculateTotalAndChange();
+ }}
+ min={0}
+ required
+ />
+
+ {
+ stateDetail.create.form.uneducatedUnemployment = Number(value) || 0;
+ calculateTotalAndChange();
+ }}
+ min={0}
+ required
+ />
+
+
+
+ Total Otomatis:
+
+
+ {stateDetail.create.form.totalUnemployment.toLocaleString()}
+
+
+
+
+
+ Perubahan Otomatis:
+
+
+ {stateDetail.create.form.percentageChange.toFixed(1)}%
+
+
+
+ {/* Action Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateJumlahPengangguran;
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx
new file mode 100644
index 00000000..ff3a8209
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx
@@ -0,0 +1,182 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack,
+ Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
+ Text, Title, Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { BarChart } from '@mantine/charts';
+
+import HeaderSearch from '../../_com/header';
+import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
+
+function DetailDataPengangguran() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListDetailDataPengangguran({ search }: { search: string }) {
+ const [chartData, setChartData] = useState([]);
+ const [mounted, setMounted] = useState(false);
+ const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateDetail.findMany;
+
+ useShallowEffect(() => {
+ setMounted(true);
+ load(page, 10, search);
+ }, [page, search]);
+
+ useEffect(() => {
+ if (data) {
+ setChartData(
+ data.map((item) => ({
+ id: item.id,
+ month: item.month,
+ year: Number(item.year),
+ educatedUnemployment: Number(item.educatedUnemployment),
+ uneducatedUnemployment: Number(item.uneducatedUnemployment),
+ percentageChange: Number(item.percentageChange),
+ totalUnemployment: Number(item.totalUnemployment),
+ }))
+ );
+ }
+ }, [data]);
+
+ const filteredData = data || []
+
+ // Loading state
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Table Section */}
+
+
+ Daftar Detail Data Pengangguran
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Bulan
+ Terdidik
+ Tidak Terdidik
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+ {item.month} {item.year}
+ {item.educatedUnemployment}
+ {item.uneducatedUnemployment}
+
+ router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Chart Section */}
+
+
+ Data Pengangguran Terdidik & Tidak Terdidik
+
+ {mounted && chartData.length > 0 ? (
+
+
+
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
+
+
+ );
+}
+
+export default DetailDataPengangguran;
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
new file mode 100644
index 00000000..ec501ee0
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
@@ -0,0 +1,201 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditLowonganKerja() {
+ const lowonganState = useProxy(lowonganKerjaState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ posisi: '',
+ namaPerusahaan: '',
+ lokasi: '',
+ tipePekerjaan: '',
+ gaji: '',
+ deskripsi: '',
+ kualifikasi: '',
+ notelp: '',
+ });
+
+ // load data sekali aja ketika mount / id berubah
+ useEffect(() => {
+ const loadLowongan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await lowonganState.update.load(id);
+ if (data) {
+ setFormData({
+ posisi: data.posisi || '',
+ namaPerusahaan: data.namaPerusahaan || '',
+ lokasi: data.lokasi || '',
+ tipePekerjaan: data.tipePekerjaan || '',
+ gaji: data.gaji || '',
+ deskripsi: data.deskripsi || '',
+ kualifikasi: data.kualifikasi || '',
+ notelp: data.notelp || '',
+ });
+ }
+ } catch (error) {
+ console.error("Error loading lowongan kerja:", error);
+ toast.error("Gagal memuat data lowongan kerja");
+ }
+ };
+
+ loadLowongan();
+ }, [params?.id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ lowonganState.update.id = params?.id as string;
+ lowonganState.update.form = { ...formData };
+
+ await lowonganState.update.update();
+ toast.success("Lowongan kerja berhasil diperbarui!");
+ router.push("/admin/ekonomi/lowongan-kerja-lokal");
+ } catch (error) {
+ console.error("Error updating lowongan kerja:", error);
+ toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
+ }
+ };
+
+ return (
+
+ {/* Header dengan tombol back */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Lowongan Kerja Lokal
+
+
+
+ {/* Card Form */}
+
+
+ handleChange("posisi", e.target.value)}
+ required
+ />
+
+ handleChange("namaPerusahaan", e.target.value)}
+ required
+ />
+
+ handleChange("lokasi", e.target.value)}
+ required
+ />
+
+ handleChange("notelp", e.target.value)}
+ required
+ />
+
+ handleChange("tipePekerjaan", e.target.value)}
+ required
+ />
+
+ handleChange("gaji", e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi Lowongan Kerja
+
+ handleChange("deskripsi", val)}
+ />
+
+
+
+
+ Kualifikasi Lowongan Kerja
+
+ handleChange("kualifikasi", val)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditLowonganKerja;
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx
new file mode 100644
index 00000000..980529aa
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx
@@ -0,0 +1,153 @@
+'use client'
+
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+import colors from '@/con/colors';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
+
+function DetailLowonganKerjaLokal() {
+ const lowonganState = useProxy(lowonganKerjaState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ lowonganState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ lowonganState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/lowongan-kerja-lokal");
+ }
+ };
+
+ if (!lowonganState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = lowonganState.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Lowongan Kerja Lokal
+
+
+
+
+
+ Bekerja Sebagai
+ {data.posisi || '-'}
+
+
+
+ Nama Usaha
+ {data.namaPerusahaan || '-'}
+
+
+
+ Lokasi
+ {data.lokasi || '-'}
+
+
+
+ Nomor Yang Dapat Dihubungi
+ {data.notelp || '-'}
+
+
+
+ Tipe Pekerjaan
+ {data.tipePekerjaan || '-'}
+
+
+
+ Gaji
+ {data.gaji || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Kualifikasi
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus lowongan kerja ini?"
+ />
+
+ );
+}
+
+export default DetailLowonganKerjaLokal;
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
new file mode 100644
index 00000000..606100e8
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
@@ -0,0 +1,172 @@
+'use client';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
+
+function CreateLowonganKerja() {
+ const lowonganState = useProxy(lowonganKerjaState);
+ const router = useRouter();
+
+ const resetForm = () => {
+ lowonganState.create.form = {
+ posisi: '',
+ namaPerusahaan: '',
+ lokasi: '',
+ tipePekerjaan: '',
+ gaji: '',
+ deskripsi: '',
+ kualifikasi: '',
+ notelp: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await lowonganState.create.create();
+ resetForm();
+ router.push('/admin/ekonomi/lowongan-kerja-lokal');
+ };
+
+ return (
+
+ {/* Header dengan tombol kembali */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Lowongan Kerja Lokal
+
+
+
+ {/* Card Form */}
+
+
+
+ (lowonganState.create.form.posisi = val.target.value)
+ }
+ label="Posisi"
+ placeholder="Masukkan posisi"
+ required
+ />
+
+ (lowonganState.create.form.namaPerusahaan = val.target.value)
+ }
+ label="Nama Perusahaan"
+ placeholder="Masukkan nama perusahaan"
+ required
+ />
+
+ (lowonganState.create.form.notelp = val.target.value)
+ }
+ label="Nomor Yang Dapat Dihubungi"
+ placeholder="Masukkan nomor yang dapat dihubungi"
+ required
+ />
+
+ (lowonganState.create.form.lokasi = val.target.value)
+ }
+ label="Lokasi"
+ placeholder="Masukkan lokasi"
+ required
+ />
+
+ (lowonganState.create.form.tipePekerjaan = val.target.value)
+ }
+ label="Tipe Pekerjaan"
+ placeholder="Masukkan tipe pekerjaan"
+ required
+ />
+
+ (lowonganState.create.form.gaji = val.target.value)
+ }
+ label="Gaji (per bulan)"
+ placeholder="Masukkan gaji"
+ required
+ />
+
+
+
+ Deskripsi Lowongan Kerja
+
+ {
+ lowonganState.create.form.deskripsi = val;
+ }}
+ />
+
+
+
+
+ Kualifikasi Lowongan Kerja
+
+ {
+ lowonganState.create.form.kualifikasi = val;
+ }}
+ />
+
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateLowonganKerja;
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx
new file mode 100644
index 00000000..2c4452ac
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx
@@ -0,0 +1,165 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
+
+function LowonganKerjaLokal() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListLowonganKerjaLokal({ search }: { search: string }) {
+ const stateLowongan = useProxy(lowonganKerjaState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = stateLowongan.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Lowongan Kerja Lokal
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/lowongan-kerja-lokal/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Pekerjaan
+ Nama Perusahaan
+ Lokasi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.posisi}
+
+
+
+
+ {item.namaPerusahaan}
+
+
+
+
+ {item.lokasi}
+
+
+
+
+ router.push(
+ `/admin/ekonomi/lowongan-kerja-lokal/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data lowongan kerja yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default LowonganKerjaLokal;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..6c4e5e2e
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Stack,
+ Tabs,
+ TabsList,
+ TabsPanel,
+ TabsTab,
+ Title,
+ Tooltip,
+ ScrollArea,
+} from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconShoppingBag, IconCategory } from '@tabler/icons-react';
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "Produk Pasar Desa",
+ value: "produkpasardesa",
+ href: "/admin/ekonomi/pasar-desa/produk-pasar-desa",
+ icon: ,
+ tooltip: "Kelola data produk yang ada di pasar desa",
+ },
+ {
+ label: "Kategori Produk",
+ value: "kategoriproduk",
+ href: "/admin/ekonomi/pasar-desa/kategori-produk",
+ icon: ,
+ tooltip: "Atur kategori produk pasar desa",
+ },
+ ];
+
+ const currentTab = tabs.find((tab) => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(
+ 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 (
+
+
+ Pasar Desa
+
+
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
new file mode 100644
index 00000000..451a4a71
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
@@ -0,0 +1,150 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import React, { useEffect, useState } from 'react';
+import { useParams, useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Title,
+ TextInput,
+ Tooltip,
+ Text,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { toast } from 'react-toastify';
+
+function EditKategoriProduk() {
+ const router = useRouter();
+ const params = useParams();
+ const id = params?.id as string;
+ const statePasar = useProxy(pasarDesaState.kategoriProduk);
+
+ const [formData, setFormData] = useState({ nama: '' });
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadKategoriProduk = async () => {
+ if (!id) return;
+
+ try {
+ const data = await statePasar.edit.load(id);
+
+ if (data) {
+ // simpan id ke state global hanya untuk referensi
+ statePasar.edit.id = id;
+
+ // simpan data ke state lokal
+ setFormData({ nama: data.nama || '' });
+ }
+ } catch (error) {
+ console.error('Error loading kategori produk:', error);
+ toast.error('Gagal memuat data kategori produk');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadKategoriProduk();
+ }, [id]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ if (!formData.nama.trim()) {
+ toast.error('Nama kategori produk tidak boleh kosong');
+ return;
+ }
+
+ // update global state hanya saat submit
+ statePasar.edit.form = { nama: formData.nama.trim() };
+ if (!statePasar.edit.id) {
+ statePasar.edit.id = id; // fallback
+ }
+
+ const success = await statePasar.edit.update();
+
+ if (success) {
+ toast.success('Kategori produk berhasil diperbarui!');
+ router.push('/admin/ekonomi/pasar-desa/kategori-produk');
+ }
+ } catch (error) {
+ console.error('Error updating kategori produk:', error);
+ toast.error('Terjadi kesalahan saat memperbarui kategori produk');
+ }
+ };
+
+ if (loading) {
+ return Loading... ;
+ }
+
+ return (
+
+ {/* Header dengan tombol back */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Kategori Produk
+
+
+
+ {/* Card form */}
+
+
+ Nama Kategori Produk}
+ placeholder="Masukkan nama kategori produk"
+ value={formData.nama}
+ onChange={handleChange}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKategoriProduk;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
new file mode 100644
index 00000000..93598be7
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
@@ -0,0 +1,102 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
+
+function CreateKategoriProduk() {
+ const router = useRouter();
+ const statePasar = useProxy(pasarDesaState.kategoriProduk);
+
+ useEffect(() => {
+ statePasar.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ statePasar.create.form = {
+ nama: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ if (!statePasar.create.form.nama) {
+ return toast.warn('Nama kategori produk wajib diisi');
+ }
+
+ await statePasar.create.create();
+ resetForm();
+ router.push('/admin/ekonomi/pasar-desa/kategori-produk');
+ };
+
+ return (
+
+ {/* Header dengan tombol kembali */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kategori Produk
+
+
+
+ {/* Card form */}
+
+
+ (statePasar.create.form.nama = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKategoriProduk;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx
new file mode 100644
index 00000000..0115aefd
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx
@@ -0,0 +1,168 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
+
+
+function KategoriProduk() {
+ const [search2, setSearch2] = useState("")
+ return (
+
+ }
+ value={search2}
+ onChange={(e) => setSearch2(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKategoriProduk({ search2 }: { search2: string }) {
+ const statePasar = useProxy(pasarDesaState.kategoriProduk)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const router = useRouter()
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = statePasar.findMany
+
+ useShallowEffect(() => {
+ load(page, 10, search2)
+ }, [page, search2])
+
+ const handleHapus = () => {
+ if (selectedId) {
+ statePasar.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ }
+ }
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ Daftar Kategori Produk
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Kategori
+ Edit
+ Delete
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+
+ router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)}
+ >
+
+
+
+
+
+
+ {
+ setSelectedId(item.id)
+ setModalHapus(true)
+ }}
+ >
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data kategori produk yang cocok
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10)
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text='Apakah anda yakin ingin menghapus kategori produk ini?'
+ />
+
+ )
+}
+
+export default KategoriProduk;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx
new file mode 100644
index 00000000..9ebad0e4
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+import LayoutTabs from "./_lib/layoutTabs"
+
+
+export default function Layout({children} : {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
new file mode 100644
index 00000000..17ebd925
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
@@ -0,0 +1,279 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ MultiSelect,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+type FormData = {
+ nama: string;
+ harga: number;
+ alamatUsaha: string;
+ imageId: string;
+ rating: number;
+ kategoriId: string[];
+ kontak: string;
+};
+
+function EditPasarDesa() {
+ const pasarState = useProxy(pasarDesaState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ nama: '',
+ harga: 0,
+ alamatUsaha: '',
+ imageId: '',
+ rating: 0,
+ kategoriId: [],
+ kontak: '',
+ });
+
+ // load data awal
+ useEffect(() => {
+ pasarState.kategoriProduk.findManyAll.load();
+
+ const loadPasarDesa = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await pasarState.pasarDesa.edit.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || '',
+ harga: data.harga || 0,
+ alamatUsaha: data.alamatUsaha || '',
+ imageId: data.imageId || '',
+ rating: data.rating || 0,
+ kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
+ kontak: data.kontak || '',
+ });
+ if (data.image?.link) setPreviewImage(data.image.link);
+ }
+ } catch (error) {
+ console.error('Error loading pasar desa:', error);
+ toast.error(
+ error instanceof Error ? error.message : 'Gagal mengambil data pasar desa'
+ );
+ }
+ };
+
+ loadPasarDesa();
+ }, [params?.id]);
+
+ const handleChange = (key: keyof FormData, value: any) => {
+ setFormData((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // upload image kalau ada file baru
+ let imageId = formData.imageId;
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ imageId = uploaded.id;
+ }
+
+ // update global state hanya saat submit
+ pasarState.pasarDesa.edit.form = {
+ ...formData,
+ imageId,
+ };
+
+ await pasarState.pasarDesa.edit.update();
+ toast.success('Pasar desa berhasil diperbarui!');
+ router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
+ } catch (error) {
+ console.error('Error updating pasar desa:', error);
+ toast.error('Terjadi kesalahan saat memperbarui pasar desa');
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Pasar Desa
+
+
+
+
+
+ {/* Dropzone upload */}
+
+
+ Gambar Produk
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Controlled Inputs */}
+ handleChange('nama', e.target.value)}
+ required
+ />
+
+ handleChange('harga', Number(e.target.value))}
+ required
+ />
+
+ handleChange('rating', Number(e.target.value))}
+ required
+ />
+
+ handleChange('alamatUsaha', e.target.value)}
+ required
+ />
+
+ handleChange('kontak', e.target.value)}
+ required
+ />
+
+ handleChange('kategoriId', val)}
+ data={
+ pasarState.kategoriProduk.findManyAll.data?.map((v) => ({
+ value: v.id,
+ label: v.nama,
+ })) || []
+ }
+ clearable
+ searchable
+ required
+ error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined}
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPasarDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx
new file mode 100644
index 00000000..96f52523
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx
@@ -0,0 +1,168 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
+import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react';
+import { useRouter, useParams } from 'next/navigation';
+import React, { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+
+function DetailPasarDesa() {
+ const statePasar = useProxy(pasarDesaState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ statePasar.pasarDesa.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ statePasar.pasarDesa.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
+ }
+ };
+
+ if (!statePasar.pasarDesa.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = statePasar.pasarDesa.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Pasar Desa
+
+
+
+
+
+ Nama Produk
+ {data.nama || '-'}
+
+
+
+ Harga Produk
+ Rp. {data.harga || '-'}
+
+
+
+ Rating Produk
+ {data.rating || '-'}
+
+
+
+ Alamat Usaha
+ {data.alamatUsaha || '-'}
+
+
+
+ Kontak
+ {data.kontak || '-'}
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+ Kategori
+
+ {data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
+ data.KategoriToPasar.map((kategori) => (
+
+ • {kategori.kategori.nama}
+
+ ))
+ ) : (
+ Tidak ada kategori
+ )}
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus produk ini?"
+ />
+
+ );
+}
+
+export default DetailPasarDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
new file mode 100644
index 00000000..72265081
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
@@ -0,0 +1,230 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ MultiSelect,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
+
+export default function CreatePasarDesa() {
+ const router = useRouter();
+ const statePasar = useProxy(pasarDesaState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ useEffect(() => {
+ statePasar.kategoriProduk.findManyAll.load();
+ }, []);
+
+ const resetForm = () => {
+ statePasar.pasarDesa.create.form = {
+ nama: '',
+ harga: 0,
+ alamatUsaha: '',
+ imageId: '',
+ rating: 0,
+ kategoriId: [],
+ kontak: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ statePasar.pasarDesa.create.form.imageId = uploaded.id;
+ await statePasar.pasarDesa.create.create();
+
+ resetForm();
+ router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
+ };
+
+ return (
+
+ {/* Header dengan tombol kembali */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Produk Pasar Desa
+
+
+
+ {/* Card Form */}
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar Produk
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Nama Produk */}
+ (statePasar.pasarDesa.create.form.nama = e.target.value)}
+ required
+ />
+
+ {/* Harga Produk */}
+ (statePasar.pasarDesa.create.form.harga = Number(e.target.value))}
+ required
+ />
+
+ {/* Rating */}
+ {
+ const value = Number(e.target.value);
+ if (value >= 0 && value <= 5) {
+ statePasar.pasarDesa.create.form.rating = value;
+ }
+ }}
+ />
+
+ {/* Alamat Usaha */}
+ (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
+ />
+
+ {/* Kontak */}
+ (statePasar.pasarDesa.create.form.kontak = e.target.value)}
+ />
+
+ {/* Kategori Produk */}
+ (statePasar.pasarDesa.create.form.kategoriId = val)}
+ data={
+ statePasar.kategoriProduk.findManyAll.data?.map((v) => ({
+ value: v.id,
+ label: v.nama,
+ })) || []
+ }
+ />
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx
new file mode 100644
index 00000000..fc14e3f3
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx
@@ -0,0 +1,166 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
+
+function PasarDesa() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPasarDesa({ search }: { search: string }) {
+ const statePasar = useProxy(pasarDesaState.pasarDesa);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = statePasar.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Produk Pasar Desa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Produk
+ Harga Produk
+ Rating
+ Alamat Usaha
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+ Rp.{item.harga}
+
+
+ {item.rating || '-'}
+
+
+
+ {item.alamatUsaha || '-'}
+
+
+
+
+ router.push(
+ `/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada produk pasar desa yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PasarDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
new file mode 100644
index 00000000..6dbb6b03
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
@@ -0,0 +1,217 @@
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
+import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
+import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, useCallback } from 'react';
+import { useProxy } from 'valtio/utils';
+import { toast } from 'react-toastify';
+
+type Statistik = {
+ tahun: string;
+ jumlah: string;
+};
+
+type FormData = {
+ nama: string;
+ deskripsi: string;
+ icon: string;
+ statistik: Statistik;
+};
+
+const initialForm: FormData = {
+ nama: '',
+ deskripsi: '',
+ icon: '',
+ statistik: {
+ tahun: '',
+ jumlah: '',
+ },
+};
+
+function EditProgramKemiskinan() {
+ const router = useRouter();
+ const { id } = useParams() as { id: string };
+ const stateProgram = useProxy(programKemiskinanState);
+
+ const [formData, setFormData] = useState(initialForm);
+
+ // Load data 1x dari global state → isi local state
+ useEffect(() => {
+ if (!id) return;
+
+ const loadData = async () => {
+ try {
+ await stateProgram.findUnique.load(id);
+ const data = stateProgram.findUnique.data;
+ if (data) {
+ setFormData({
+ nama: data.nama ?? '',
+ deskripsi: data.deskripsi ?? '',
+ icon: data.icon ?? '',
+ statistik: {
+ tahun: data.statistik?.tahun?.toString() ?? '',
+ jumlah: data.statistik?.jumlah?.toString() ?? '',
+ },
+ });
+ }
+ } catch (err) {
+ console.error('Error load data:', err);
+ toast.error('Gagal mengambil data program');
+ }
+ };
+
+ loadData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [id]); // ✅ hanya trigger saat id berubah
+
+
+ // generic handler untuk field top-level
+ const handleChange = useCallback(
+ (field: keyof FormData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ // khusus nested statistik
+ const handleStatistikChange = useCallback(
+ (field: keyof Statistik, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ statistik: { ...prev.statistik, [field]: value },
+ }));
+ },
+ []
+ );
+
+ const handleSubmit = async () => {
+ try {
+ stateProgram.update.id = id;
+ stateProgram.update.form = formData;
+ await stateProgram.update.update();
+
+ toast.success('Program berhasil diperbarui!');
+ router.push('/admin/ekonomi/program-kemiskinan');
+ } catch (error) {
+ console.error('Error update program:', error);
+ toast.error('Terjadi kesalahan saat memperbarui program');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Program Kemiskinan
+
+
+
+
+
+ handleChange('nama', e.target.value)}
+ label={Judul Program }
+ placeholder="Masukkan judul program"
+ required
+ />
+
+
+
+ Deskripsi
+
+ handleChange('deskripsi', val)}
+ />
+
+
+
+
+ Ikon Program Kreatif Desa
+
+ handleChange('icon', val)}
+ />
+
+
+
+
+ Statistik Jumlah Masyarakat Miskin
+
+ handleStatistikChange('jumlah', e.target.value)}
+ label="Jumlah Masyarakat Miskin"
+ placeholder="Masukkan jumlah masyarakat miskin"
+ required
+ />
+
+ handleStatistikChange('tahun', e.target.value)}
+ label="Tahun"
+ placeholder="Masukkan tahun"
+ required
+ mt="sm"
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditProgramKemiskinan;
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx
new file mode 100644
index 00000000..5e4acf64
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx
@@ -0,0 +1,170 @@
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
+import { IconKey, IconMapper } from '../../../_com/iconMap';
+
+function DetailProgramKemiskinan() {
+ const programState = useProxy(programKemiskinanState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ programState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ programState.delete.delete(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/program-kemiskinan");
+ }
+ };
+
+ if (!programState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = programState.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card utama */}
+
+
+
+ Detail Program Kemiskinan
+
+
+ {/* Detail Content */}
+
+
+
+ Judul Program
+ {data.nama || '-'}
+
+
+
+ Deskripsi Singkat
+
+
+
+
+ Ikon Program
+ {data.icon ? (
+
+ ) : (
+ Tidak ada ikon
+ )}
+
+
+
+ Statistik Jumlah Masyarakat Miskin
+
+
+ Jumlah
+ {data.statistik?.jumlah || '-'}
+
+
+ Tahun
+ {data.statistik?.tahun || '-'}
+
+
+
+
+ {/* Action Buttons */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={programState.delete.loading}
+ >
+
+
+
+
+
+ router.push(`/admin/ekonomi/program-kemiskinan/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus program kemiskinan ini?"
+ />
+
+ );
+}
+
+export default DetailProgramKemiskinan;
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
new file mode 100644
index 00000000..cf4c0696
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
@@ -0,0 +1,172 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
+import CreateEditor from '../../../_com/createEditor';
+import SelectIconProgram from '../../../_com/selectIcon';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+
+function CreateProgramKemiskinan() {
+ const programState = useProxy(programKemiskinanState);
+ const router = useRouter();
+ const [lineChart, setLineChart] = useState([]);
+
+ const resetForm = () => {
+ programState.create.form = {
+ nama: '',
+ deskripsi: '',
+ icon: '',
+ statistik: {
+ tahun: '',
+ jumlah: '',
+ },
+ };
+ };
+
+ const handleSubmit = async () => {
+ if (!programState.create.form.nama || !programState.create.form.deskripsi) {
+ return toast.warn('Judul dan deskripsi wajib diisi');
+ }
+
+ const id = await programState.create.create();
+ if (id) {
+ const idStr = String(id);
+ await programState.findUnique.load(idStr);
+ if (programState.findUnique.data) {
+ setLineChart([programState.findUnique.data]);
+ }
+ toast.success('Program berhasil ditambahkan');
+ } else {
+ toast.error('Gagal menambahkan program, coba lagi');
+ }
+
+ resetForm();
+ router.push('/admin/ekonomi/program-kemiskinan');
+ };
+
+ return (
+
+ {/* Header dengan tombol back */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Program Kemiskinan
+
+
+
+
+
+ {/* Judul Program */}
+ (programState.create.form.nama = val.target.value)}
+ required
+ />
+
+ {/* Ikon Program */}
+
+
+ Ikon Program Kreatif Desa
+
+ (programState.create.form.icon = value)}
+ />
+
+
+ {/* Deskripsi */}
+
+
+ Deskripsi
+
+ {
+ programState.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Statistik */}
+
+ Statistik Jumlah Masyarakat Miskin
+
+
+
+ (programState.create.form.statistik.jumlah = val.target.value)
+ }
+ label="Jumlah"
+ placeholder="Masukkan jumlah masyarakat miskin"
+ required
+ />
+
+ (programState.create.form.statistik.tahun = val.target.value)
+ }
+ label="Tahun"
+ placeholder="Masukkan tahun"
+ required
+ />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateProgramKemiskinan;
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx
new file mode 100644
index 00000000..def7ec5e
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx
@@ -0,0 +1,168 @@
+'use client'
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, XAxis, YAxis } from 'recharts';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../_com/header';
+import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
+
+function ProgramKemiskinan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListProgramKemiskinan({ search }: { search: string }) {
+ const programState = useProxy(programKemiskinanState);
+ const router = useRouter();
+ const [lineChart, setLineChart] = useState([]);
+ const [mounted, setMounted] = useState(false);
+
+ const { data, page, totalPages, loading, load } = programState.findMany;
+
+ useShallowEffect(() => {
+ setMounted(true);
+ load(page, 10, search);
+ }, [page, search]);
+
+ useEffect(() => {
+ if (data) {
+ const chartData = data
+ .filter(item => item.statistik)
+ .map(item => ({
+ tahun: item.statistik?.tahun,
+ jumlah: Number(item.statistik?.jumlah)
+ }))
+ .sort((a, b) => (a.tahun || 0) - (b.tahun || 0));
+
+ setLineChart(chartData);
+ }
+ }, [data]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Program Kemiskinan
+
+ } color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}>
+ Tambah Baru
+
+
+
+
+
+
+
+ Judul Program
+ Deskripsi Singkat
+ Jumlah Masyarakat Miskin
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map(item => (
+
+
+ {item.nama}
+
+
+
+
+ {item.statistik?.jumlah || '-'}
+
+ router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data program kemiskinan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Chart */}
+
+
+ Grafik Berdasarkan Responden
+ {mounted && lineChart.length > 0 ? (
+
+
+
+
+
+ [`${value} orang`, name]}
+ labelFormatter={(label: any) => `Tahun: ${label}`}
+ />
+
+
+
+
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
+
+
+
+ );
+}
+
+export default ProgramKemiskinan;
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
new file mode 100644
index 00000000..f746afeb
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
@@ -0,0 +1,154 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { toast } from 'react-toastify';
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+
+function EditSektorUnggulanDesa() {
+ const router = useRouter();
+ const params = useParams() as { id: string };
+ const stateGrafik = useProxy(grafikSektorUnggulan);
+
+ const id = params.id;
+
+ // state lokal buat form
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ value: 0,
+ });
+
+ // Load data saat komponen mount
+ useEffect(() => {
+ if (id) {
+ stateGrafik.findUnique
+ .load(id)
+ .then(() => {
+ const data = stateGrafik.findUnique.data;
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ description: data.description || '',
+ value: data.value || 0,
+ });
+ }
+ })
+ .catch((err) => {
+ console.error('Error load sektor unggulan:', err);
+ toast.error('Gagal mengambil data sektor unggulan');
+ });
+ }
+ }, [id]);
+
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (e: React.ChangeEvent) => {
+ const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value;
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ stateGrafik.update.id = id;
+ stateGrafik.update.form = { ...formData }; // update global pas submit
+ await stateGrafik.update.submit();
+ toast.success('Sektor unggulan berhasil diperbarui!');
+ router.push('/admin/ekonomi/sektor-unggulan-desa');
+ } catch (error) {
+ console.error('Error update sektor unggulan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui sektor unggulan');
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Sektor Unggulan Desa
+
+
+
+
+
+
+
+
+ Konten
+
+
+ setFormData((prev) => ({ ...prev, description: htmlContent }))
+ }
+ />
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditSektorUnggulanDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx
new file mode 100644
index 00000000..631a4f52
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx
@@ -0,0 +1,142 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
+
+function DetailSektorUnggulanDesa() {
+ const stateGrafik = useProxy(grafikSektorUnggulan);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ stateGrafik.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateGrafik.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/ekonomi/sektor-unggulan-desa');
+ }
+ };
+
+ if (!stateGrafik.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = stateGrafik.findUnique.data;
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Sektor Unggulan Desa
+
+
+
+
+
+ Nama Sektor Unggulan
+ {data.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Jumlah
+ {data.value ?? '-'}
+
+
+ {/* Tombol Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/ekonomi/sektor-unggulan-desa/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus sektor unggulan ini?"
+ />
+
+ );
+}
+
+export default DetailSektorUnggulanDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
new file mode 100644
index 00000000..4761de26
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
+import CreateEditor from '../../../_com/createEditor';
+
+function CreateSektorUnggulanDesa() {
+ const stateGrafik = useProxy(grafikSektorUnggulan);
+ const [chartData, setChartData] = useState([]);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateGrafik.create.form = {
+ name: '',
+ description: '',
+ value: 0,
+ };
+ };
+
+ const handleSubmit = async () => {
+ const id = await stateGrafik.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stateGrafik.findUnique.load(idStr);
+ if (stateGrafik.findUnique.data) {
+ setChartData([stateGrafik.findUnique.data]);
+ }
+ }
+ resetForm();
+ router.push('/admin/ekonomi/sektor-unggulan-desa');
+ };
+
+ return (
+
+ {/* Header dengan back button */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Sektor Unggulan Desa
+
+
+
+ {/* Form */}
+
+
+ {
+ stateGrafik.create.form.name = e.currentTarget.value;
+ }}
+ required
+ />
+
+
+
+ Deskripsi Sektor Unggulan
+
+ {
+ stateGrafik.create.form.description = val;
+ }}
+ />
+
+
+ {
+ stateGrafik.create.form.value = Number(e.currentTarget.value);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateSektorUnggulanDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx
new file mode 100644
index 00000000..3a7cc1e8
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx
@@ -0,0 +1,202 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip as ReTooltip, XAxis, YAxis } from 'recharts';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../_com/header';
+import grafikSektorUnggulan from '../../_state/ekonomi/sektor-unggulan-desa';
+
+function SektorUnggulanDesa() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListSektorUnggulanDesa({ search }: { search: string }) {
+ const router = useRouter();
+ const state = useProxy(grafikSektorUnggulan);
+ const [chartData, setChartData] = useState<
+ { id: string; name: string; description: string | null; value: number | null }[]
+ >([]);
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = state.findMany;
+
+ useEffect(() => {
+ if (state.findMany.data) {
+ setChartData(
+ state.findMany.data.map((item) => ({
+ id: item.id,
+ name: item.name,
+ description: item.description,
+ value: Number(item.value),
+ }))
+ );
+ }
+ }, [state.findMany.data]);
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* List Table */}
+
+
+ List Sektor Unggulan Desa
+
+ } color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}>
+ Tambah Baru
+
+
+
+ {loading ? (
+
+ ) : (
+
+
+
+
+ Nama Sektor
+ Deskripsi
+ Detail
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
+ >
+
+ Detail
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data sektor unggulan yang cocok
+
+
+
+ )}
+
+
+
+ )}
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Chart */}
+
+
+ Grafik Sektor Unggulan Desa
+
+ {loading ? (
+
+ ) : chartData.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ Belum ada data untuk ditampilkan dalam grafik
+
+ )}
+
+
+ );
+}
+
+export default SektorUnggulanDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..4425aa88
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx
@@ -0,0 +1,142 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ ScrollArea,
+ Stack,
+ Tabs,
+ TabsList,
+ TabsPanel,
+ TabsTab,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import {
+ IconBuildingCommunity,
+ IconHierarchy,
+ IconUsers
+} from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "Pegawai",
+ value: "pegawai",
+ href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai",
+ icon: ,
+ tooltip: "Kelola data pegawai BUMDesa",
+ },
+ {
+ label: "Posisi Organisasi",
+ value: "posisiorganisasi",
+ href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
+ icon: ,
+ tooltip: "Kelola daftar posisi organisasi",
+ },
+ {
+ label: "Struktur Organisasi",
+ value: "strukturorganisasi",
+ href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi",
+ icon: ,
+ tooltip: "Kelola struktur organisasi BUMDesa"
+ }
+ ];
+
+ const currentTab = tabs.find((tab) => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(
+ 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 (
+
+
+ Struktur Organisasi & SK Pengurus BUMDesa
+
+
+
+ {/* ✅ Scroll horizontal biar rapi kalau label panjang */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx
new file mode 100644
index 00000000..57a94a4f
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+import LayoutTabs from "./_lib/layoutTabs"
+
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx
new file mode 100644
index 00000000..c1a8eef5
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx
@@ -0,0 +1,269 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+export default function EditPegawaiBumDes() {
+ const router = useRouter();
+ const { id } = useParams<{ id: string }>();
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
+
+ const [formData, setFormData] = useState({
+ namaLengkap: '',
+ gelarAkademik: '',
+ imageId: '',
+ tanggalMasuk: '',
+ email: '',
+ telepon: '',
+ alamat: '',
+ posisiId: '',
+ isActive: true,
+ });
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ // Format date for
+ const formatDateForInput = (dateString: string) => {
+ if (!dateString) return '';
+ const date = new Date(dateString);
+ return date.toISOString().split('T')[0];
+ };
+
+ useEffect(() => {
+ const loadPegawai = async () => {
+ try {
+ await stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
+
+ const data = await stateOrganisasi.edit.load(id);
+ if (data) {
+ setFormData({
+ namaLengkap: data.namaLengkap || '',
+ gelarAkademik: data.gelarAkademik || '',
+ imageId: data.imageId || '',
+ tanggalMasuk: data.tanggalMasuk || '',
+ email: data.email || '',
+ telepon: data.telepon || '',
+ alamat: data.alamat || '',
+ posisiId: data.posisiId || '',
+ isActive: data.isActive ?? true,
+ });
+
+ setPreviewImage(data.image?.link || null);
+ }
+ } catch (error) {
+ console.error('Error loading pegawai:', error);
+ toast.error(error instanceof Error ? error.message : 'Gagal mengambil data pegawai');
+ }
+ };
+
+ loadPegawai();
+ }, [id]);
+
+ const handleSubmit = async () => {
+ try {
+ if (!formData.namaLengkap.trim()) {
+ return toast.error('Nama lengkap tidak boleh kosong');
+ }
+
+ // Update global state only on submit
+ const updatedForm = { ...formData };
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ updatedForm.imageId = uploaded.id;
+ }
+
+ stateOrganisasi.edit.form = updatedForm;
+ if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id;
+
+ const success = await stateOrganisasi.edit.submit();
+ if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai');
+ } catch (error) {
+ console.error('Error updating pegawai:', error);
+ toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+ Edit Data Pegawai PPID
+
+
+
+
+ {/* Nama Lengkap */}
+ setFormData({ ...formData, namaLengkap: e.target.value })}
+ required
+ />
+
+ {/* Gelar Akademik */}
+ setFormData({ ...formData, gelarAkademik: e.target.value })}
+ />
+
+ {/* Foto Profil */}
+
+ Foto Profil
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Tanggal Masuk */}
+ setFormData({ ...formData, tanggalMasuk: e.target.value })}
+ />
+
+ {/* Email */}
+ setFormData({ ...formData, email: e.target.value })}
+ />
+
+ {/* Telepon */}
+ setFormData({ ...formData, telepon: e.target.value })}
+ />
+
+ {/* Alamat */}
+ setFormData({ ...formData, alamat: e.target.value })}
+ />
+
+ {/* Posisi */}
+
+ Posisi
+ ({ value: p.id, label: p.nama })) || []}
+ value={formData.posisiId}
+ onChange={(value) => value && setFormData({ ...formData, posisiId: value })}
+ searchable
+ clearable
+ />
+
+
+ {/* Status Pegawai */}
+
+ Status Pegawai
+ setFormData({ ...formData, isActive: val === 'true' })}
+ clearable
+ />
+
+
+ {/* Submit Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx
new file mode 100644
index 00000000..7a0626e4
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx
@@ -0,0 +1,204 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+import colors from '@/con/colors';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailPegawai() {
+ const statePegawai = useProxy(stateStrukturBumDes.pegawai);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [modalNonActive, setModalNonActive] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ stateStrukturBumDes.posisiOrganisasi.findMany.load();
+ statePegawai.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ statePegawai.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
+ }
+ };
+
+ const handleNonActive = () => {
+ if (selectedId) {
+ statePegawai.nonActive.byId(selectedId);
+ setModalNonActive(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
+ }
+ };
+
+ if (!statePegawai.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = statePegawai.findUnique.data;
+
+ return (
+
+
+ router.back()}>
+
+
+
+
+
+
+ Detail Pegawai PPID
+
+
+
+
+
+ Nama Lengkap
+
+ {data.namaLengkap || '-'} {data.gelarAkademik || ''}
+
+
+
+
+ Posisi
+ {data.posisi?.nama || '-'}
+
+
+
+ Email
+ {data.email || '-'}
+
+
+
+ Telepon
+ {data.telepon || '-'}
+
+
+
+ Alamat
+ {data.alamat || '-'}
+
+
+
+ Tanggal Masuk
+
+ {data.tanggalMasuk ? new Date(data.tanggalMasuk).toLocaleDateString() : '-'}
+
+
+
+
+ Status
+
+ {data.isActive ? 'Aktif' : 'Tidak Aktif'}
+
+
+
+
+ Foto Profil
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada foto profil
+ )}
+
+
+ {
+ setSelectedId(data.id || null);
+ setModalNonActive(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+ {data.isActive ? "Aktif" : "Nonaktif"}
+
+
+
+
+ {
+ setSelectedId(data.id || null);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus data pegawai ini?"
+ />
+
+ {/* Modal NonActive */}
+ setModalNonActive(false)}
+ onConfirm={handleNonActive}
+ text="Apakah Anda yakin ingin menonaktifkan pegawai ini?"
+ />
+
+ );
+}
+
+export default DetailPegawai;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx
new file mode 100644
index 00000000..dbf697ee
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx
@@ -0,0 +1,266 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreatePegawaiBumDes() {
+ const router = useRouter();
+ const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai)
+ useEffect(() => {
+ stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
+ resetForm();
+ }, []);
+
+ const resetForm = () => {
+ stateOrganisasi.create.form = {
+ namaLengkap: "",
+ gelarAkademik: "",
+ imageId: "",
+ tanggalMasuk: "",
+ email: "",
+ telepon: "",
+ alamat: "",
+ posisiId: "",
+ isActive: true,
+ };
+ };
+
+ const handleSubmit = async () => {
+ if (!previewImage) {
+ return toast.warn("Pilih file gambar terlebih dahulu");
+ }
+
+ try {
+ // Upload gambar dulu
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file: previewImage.file,
+ name: previewImage.file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+
+ // Set status aktif secara otomatis
+ stateOrganisasi.create.form.isActive = true;
+
+ // Simpan ID gambar ke form
+ stateOrganisasi.create.form.imageId = uploaded.id;
+
+ // Submit form
+ await stateOrganisasi.create.submit();
+
+
+ // Reset form dan redirect
+ resetForm();
+ toast.success("Data pegawai berhasil ditambahkan");
+ router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
+ } catch (error) {
+ console.error("Error creating pegawai:", error);
+ toast.error("Terjadi kesalahan saat menambahkan pegawai");
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Pegawai BUMDesa
+
+
+
+
+
+
+ (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
+ required
+ />
+
+
+ (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
+ />
+
+
+
+ Foto Profil
+
+ {
+ const file = files[0];
+ if (file) {
+ setPreviewImage({
+ file,
+ preview: URL.createObjectURL(file)
+ });
+ }
+ }}
+ maxSize={5 * 1024 ** 2} // 5MB
+ accept={{
+ 'image/*': ['.jpeg', '.jpg', '.png', '.webp']
+ }}
+ styles={{
+ root: {
+ border: '2px dashed #ced4da',
+ borderRadius: '8px',
+ padding: '20px',
+ textAlign: 'center',
+ cursor: 'pointer',
+ '&:hover': {
+ borderColor: '#228be6',
+ },
+ },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar ke sini atau klik untuk memilih file
+
+
+ Format yang didukung: JPG, PNG, WebP. Maksimal 5MB
+
+
+
+
+
+ {previewImage && (
+
+
+ Preview Gambar
+
+
+
+ )}
+
+
+ (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
+ />
+
+
+
+ (stateOrganisasi.create.form.email = e.currentTarget.value)}
+ />
+
+
+
+ (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
+ />
+
+
+
+ (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
+ />
+
+
+
+
+ Posisi
+
+ ({
+ value: p.id,
+ label: p.nama
+ })) || []}
+ value={stateOrganisasi.create.form.posisiId}
+ onChange={(value) => {
+ if (value) stateOrganisasi.create.form.posisiId = value;
+ }}
+ searchable
+ clearable
+ />
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePegawaiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx
new file mode 100644
index 00000000..d56609de
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx
@@ -0,0 +1,189 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
+import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+
+function PegawaiBumDes() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPegawaiBumdes({ search }: { search: string }) {
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateOrganisasi.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ // Handle loading state
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+ Daftar Pegawai BUMDesa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
+ >
+ Tambah Baru
+
+
+
+
+ Tidak ada data pegawai yang ditemukan
+
+
+
+ );
+ }
+ return (
+
+
+
+ Daftar Pegawai BUMDesa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Lengkap
+ Posisi
+ Status
+ Aksi
+
+
+
+ {(() => {
+ console.log('Rendering table with items:', stateOrganisasi.findMany.data);
+ return null;
+ })()}
+ {([...filteredData]
+ .sort((a, b) => {
+ if (a.isActive === b.isActive) {
+ return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
+ }
+ return Number(b.isActive) - Number(a.isActive); // aktif duluan
+ }) // Aktif di atas
+ ).map((item) => (
+
+
+
+
+ {item.namaLengkap}
+
+
+
+
+
+
+ {item.posisi?.nama || 'Belum diatur'}
+
+
+
+
+
+
+
+ {item.isActive ? "Aktif" : "Tidak Aktif"}
+
+
+
+ {item.isActive ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ }
+ onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}
+ >
+ Detail
+
+
+
+ ))}
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo(0, 0);
+ }}
+ total={totalPages}
+ withEdges
+ withControls
+ radius="md"
+ />
+
+
+
+ );
+}
+
+export default PegawaiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx
new file mode 100644
index 00000000..0d8f7ed7
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx
@@ -0,0 +1,158 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPosisiOrganisasiBumDes() {
+ const router = useRouter();
+ const params = useParams();
+ const id = params?.id as string;
+ const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ deskripsi: '',
+ hierarki: 0,
+ });
+
+ // Fungsi generik untuk update formData
+ const handleChange = (field: keyof typeof formData, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ useEffect(() => {
+ if (!id) return;
+
+ const loadPosisiOrganisasi = async () => {
+ try {
+ const data = await stateOrganisasi.edit.load(id);
+ if (data) {
+ stateOrganisasi.edit.id = id;
+ setFormData({
+ nama: data.nama || '',
+ deskripsi: data.deskripsi || '',
+ hierarki: data.hierarki || 0,
+ });
+ }
+ } catch (err) {
+ console.error('Error loading posisi organisasi:', err);
+ toast.error('Gagal memuat data posisi organisasi');
+ }
+ };
+
+ loadPosisiOrganisasi();
+ }, [id]);
+
+ const handleSubmit = async () => {
+ if (!formData.nama.trim()) {
+ toast.error('Nama posisi organisasi tidak boleh kosong');
+ return;
+ }
+
+ try {
+ // Update global state hanya saat submit
+ stateOrganisasi.edit.form = {
+ nama: formData.nama.trim(),
+ deskripsi: formData.deskripsi.trim(),
+ hierarki: formData.hierarki,
+ };
+
+ if (!stateOrganisasi.edit.id) {
+ stateOrganisasi.edit.id = id;
+ }
+
+ const success = await stateOrganisasi.edit.update();
+
+ if (success) {
+ router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
+ }
+ } catch (err) {
+ console.error('Error updating posisi organisasi:', err);
+ // toast error biasanya sudah ada di update
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Posisi Organisasi BUMDes
+
+
+
+
+
+ handleChange('nama', e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi
+
+ handleChange('deskripsi', html)}
+ />
+
+
+ {
+ const value = parseInt(e.target.value, 10);
+ handleChange('hierarki', isNaN(value) ? 0 : value);
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPosisiOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx
new file mode 100644
index 00000000..2045244c
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx
@@ -0,0 +1,127 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreatePosisiOrganisasiBumDes() {
+ const router = useRouter();
+ const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
+
+ useEffect(() => {
+ stateOrganisasi.findMany.load();
+ // Initialize form with default values
+ stateOrganisasi.create.form = {
+ nama: "",
+ deskripsi: "",
+ hierarki: 0,
+ };
+
+ return () => {
+ // Clean up form on unmount
+ stateOrganisasi.create.form = {
+ nama: "",
+ deskripsi: "",
+ hierarki: 0,
+ };
+ };
+ }, []);
+
+ const handleSubmit = async () => {
+ try {
+ if (!stateOrganisasi.create.form.nama.trim()) {
+ return toast.error('Nama posisi tidak boleh kosong');
+ }
+
+ await stateOrganisasi.create.submit();
+ toast.success('Posisi organisasi berhasil ditambahkan');
+ router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
+ } catch (error) {
+ toast.error('Gagal menambahkan posisi organisasi');
+ console.error('Error:', error);
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Posisi Organisasi BUMDes
+
+
+
+
+
+ (stateOrganisasi.create.form.nama = e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi
+
+ {
+ stateOrganisasi.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+ {
+ const value = parseInt(e.target.value, 10);
+ stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
+ }}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePosisiOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx
new file mode 100644
index 00000000..ea181a8f
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx
@@ -0,0 +1,175 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+
+function PosisiOrganisasiBumDes() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
+ const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi)
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateOrganisasi.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const handleHapus = async () => {
+ if (selectedId) {
+ await stateOrganisasi.delete.byId(selectedId);
+ setModalHapus(false)
+ setSelectedId(null)
+ }
+ }
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Posisi Organisasi BumDes
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Posisi
+ Deskripsi
+ Hierarki
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+ {item.nama}
+
+
+
+
+
+
+
+ {item.hierarki || '-'}
+
+
+
+ router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`)}
+ >
+
+
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data posisi organisasi yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
+ />
+
+ );
+}
+
+export default PosisiOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx
new file mode 100644
index 00000000..be24d78b
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx
@@ -0,0 +1,133 @@
+/* eslint-disable prefer-const */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import { Box, Center, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core';
+import { IconUsers } from '@tabler/icons-react';
+import { OrganizationChart } from 'primereact/organizationchart';
+import { useEffect } from 'react';
+import { useProxy } from 'valtio/utils';
+import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+function StrukturOrganisasiBumDes() {
+ return (
+
+
+
+ );
+}
+
+function ListStrukturOrganisasiBumDes() {
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
+
+ useEffect(() => {
+ stateOrganisasi.findMany.load();
+ }, []);
+
+ if (stateOrganisasi.findMany.loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
+ return (
+
+
+ Belum ada struktur organisasi yang ditambahkan
+
+ );
+ }
+
+ const posisiMap = new Map();
+
+ const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
+
+ for (const pegawai of aktifPegawai) {
+ const posisiId = pegawai.posisi.id;
+ if (!posisiMap.has(posisiId)) {
+ posisiMap.set(posisiId, {
+ ...pegawai.posisi,
+ pegawaiList: [],
+ children: [],
+ });
+ }
+ posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
+ }
+
+ let root: any[] = [];
+ posisiMap.forEach((posisi) => {
+ if (posisi.parentId) {
+ const parent = posisiMap.get(posisi.parentId);
+ if (parent) {
+ parent.children.push(posisi);
+ }
+ } else {
+ root.push(posisi);
+ }
+ });
+
+ function toOrgChartFormat(node: any): any {
+ return {
+ expanded: true,
+ type: 'person',
+ styleClass: 'p-person',
+ data: {
+ name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
+ status: node.nama,
+ image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
+ },
+ children: node.children.map(toOrgChartFormat),
+ };
+ }
+
+ const chartData = root.map(toOrgChartFormat);
+
+ return (
+
+
+
+
+
+ );
+}
+
+function nodeTemplate(node: any) {
+ const imageSrc = node?.data?.image || '/img/default.png';
+ const name = node?.data?.name || 'Tanpa Nama';
+ const status = node?.data?.status || 'Tidak ada deskripsi';
+
+ return (
+
+
+
+
+ {name}
+ {status}
+
+ );
+}
+
+export default StrukturOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx
new file mode 100644
index 00000000..f1857aec
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx
@@ -0,0 +1,135 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, 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 ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif';
+
+function DetailAjukanIdeInofativDesa() {
+ const state = useProxy(ajukanIdeInovatifState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/inovasi/ajukan-ide-inovatif");
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card Utama */}
+
+
+ {/* Header */}
+
+
+ Detail Ajukan Ide Inovatif Desa
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={state.delete.loading}
+ >
+
+
+
+
+
+ {/* Detail Data */}
+
+
+
+ Nama
+ {data?.name || '-'}
+
+
+
+ Alamat
+
+
+
+
+ Nama Ide Inovatif
+ {data?.namaIde || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Masalah
+ {data?.masalah || '-'}
+
+
+
+ Benefit
+ {data?.benefit || '-'}
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus ajukan ide inovatif ini?"
+ />
+
+ );
+}
+
+export default DetailAjukanIdeInofativDesa;
diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx
new file mode 100644
index 00000000..24bdbd06
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx
@@ -0,0 +1,142 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 ajukanIdeInovatifState from '../../_state/inovasi/ajukan-ide-inovatif';
+
+function AjukanIdeInovatif() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListAjukanIdeInovatif({ search }: { search: string }) {
+ const state = useProxy(ajukanIdeInovatifState)
+ const router = useRouter()
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ Daftar Ide Inovatif
+
+
+
+
+ Nama
+ Alamat
+ Nama Ide Inovatif
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+ }
+ onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada ide inovatif yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default AjukanIdeInovatif;
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
new file mode 100644
index 00000000..05c93b59
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
@@ -0,0 +1,210 @@
+'use client';
+/* eslint-disable react-hooks/exhaustive-deps */
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditDigitalSmartVillage() {
+ const stateDesaDigital = useProxy(desaDigitalState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ });
+
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateDesaDigital.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data?.image?.link) setPreviewImage(data.image.link);
+ }
+ } catch (error) {
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data desa digital smart village');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+
+ stateDesaDigital.edit.form.imageId = uploaded.id;
+ }
+
+ await stateDesaDigital.edit.update();
+ toast.success('Desa digital smart village berhasil diperbarui!');
+ router.push('/admin/inovasi/desa-digital-smart-village');
+ } catch (error) {
+ console.error('Error updating desa digital:', error);
+ toast.error('Terjadi kesalahan saat memperbarui data');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Desa Digital Smart Village
+
+
+
+ {/* Form Card */}
+
+
+ {/* Dropzone Upload */}
+
+
+ Gambar Desa Digital
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Input Judul */}
+ setFormData({ ...formData, name: e.target.value })}
+ required
+ />
+
+ {/* Editor Deskripsi */}
+
+
+ Deskripsi
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditDigitalSmartVillage;
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx
new file mode 100644
index 00000000..c82485e0
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx
@@ -0,0 +1,158 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import desaDigitalState from '../../../_state/inovasi/desa-digital';
+
+function DetailDesaDigital() {
+ const stateDesaDigital = useProxy(desaDigitalState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ stateDesaDigital.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateDesaDigital.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/inovasi/desa-digital-smart-village");
+ }
+ };
+
+ if (!stateDesaDigital.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = stateDesaDigital.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card Utama */}
+
+
+
+ Detail Desa Digital Smart Village
+
+
+ {/* Sub Card Detail */}
+
+
+
+ Judul
+ {data?.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data?.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+ {/* Tombol Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/inovasi/desa-digital-smart-village/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus desa digital smart village ini?"
+ />
+
+ );
+}
+
+export default DetailDesaDigital;
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
new file mode 100644
index 00000000..6a00b669
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
@@ -0,0 +1,221 @@
+'use client';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import desaDigitalState from '../../../_state/inovasi/desa-digital';
+import { Dropzone } from '@mantine/dropzone';
+
+export default function CreateDesaDigital() {
+ const stateDesaDigital = useProxy(desaDigitalState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateDesaDigital.create.form = {
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ try {
+ const uploadRes = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = uploadRes.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar');
+ }
+
+ stateDesaDigital.create.form.imageId = uploaded.id;
+
+ const success = await stateDesaDigital.create.create();
+ if (success) {
+ resetForm();
+ router.push('/admin/inovasi/desa-digital-smart-village');
+ }
+ } catch (error) {
+ console.error('Error in handleSubmit:', error);
+ toast.error('Terjadi kesalahan saat menyimpan data');
+ }
+ };
+
+ return (
+
+ {/* Header dengan tombol kembali */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ style={{ transition: 'background 0.2s ease' }}
+ >
+
+
+
+
+ Tambah Desa Digital Smart Village
+
+
+
+ {/* Card Form */}
+
+
+ {/* Input Nama */}
+ (stateDesaDigital.create.form.name = e.target.value)}
+ radius="md"
+ withAsterisk
+ />
+
+ {/* Deskripsi */}
+
+
+ Deskripsi
+
+ {
+ stateDesaDigital.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar Desa Digital
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ style={{
+ border: '2px dashed #cfd8dc',
+ backgroundColor: '#fafafa',
+ transition: 'background-color 0.2s ease, border-color 0.2s ease',
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {/* Preview */}
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Tombol Submit */}
+
+ {
+ (e.currentTarget.style.transform = 'translateY(-2px)');
+ (e.currentTarget.style.boxShadow =
+ '0 6px 20px rgba(79, 172, 254, 0.5)');
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget.style.transform = 'translateY(0)');
+ (e.currentTarget.style.boxShadow =
+ '0 4px 15px rgba(79, 172, 254, 0.4)');
+ }}
+ >
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx
new file mode 100644
index 00000000..84cb3baf
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx
@@ -0,0 +1,165 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 desaDigitalState from '../../_state/inovasi/desa-digital';
+
+function DesaDigitalSmartVillage() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListDesaDigitalSmartVillage({ search }: { search: string }) {
+ const state = useProxy(desaDigitalState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ List Desa Digital Smart Village
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/inovasi/desa-digital-smart-village/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Inovasi
+
+ Deskripsi Singkat Inovasi
+
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+
+ router.push(
+ `/admin/inovasi/desa-digital-smart-village/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data inovasi digital yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default DesaDigitalSmartVillage;
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx
new file mode 100644
index 00000000..e7d83362
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx
@@ -0,0 +1,223 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditInfoTeknologiTepatGuna() {
+ const stateInfoTekno = useProxy(infoTeknoState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ });
+
+ // Load data pertama kali
+ useEffect(() => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ const loadData = async () => {
+ try {
+ const data = await stateInfoTekno.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data?.image?.link) setPreviewImage(data.image.link);
+ }
+ } catch (error) {
+ console.error('Error loading info teknologi tepat guna:', error);
+ toast.error('Gagal memuat data info teknologi tepat guna');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ // Submit form
+ const handleSubmit = async () => {
+ try {
+ // sync local → global pas submit
+ stateInfoTekno.edit.form = {
+ ...stateInfoTekno.edit.form,
+ ...formData,
+ };
+
+ // upload file kalau ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ stateInfoTekno.edit.form.imageId = uploaded.id;
+ }
+
+ await stateInfoTekno.edit.update();
+ toast.success('Info teknologi tepat guna berhasil diperbarui!');
+ router.push('/admin/inovasi/info-teknologi-tepat-guna');
+ } catch (error) {
+ console.error('Error updating info teknologi tepat guna:', error);
+ toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna');
+ }
+ };
+
+ return (
+
+ {/* Tombol back + title */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Info Teknologi Tepat Guna
+
+
+
+ {/* Card form */}
+
+
+ {/* Input Judul */}
+ setFormData((prev) => ({ ...prev, name: e.target.value }))}
+ required
+ />
+
+ {/* Upload gambar */}
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Deskripsi pakai editor */}
+
+
+ Deskripsi
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+ {/* Tombol submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditInfoTeknologiTepatGuna;
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx
new file mode 100644
index 00000000..8e9d2dab
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx
@@ -0,0 +1,150 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import infoTeknoState from '../../../_state/inovasi/info-tekno';
+
+function DetailInfoTeknologiTepatGuna() {
+ const stateInfoTekno = useProxy(infoTeknoState)
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter()
+ const params = useParams()
+
+ useShallowEffect(() => {
+ stateInfoTekno.findUnique.load(params?.id as string)
+ }, [])
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateInfoTekno.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/inovasi/info-teknologi-tepat-guna")
+ }
+ }
+
+ if (!stateInfoTekno.findUnique.data) {
+ return (
+
+
+
+ )
+ }
+
+ const data = stateInfoTekno.findUnique.data
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card Utama */}
+
+
+
+ Detail Info Teknologi Tepat Guna
+
+
+
+
+
+ Judul
+ {data?.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data?.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+ {/* Action Buttons */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={stateInfoTekno.delete.loading}
+ >
+
+
+
+
+
+
+ router.push(`/admin/inovasi/info-teknologi-tepat-guna/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text='Apakah anda yakin ingin menghapus info teknologi tepat guna ini?'
+ />
+
+ );
+}
+
+export default DetailInfoTeknologiTepatGuna;
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
new file mode 100644
index 00000000..be8b23b7
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
@@ -0,0 +1,188 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import infoTeknoState from '../../../_state/inovasi/info-tekno';
+import { Dropzone } from '@mantine/dropzone';
+
+function CreateInfoTeknologiTepatGuna() {
+ const stateInfoTekno = useProxy(infoTeknoState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateInfoTekno.create.form = {
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.error('Silahkan pilih file gambar terlebih dahulu');
+ }
+
+ try {
+ const uploadRes = await ApiFetch.api.fileStorage.create.post({
+ file: file,
+ name: file.name,
+ });
+
+ const uploaded = uploadRes.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ stateInfoTekno.create.form.imageId = uploaded.id;
+
+ const success = await stateInfoTekno.create.create();
+
+ if (success) {
+ resetForm();
+ router.push('/admin/inovasi/info-teknologi-tepat-guna');
+ }
+ } catch (error) {
+ console.error('Error in handleSubmit:', error);
+ toast.error('Terjadi kesalahan saat menyimpan data');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Info Teknologi Tepat Guna
+
+
+
+ {/* Form Card */}
+
+
+ {/* Nama */}
+ {
+ stateInfoTekno.create.form.name = val.target.value;
+ }}
+ label="Nama Info Teknologi Tepat Guna"
+ placeholder="Masukkan nama info teknologi tepat guna"
+ required
+ />
+
+ {/* Deskripsi */}
+
+
+ Deskripsi
+
+ {
+ stateInfoTekno.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Submit Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateInfoTeknologiTepatGuna;
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx
new file mode 100644
index 00000000..3fcb8f8a
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx
@@ -0,0 +1,170 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Group,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 infoTeknoState from '../../_state/inovasi/info-tekno';
+
+function InfoTeknologiTepatGuna() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
+ const state = useProxy(infoTeknoState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Info Teknologi Tepat Guna
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/inovasi/info-teknologi-tepat-guna/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+
+ Nama Info Teknologi
+
+
+ Deskripsi Singkat
+
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+
+
+
+
+ }
+ onClick={() =>
+ router.push(
+ `/admin/inovasi/info-teknologi-tepat-guna/${item.id}`,
+ )
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data Info Teknologi yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default InfoTeknologiTepatGuna;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..781f1ec5
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx
@@ -0,0 +1,118 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { IconListDetails, IconUsers } from '@tabler/icons-react';
+
+function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "List Kolaborasi Inovasi",
+ value: "listkolaborasiinovasi",
+ href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi",
+ tooltip: "Lihat daftar kolaborasi inovasi",
+ icon: ,
+ },
+ {
+ label: "Mitra Kolaborasi",
+ value: "mitarakolaborasi",
+ href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi",
+ tooltip: "Kelola mitra kolaborasi",
+ icon: ,
+ }
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+
+ Kolaborasi Inovasi
+
+
+ {/* ✅ Scroll horizontal wrapper biar rapi */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsKolaborasi;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/layout.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/layout.tsx
new file mode 100644
index 00000000..5e639e29
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import LayoutTabsKolaborasi from './_lib/layoutTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
new file mode 100644
index 00000000..cce425a1
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
@@ -0,0 +1,173 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
+import kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi";
+import colors from "@/con/colors";
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { IconArrowBack } from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+function EditKolaborasiInovasi() {
+ const kolaborasiState = useProxy(kolaborasiInovasiState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: "",
+ deskripsi: "",
+ tahun: "",
+ slug: "",
+ kolaborator: "",
+ });
+
+ // Load data awal dari server
+ useEffect(() => {
+ const loadKolaborasi = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await kolaborasiState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name ?? "",
+ deskripsi: data.deskripsi ?? "",
+ tahun: data.tahun?.toString() ?? "",
+ slug: data.slug ?? "",
+ kolaborator: data.kolaborator ?? "",
+ });
+ }
+ } catch (error) {
+ console.error("Error loading kolaborasi:", error);
+ toast.error("Gagal memuat data kolaborasi inovasi");
+ }
+ };
+
+ loadKolaborasi();
+ }, [params?.id]);
+
+ // Handler submit → baru update global state
+ const handleSubmit = async () => {
+ try {
+ kolaborasiState.update.form = {
+ ...kolaborasiState.update.form,
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ tahun: Number(formData.tahun),
+ slug: formData.slug,
+ kolaborator: formData.kolaborator,
+ };
+
+ await kolaborasiState.update.submit();
+ toast.success("Kolaborasi inovasi berhasil diperbarui!");
+ router.push("/admin/inovasi/kolaborasi-inovasi");
+ } catch (error) {
+ console.error("Error updating kolaborasi:", error);
+ toast.error("Terjadi kesalahan saat memperbarui data");
+ }
+ };
+
+ // Handler input (biar lebih DRY)
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Kolaborasi Inovasi
+
+
+
+
+
+ handleChange("name", e.target.value)}
+ required
+ />
+
+ handleChange("slug", e.target.value)}
+ required
+ />
+
+ handleChange("tahun", e.target.value)}
+ required
+ />
+
+ handleChange("kolaborator", e.target.value)}
+ required
+ />
+
+
+
+ Konten
+
+ handleChange("deskripsi", htmlContent)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKolaborasiInovasi;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx
new file mode 100644
index 00000000..0d3b0f9a
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx
@@ -0,0 +1,143 @@
+'use client'
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
+import colors from '@/con/colors';
+
+function DetailKolaborasiInovasi() {
+ const kolaborasiState = useProxy(kolaborasiInovasiState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ kolaborasiState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ kolaborasiState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
+ }
+ };
+
+ if (!kolaborasiState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = kolaborasiState.findUnique.data;
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card utama */}
+
+
+
+ Detail Kolaborasi Inovasi
+
+
+ {/* Isi detail */}
+
+
+
+ Nama Kolaborasi Inovasi
+ {data?.name || '-'}
+
+
+
+ Tahun
+ {data?.tahun || '-'}
+
+
+
+ Deskripsi Singkat
+ {data?.slug || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Kolaborator
+ {data?.kolaborator || '-'}
+
+
+ {/* Tombol aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={kolaborasiState.delete.loading}
+ >
+
+
+
+
+
+ router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal konfirmasi hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus kolaborasi inovasi ini?"
+ />
+
+ );
+}
+
+export default DetailKolaborasiInovasi;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
new file mode 100644
index 00000000..8ed78d71
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
@@ -0,0 +1,133 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { YearPickerInput } from '@mantine/dates';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreateProgramKreatifDesa() {
+ const stateCreate = useProxy(kolaborasiInovasiState);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateCreate.create.form = {
+ name: "",
+ tahun: 0,
+ slug: "",
+ deskripsi: "",
+ kolaborator: "",
+ };
+ };
+
+ // Generate slug dari name
+ useEffect(() => {
+ const { name } = stateCreate.create.form;
+ if (name) {
+ const slug = name
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, '')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-');
+ stateCreate.create.form.slug = slug;
+ }
+ }, [stateCreate.create.form.name]);
+
+ const handleSubmit = async () => {
+ try {
+ await stateCreate.create.create();
+ resetForm();
+ router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
+ toast.success("Berhasil menambahkan kolaborasi inovasi");
+ } catch (error) {
+ console.error("Error creating kolaborasi inovasi:", error);
+ toast.error("Terjadi kesalahan saat menyimpan data");
+ }
+ };
+
+ return (
+
+ {/* Back Button */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Kolaborasi Inovasi
+
+
+
+ {/* Form */}
+
+
+ Nama Kolaborasi Inovasi}
+ placeholder="Masukkan nama kolaborasi inovasi"
+ defaultValue={stateCreate.create.form.name || ''}
+ onChange={(val) => stateCreate.create.form.name = val.target.value}
+ required
+ />
+
+ Tahun}
+ placeholder="Pilih tahun"
+ onChange={(date) => {
+ const year = date ? new Date(date).getFullYear() : 0;
+ stateCreate.create.form.tahun = year;
+ }}
+ />
+
+
+
+ Deskripsi
+
+ stateCreate.create.form.deskripsi = val}
+ />
+
+
+ Kolaborator}
+ placeholder="Masukkan kolaborator"
+ defaultValue={stateCreate.create.form.kolaborator || ''}
+ onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateProgramKreatifDesa;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx
new file mode 100644
index 00000000..f0899d36
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx
@@ -0,0 +1,160 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
+import HeaderSearch from '../../../_com/header';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
+import { useProxy } from 'valtio/utils';
+
+function KolaborasiInovasi() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKolaborasiInovasi({ search }: { search: string }) {
+ const listState = useProxy(kolaborasiInovasiState);
+ const router = useRouter();
+
+ const { data, loading, page, totalPages, load } = listState.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Kolaborasi Inovasi
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ No
+ Nama Kolaborasi Inovasi
+ Tahun
+ Deskripsi Singkat
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item, index) => (
+
+ {index + 1}
+
+
+ {item.name}
+
+
+
+
+ {item.tahun}
+
+
+
+
+ {item.slug}
+
+
+
+ }
+ onClick={() =>
+ router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`)
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data kolaborasi inovasi yang tersedia
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default KolaborasiInovasi;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
new file mode 100644
index 00000000..a1762db1
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
@@ -0,0 +1,228 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import {
+ IconArrowBack,
+ IconImageInPicture,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditMitraKolaborasi() {
+ const state = useProxy(mitraKolaborasi);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ // Local form state (controlled)
+ const [formData, setFormData] = useState({
+ name: '',
+ imageId: '',
+ });
+
+ // Load data ke state lokal sekali saja
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await state.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data?.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (error) {
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data mitra');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ const handleChange = (key: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [key]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // upload file jika ada
+ let imageId = formData.imageId;
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+ imageId = uploaded.id;
+ }
+
+ // update global state hanya saat submit
+ state.update.form = {
+ ...state.update.form,
+ name: formData.name,
+ imageId,
+ };
+
+ await state.update.update();
+ toast.success('Mitra berhasil diperbarui!');
+ router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
+ } catch (error) {
+ console.error('Error updating mitra:', error);
+ toast.error('Terjadi kesalahan saat memperbarui mitra');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Mitra
+
+
+
+ {/* Form Card */}
+
+
+ {/* Input Nama */}
+ handleChange('name', e.target.value)}
+ required
+ />
+
+ {/* Upload Foto */}
+
+
+ Upload Foto
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {/* Preview Foto */}
+ {previewImage ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditMitraKolaborasi;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
new file mode 100644
index 00000000..d7da8e8b
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
@@ -0,0 +1,162 @@
+'use client'
+import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreateMitraKolaborasi() {
+ const state = useProxy(mitraKolaborasi);
+ const router = useRouter();
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const resetForm = () => {
+ state.create.form = {
+ name: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ state.create.form.imageId = uploaded.id;
+ await state.create.create();
+ resetForm();
+ router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
+ };
+
+ return (
+
+ {/* Back Button + Title */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Mitra Kolaborasi
+
+
+
+ {/* Card Wrapper */}
+
+
+ {/* Input Nama Mitra */}
+ (state.create.form.name = e.target.value)}
+ required
+ />
+
+ {/* Upload Image */}
+
+
+ Gambar Mitra
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Submit Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateMitraKolaborasi;
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx
new file mode 100644
index 00000000..a2d6cbbb
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx
@@ -0,0 +1,215 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Image,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconEdit, IconSearch, IconX, IconPlus } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function MitraKolaborasi() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListMitraKolaborasi({ search }: { search: string }) {
+ const listState = useProxy(mitraKolaborasi);
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ mitraKolaborasi.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/inovasi/kolaborasi-inovasi');
+ }
+ };
+
+ const { data, loading, page, totalPages, load } = listState.findMany;
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Mitra Kolaborasi
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ No
+ Nama Mitra
+ Image
+ Delete
+ Edit
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item, index) => (
+
+ {index + 1}
+
+
+ {item.name}
+
+
+
+
+ {item.image?.link ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+
+
+ router.push(
+ `/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
+ )
+ }
+ >
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data mitra kolaborasi yang tersedia
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus mitra kolaborasi ini?"
+ />
+
+ );
+}
+
+export default MitraKolaborasi;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..55a8cdb2
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx
@@ -0,0 +1,148 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ ScrollArea,
+ Stack,
+ Tabs,
+ TabsList,
+ TabsPanel,
+ TabsTab,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import {
+ IconFileText,
+ IconListDetails,
+ IconMessage,
+ IconAlertCircle
+} from '@tabler/icons-react';
+
+function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ // ✅ Tambahin icon + tooltip biar konsisten sama versi berita
+ const tabs = [
+ {
+ label: "Administrasi Online",
+ value: "administrasionline",
+ href: "/admin/inovasi/layanan-online-desa/administrasi-online",
+ icon: ,
+ tooltip: "Kelola administrasi online desa"
+ },
+ {
+ label: "Jenis Layanan",
+ value: "jenislayanan",
+ href: "/admin/inovasi/layanan-online-desa/jenis-layanan",
+ icon: ,
+ tooltip: "Daftar jenis layanan desa"
+ },
+ {
+ label: "Pengaduan Masyarakat",
+ value: "pengaduanmasyarakat",
+ href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat",
+ icon: ,
+ tooltip: "Laporan pengaduan masyarakat"
+ },
+ {
+ label: "Jenis Pengaduan",
+ value: "jenispengaduan",
+ href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan",
+ icon: ,
+ tooltip: "Kategori/jenis pengaduan masyarakat"
+ }
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+
+ Layanan Online Desa
+
+
+
+ {/* ✅ Scroll horizontal biar gak overflow */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ <>{children}>
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabsLayananOnlineDesa;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx
new file mode 100644
index 00000000..b08d0582
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx
@@ -0,0 +1,123 @@
+'use client'
+
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+
+function DetailAdministrasiOnline() {
+ const stateAdminOnline = useProxy(layananonlineDesa.administrasiOnline);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ stateAdminOnline.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateAdminOnline.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/inovasi/layanan-online-desa/administrasi-online");
+ }
+ };
+
+ if (!stateAdminOnline.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = stateAdminOnline.findUnique.data;
+
+ return (
+
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ >
+ Kembali
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+ {/* Konten Detail */}
+
+
+
+ Detail Administrasi Online
+
+
+
+
+
+ Nama
+ {data.name || '-'}
+
+
+
+ Alamat
+ {data.alamat || '-'}
+
+
+
+ Nomor Telepon
+ {data.nomorTelepon || '-'}
+
+
+
+ Jenis Layanan
+ {data.jenisLayanan?.nama || '-'}
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus administrasi online ini?"
+ />
+
+ );
+}
+
+export default DetailAdministrasiOnline;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/page.tsx
new file mode 100644
index 00000000..b3f7a4f6
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/page.tsx
@@ -0,0 +1,148 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
+
+function AdministrasiOnline() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListAdministrasiOnline({ search }: { search: string }) {
+ const state = useProxy(layananonlineDesa.administrasiOnline);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Daftar Administrasi Online
+
+
+
+
+
+ Nama
+ Alamat
+ Nomor Telepon
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+
+
+ {item.alamat}
+
+
+
+
+ {item.nomorTelepon || '-'}
+
+
+
+ }
+ onClick={() =>
+ router.push(
+ `/admin/inovasi/layanan-online-desa/administrasi-online/${item.id}`
+ )
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data administrasi online yang cocok
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default AdministrasiOnline;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx
new file mode 100644
index 00000000..80459e66
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx
@@ -0,0 +1,148 @@
+'use client';
+/* eslint-disable react-hooks/exhaustive-deps */
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditJenisLayanan() {
+ const state = useProxy(layananonlineDesa.jenisLayanan);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ deskripsi: '',
+ });
+
+ useEffect(() => {
+ const loadJenisLayanan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await state.edit.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama ?? '',
+ deskripsi: data.deskripsi ?? '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading jenis layanan:', error);
+ toast.error('Gagal memuat data jenis layanan');
+ }
+ };
+
+ loadJenisLayanan();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ state.edit.form = {
+ ...state.edit.form,
+ ...formData,
+ };
+
+ await state.edit.update();
+ toast.success('Jenis layanan berhasil diperbarui!');
+ router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
+ } catch (error) {
+ console.error('Error updating jenis layanan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui jenis layanan');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Jenis Layanan
+
+
+
+ {/* Form Container */}
+
+
+ {/* Input: Nama Jenis Layanan */}
+
+ setFormData((prev) => ({ ...prev, nama: e.target.value }))
+ }
+ required
+ />
+
+ {/* Input: Deskripsi (Rich Text Editor) */}
+
+
+ Deskripsi
+
+
+ setFormData((prev) => ({
+ ...prev,
+ deskripsi: htmlContent,
+ }))
+ }
+ />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditJenisLayanan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx
new file mode 100644
index 00000000..41990ab2
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx
@@ -0,0 +1,132 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailJenisLayanan() {
+ const state = useProxy(layananonlineDesa.jenisLayanan)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const params = useParams()
+ const router = useRouter()
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string)
+ }, [])
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
+ }
+ }
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ )
+ }
+
+ const data = state.findUnique.data
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card utama */}
+
+
+
+ Detail Jenis Layanan
+
+
+ {/* Detail isi */}
+
+
+
+ Nama
+ {data?.nama || '-'}
+
+
+
+ Deskripsi
+
+
+
+ {/* Tombol aksi */}
+
+
+ {
+ setSelectedId(data.id)
+ setModalHapus(true)
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={state.delete.loading}
+ >
+
+
+
+
+
+ router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text='Apakah Anda yakin ingin menghapus jenis layanan ini?'
+ />
+
+ );
+}
+
+export default DetailJenisLayanan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx
new file mode 100644
index 00000000..b1b5a107
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx
@@ -0,0 +1,110 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function CreateJenisLayanan() {
+ const router = useRouter();
+ const statePasar = useProxy(layananonlineDesa.jenisLayanan);
+
+ useEffect(() => {
+ statePasar.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ statePasar.create.form = {
+ nama: '',
+ deskripsi: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await statePasar.create.create();
+ resetForm();
+ router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
+ };
+
+ return (
+
+ {/* Header dengan tombol back */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Jenis Layanan
+
+
+
+ {/* Form */}
+
+
+ {
+ statePasar.create.form.nama = val.target.value;
+ }}
+ label={Nama Jenis Layanan }
+ placeholder="Masukkan nama jenis layanan"
+ required
+ />
+ {
+ statePasar.create.form.deskripsi = val.target.value;
+ }}
+ label={Deskripsi }
+ placeholder="Masukkan deskripsi"
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateJenisLayanan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx
new file mode 100644
index 00000000..ba176cd6
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx
@@ -0,0 +1,160 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
+
+function JenisLayanan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListJenisLayanan({ search }: { search: string }) {
+ const stateList = useProxy(layananonlineDesa.jenisLayanan);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = stateList.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Jenis Layanan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/layanan-online-desa/jenis-layanan/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Jenis Layanan
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+
+ {item.deskripsi || '-'}
+
+
+
+ }
+ onClick={() =>
+ router.push(
+ `/admin/inovasi/layanan-online-desa/jenis-layanan/${item.id}`
+ )
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada jenis layanan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default JenisLayanan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx
new file mode 100644
index 00000000..c64bb102
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx
@@ -0,0 +1,146 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditJenisPengaduan() {
+ const router = useRouter();
+ const params = useParams();
+ const id = params?.id as string;
+ const state = useProxy(layananonlineDesa.jenisPengaduan);
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ });
+
+ // Load data sekali aja
+ useEffect(() => {
+ const loadJenisPengaduan = async () => {
+ if (!id) return;
+
+ try {
+ const data = await state.edit.load(id);
+
+ if (data) {
+ state.edit.id = id; // inject id ke state global (hanya sekali)
+ setFormData({
+ nama: data.nama || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading jenis pengaduan:', error);
+ toast.error('Gagal memuat data jenis pengaduan');
+ }
+ };
+
+ loadJenisPengaduan();
+ }, [id]);
+
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ if (!formData.nama.trim()) {
+ toast.error('Nama jenis pengaduan tidak boleh kosong');
+ return;
+ }
+
+ // Update ke global state HANYA pas submit
+ state.edit.form = {
+ nama: formData.nama.trim(),
+ };
+
+ // Safety fallback kalau ID belum ada
+ if (!state.edit.id) {
+ state.edit.id = id;
+ }
+
+ try {
+ const success = await state.edit.update();
+
+ if (success) {
+ router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
+ }
+ } catch (error) {
+ console.error('Error updating jenis pengaduan:', error);
+ // toast ditangani di dalam state.update
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Jenis Pengaduan
+
+
+
+ {/* Form */}
+
+
+ handleChange('nama', e.target.value)}
+ label="Nama Jenis Pengaduan"
+ placeholder="Masukkan nama jenis pengaduan"
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditJenisPengaduan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx
new file mode 100644
index 00000000..bb0ae5b9
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx
@@ -0,0 +1,92 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function CreateJenisPengaduan() {
+ const router = useRouter();
+ const state = useProxy(layananonlineDesa.jenisPengaduan);
+
+ useEffect(() => {
+ state.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ state.create.form = {
+ nama: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await state.create.create();
+ resetForm();
+ router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Jenis Pengaduan
+
+
+
+ {/* Form */}
+
+
+ (state.create.form.nama = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateJenisPengaduan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx
new file mode 100644
index 00000000..4bbee3b6
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx
@@ -0,0 +1,191 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
+
+function JenisPengaduan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListJenisPengaduan({ search }: { search: string }) {
+ const state = useProxy(layananonlineDesa.jenisPengaduan);
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ const { data, page, totalPages, loading, load, } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const handleHapus = async () => {
+ if (selectedId) {
+ await state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ }
+ };
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Jenis Pengaduan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Jenis Pengaduan
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+ }
+ onClick={() =>
+ router.push(
+ `/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`
+ )
+ }
+ >
+ Edit
+
+
+
+ }
+ onClick={() => {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+ Hapus
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada jenis pengaduan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus jenis pengaduan ini?"
+ />
+
+ );
+}
+
+export default JenisPengaduan;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx
new file mode 100644
index 00000000..dcb53527
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx
@@ -0,0 +1,12 @@
+'use client'
+import LayoutTabsLayananOnlineDesa from "./_lib/layoutTabs";
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx
new file mode 100644
index 00000000..a9a9ad3e
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx
@@ -0,0 +1,164 @@
+'use client'
+import { useProxy } from 'valtio/utils';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
+import colors from '@/con/colors';
+
+function DetailPengaduanMasyarakat() {
+ const pengaduanState = useProxy(layananonlineDesa);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ pengaduanState.pengaduanMasyarakat.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat");
+ }
+ };
+
+ if (!pengaduanState.pengaduanMasyarakat.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = pengaduanState.pengaduanMasyarakat.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card Detail */}
+
+
+ {/* Judul Halaman */}
+
+ Detail Pengaduan Masyarakat
+
+
+ {/* Isi Data */}
+
+
+
+ Nama
+ {data?.name || '-'}
+
+
+
+ Email
+ {data?.email || '-'}
+
+
+
+ Nomor Telepon
+ {data?.nomorTelepon || '-'}
+
+
+
+ NIK
+ {data?.nik || '-'}
+
+
+
+ Judul Pengaduan
+ {data?.judulPengaduan || '-'}
+
+
+
+ Lokasi Kejadian
+ {data?.lokasiKejadian || '-'}
+
+
+
+ Deskripsi Pengaduan
+
+
+
+
+ Jenis Pengaduan
+ {data?.jenisPengaduan?.nama || '-'}
+
+
+
+ Gambar
+ {data?.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+ {/* Action Button */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={pengaduanState.pengaduanMasyarakat.delete.loading}
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus pengaduan masyarakat ini?"
+ />
+
+ );
+}
+
+export default DetailPengaduanMasyarakat;
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
new file mode 100644
index 00000000..11140fff
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
@@ -0,0 +1,148 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
+
+import { useShallowEffect } from '@mantine/hooks';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
+
+function PengaduanMasyarakat() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListPengaduanMasyarakat({ search }: { search: string }) {
+ const listState = useProxy(layananonlineDesa.pengaduanMasyarakat);
+ const router = useRouter();
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = listState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Pengaduan Masyarakat
+
+
+
+
+
+ Nama
+ Email
+ Nomor Telepon
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+ {item.name}
+
+
+ {item.email}
+
+
+ {item.nomorTelepon}
+
+
+
+ }
+ onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
+ >
+ Detail
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data pengaduan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PengaduanMasyarakat;
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
new file mode 100644
index 00000000..5e2a29ff
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
@@ -0,0 +1,175 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
+import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
+
+interface FormProgramKreatif {
+ name: string;
+ deskripsi: string;
+ slug: string;
+ icon: string;
+}
+
+function EditProgramKreatifDesa() {
+ const stateProgramKreatif = useProxy(programKreatifState);
+ const params = useParams();
+ const router = useRouter();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ slug: '',
+ icon: '',
+ });
+
+ // Load data hanya sekali berdasarkan params.id
+ useEffect(() => {
+ const loadProgramKreatif = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateProgramKreatif.update.load(id);
+ if (data) {
+ stateProgramKreatif.update.id = id;
+ setFormData({
+ name: data.name || '',
+ slug: data.slug || '',
+ deskripsi: data.deskripsi || '',
+ icon: data.icon || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading program kreatif:', error);
+ toast.error('Gagal memuat data program kreatif');
+ }
+ };
+
+ loadProgramKreatif();
+ }, [params?.id]);
+
+ const handleChange =
+ (field: keyof FormProgramKreatif) =>
+ (value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ stateProgramKreatif.update.form = {
+ name: formData.name.trim(),
+ deskripsi: formData.deskripsi.trim(),
+ slug: formData.slug.trim(),
+ icon: formData.icon.trim(),
+ };
+ await stateProgramKreatif.update.submit();
+ router.push('/admin/inovasi/program-kreatif-desa');
+ } catch (error) {
+ console.error('Error updating program kreatif:', error);
+ toast.error('Gagal menyimpan program kreatif');
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Program Kreatif Desa
+
+
+
+
+
+ handleChange('name')(e.target.value)}
+ required
+ />
+
+ handleChange('slug')(e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi
+
+
+
+
+
+
+ Ikon Program Kreatif Desa
+
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditProgramKreatifDesa;
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx
new file mode 100644
index 00000000..e402bf15
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx
@@ -0,0 +1,149 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { IconKey, IconMapper } from '../../../_com/iconMap';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import programKreatifState from '../../../_state/inovasi/program-kreatif';
+
+function DetailProgramKreatifDesa() {
+ const [modalHapus, setModalHapus] = useState(false)
+ const stateProgramKreatif = useProxy(programKreatifState)
+ const router = useRouter()
+ const params = useParams()
+ const [selectedId, setSelectedId] = useState(null)
+
+ useShallowEffect(() => {
+ stateProgramKreatif.findUnique.load(params?.id as string)
+ }, [params?.id])
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateProgramKreatif.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/inovasi/program-kreatif-desa")
+ }
+ }
+
+ if (!stateProgramKreatif.findUnique.data) {
+ return (
+
+
+
+ )
+ }
+
+ const data = stateProgramKreatif.findUnique.data
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Program Kreatif Desa
+
+
+
+
+
+ Nama Program Kreatif Desa
+ {data?.name || '-'}
+
+
+
+ Ikon Program Kreatif Desa
+ {data?.icon ? (
+
+ ) : (
+ Tidak ada ikon
+ )}
+
+
+
+ Deskripsi Singkat
+ {data?.slug || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+
+ {
+ if (data) {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }
+ }}
+ disabled={stateProgramKreatif.delete.loading || !data}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ {
+ if (data) {
+ router.push(`/admin/inovasi/program-kreatif-desa/${data.id}/edit`);
+ }
+ }}
+ disabled={!data}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus program kreatif desa ini?"
+ />
+
+ );
+}
+
+export default DetailProgramKreatifDesa;
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
new file mode 100644
index 00000000..b5969bf7
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
@@ -0,0 +1,122 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import programKreatifState from '../../../_state/inovasi/program-kreatif';
+import SelectIconProgram from '../../../_com/selectIcon';
+
+function CreateProgramKreatifDesa() {
+ const stateCreate = useProxy(programKreatifState);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateCreate.create.form = {
+ name: "",
+ slug: "",
+ deskripsi: "",
+ icon: "",
+ };
+ };
+
+ const handleSubmit = async () => {
+ await stateCreate.create.create();
+ resetForm();
+ router.push("/admin/inovasi/program-kreatif-desa");
+ };
+
+ return (
+
+ {/* Tombol kembali */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Program Kreatif Desa
+
+
+
+ {/* Card Form */}
+
+
+ Nama Program Kreatif Desa}
+ placeholder="Masukkan nama program kreatif desa"
+ defaultValue={stateCreate.create.form.name || ""}
+ onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)}
+ required
+ />
+
+
+
+ Ikon Program Kreatif Desa
+
+ (stateCreate.create.form.icon = value)}
+ />
+
+
+ Deskripsi Singkat Program Kreatif Desa}
+ placeholder="Masukkan deskripsi singkat program kreatif desa"
+ defaultValue={stateCreate.create.form.slug || ""}
+ onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)}
+ required
+ />
+
+
+
+ Deskripsi Program Kreatif Desa
+
+
+ (stateCreate.create.form.deskripsi = htmlContent)
+ }
+ />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateProgramKreatifDesa;
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx
new file mode 100644
index 00000000..5ead6694
--- /dev/null
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx
@@ -0,0 +1,256 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import {
+ IconCash,
+ IconChartLine,
+ IconChristmasTreeFilled,
+ IconClipboard,
+ IconDeviceImac,
+ IconDroplet,
+ IconHome,
+ IconHomeEco,
+ IconHospital,
+ IconLeaf,
+ IconPlus,
+ IconRecycle,
+ IconScale,
+ IconSchool,
+ IconSearch,
+ IconShieldFilled,
+ IconShoppingCart,
+ IconTent,
+ IconTrash,
+ IconTree,
+ IconTrendingUp,
+ IconTrophy,
+ IconTruck,
+} from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../_com/header';
+import programKreatifState from '../../_state/inovasi/program-kreatif';
+
+function ProgramKreatifDesa() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListProgramKreatifDesa({ search }: { search: string }) {
+ const listState = useProxy(programKreatifState);
+ const { data, loading, page, totalPages, load } = listState.findMany;
+ const router = useRouter();
+
+ useEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ const iconMap: Record> = {
+ ekowisata: IconLeaf,
+ kompetisi: IconTrophy,
+ wisata: IconTent,
+ ekonomi: IconChartLine,
+ sampah: IconRecycle,
+ truck: IconTruck,
+ scale: IconScale,
+ clipboard: IconClipboard,
+ trash: IconTrash,
+ lingkunganSehat: IconHomeEco,
+ sumberOksigen: IconChristmasTreeFilled,
+ ekonomiBerkelanjutan: IconTrendingUp,
+ mencegahBencana: IconShieldFilled,
+ rumah: IconHome,
+ pohon: IconTree,
+ air: IconDroplet,
+ bantuan: IconCash,
+ pelatihan: IconSchool,
+ subsidi: IconShoppingCart,
+ layananKesehatan: IconHospital,
+ };
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+
+ Daftar Program Kreatif Desa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/program-kreatif-desa/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+ No
+ Nama Program Kreatif Desa
+ Deskripsi Singkat
+ Ikon
+ Detail
+
+
+
+
+ Tidak ada data program kreatif desa yang tersedia
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Program Kreatif Desa
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/program-kreatif-desa/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ No
+
+ Nama Program Kreatif Desa
+
+ Deskripsi Singkat
+ Ikon
+ Detail
+
+
+
+ {filteredData.map((item, index) => (
+
+ {index + 1}
+
+
+ {item.name}
+
+
+
+
+
+ {item.slug}
+
+
+
+
+ {iconMap[item.icon] && (
+
+ {React.createElement(iconMap[item.icon], { size: 24 })}
+
+ )}
+
+
+
+ router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)
+ }
+ >
+
+ Detail
+
+
+
+ ))}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default ProgramKreatifDesa;
diff --git a/src/app/admin/(dashboard)/keamanan/_com/keamananEditor.tsx b/src/app/admin/(dashboard)/keamanan/_com/keamananEditor.tsx
new file mode 100644
index 00000000..437e3242
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/_com/keamananEditor.tsx
@@ -0,0 +1,93 @@
+'use client'
+import colors from '@/con/colors';
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+const content =
+ 'Welcome to Mantine rich text editor RichTextEditor component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. RichTextEditor is based on Tiptap.dev and supports all of its features:
General text formatting: bold , italic , underline , strike-through Headings (h1-h6) Sub and super scripts (<sup /> and <sub /> tags) Ordered and bullet lists Text align And all other extensions ';
+
+export function KeamananEditor({showSubmit = true} : {
+ showSubmit: boolean
+}) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+
+ Submit
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/keamanan/_com/keamananEdtorText.tsx b/src/app/admin/(dashboard)/keamanan/_com/keamananEdtorText.tsx
new file mode 100644
index 00000000..77864dc7
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/_com/keamananEdtorText.tsx
@@ -0,0 +1,95 @@
+'use client'
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+
+function KeamananEditorText({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
+ onSubmit?: (val: string) => void,
+ onChange: (val: string) => void,
+ showSubmit?: boolean,
+ initialContent?: string }) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content: initialContent,
+ onUpdate : ({editor}) => {
+ onChange(editor.getHTML())
+ }
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+ {
+ if (!editor) return
+ onSubmit?.(editor?.getHTML())
+ }}>Submit
+ )}
+
+ );
+}
+
+export default KeamananEditorText;
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
new file mode 100644
index 00000000..c1a7d88e
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
@@ -0,0 +1,241 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import {
+ IconArrowBack,
+ IconImageInPicture,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
+import colors from "@/con/colors";
+import ApiFetch from "@/lib/api-fetch";
+import { Dropzone } from "@mantine/dropzone";
+import keamananLingkunganState from "../../../../_state/keamanan/keamanan-lingkungan";
+
+function EditKeamananLingkungan() {
+ const keamananState = useProxy(keamananLingkunganState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: "",
+ deskripsi: "",
+ imageId: "",
+ });
+
+ // Load data sekali pas mount
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await keamananState.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ imageId: data.imageId || "",
+ });
+
+ if (data?.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (error) {
+ console.error("Error loading keamananLingkungan:", error);
+ toast.error("Gagal memuat data keamananLingkungan");
+ }
+ };
+
+ loadData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [params?.id]);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ let imageId = formData.imageId;
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+
+ imageId = uploaded.id;
+ }
+
+ // update global state hanya sekali pas submit
+ keamananState.edit.form = {
+ ...keamananState.edit.form,
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ imageId,
+ };
+
+ await keamananState.edit.update();
+ toast.success("Keamanan Lingkungan berhasil diperbarui!");
+ router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal");
+ } catch (error) {
+ console.error("Error updating keamananLingkungan:", error);
+ toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Keamanan Lingkungan
+
+
+
+ {/* Form */}
+
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error("File tidak valid.")}
+ maxSize={5 * 1024 ** 2}
+ accept={{ "image/*": [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage ? (
+
+ ) : (
+
+
+
+ )}
+
+ handleChange("name", e.target.value)}
+ label="Judul Keamanan Lingkungan"
+ placeholder="Masukkan judul"
+ required
+ />
+
+
+
+ Deskripsi
+
+ handleChange("deskripsi", htmlContent)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKeamananLingkungan;
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx
new file mode 100644
index 00000000..1ce73db9
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx
@@ -0,0 +1,143 @@
+'use client'
+import { useProxy } from 'valtio/utils';
+
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+import colors from '@/con/colors';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan';
+
+
+function DetailKeamananLingkungan() {
+ const keamananState = useProxy(keamananLingkunganState)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const params = useParams()
+ const router = useRouter()
+
+ useShallowEffect(() => {
+ keamananState.findUnique.load(params?.id as string)
+ }, [])
+
+
+ const handleHapus = () => {
+ if (selectedId) {
+ keamananState.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal")
+ }
+ }
+
+ if (!keamananState.findUnique.data) {
+ return (
+
+
+
+ )
+ }
+
+ const data = keamananState.findUnique.data
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Keamanan Lingkungan
+
+
+
+
+
+ Gambar
+
+
+
+
+ Judul Keamanan Lingkungan
+ {data?.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus keamanan lingkungan ini?"
+ />
+
+ );
+}
+
+export default DetailKeamananLingkungan;
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
new file mode 100644
index 00000000..4ff05394
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
@@ -0,0 +1,218 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import {
+ IconArrowBack,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan';
+
+function CreateKeamananLingkungan() {
+ const keamananState = useProxy(keamananLingkunganState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ keamananState.create.form = {
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengupload file');
+ }
+
+ keamananState.create.form.imageId = uploaded.id;
+
+ await keamananState.create.create();
+
+ resetForm();
+ router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Data Keamanan Lingkungan
+
+
+
+ {/* Form */}
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Input Nama */}
+ {
+ keamananState.create.form.name = val.target.value;
+ }}
+ label={Nama Keamanan Lingkungan }
+ placeholder="Masukkan nama Keamanan Lingkungan"
+ required
+ />
+
+ {/* Input Deskripsi */}
+
+
+ Deskripsi Keamanan Lingkungan
+
+ {
+ keamananState.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKeamananLingkungan;
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx
new file mode 100644
index 00000000..8fde1a33
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx
@@ -0,0 +1,168 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 keamananLingkunganState from '../../_state/keamanan/keamanan-lingkungan';
+
+function KeamananLingkungan() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListKeamananLingkungan({ search }: { search: string }) {
+ const keamananState = useProxy(keamananLingkunganState)
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = keamananState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Keamanan Lingkungan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Nama
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data keamanan lingkungan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default KeamananLingkungan;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..17c298f4
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx
@@ -0,0 +1,112 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { IconPhone, IconTag } from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "Kontak Darurat Keamanan",
+ value: "kontak-darurat-keamanan",
+ href: "/admin/keamanan/kontak-darurat/kontak-darurat-keamanan",
+ icon: ,
+ tooltip: "Lihat dan kelola kontak darurat keamanan",
+ },
+ {
+ label: "Kontak Darurat Item",
+ value: "kontak-darurat-item",
+ href: "/admin/keamanan/kontak-darurat/kontak-darurat-item",
+ icon: ,
+ tooltip: "Kelola data kontak darurat item",
+ }
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+
+ Kontak Darurat
+
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
new file mode 100644
index 00000000..270ff1b4
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
+import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
+import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKontakItem() {
+ const router = useRouter();
+ const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ nomorTelepon: '',
+ icon: '',
+ });
+
+ // Load data sekali dari global state
+ useEffect(() => {
+ const loadKontakDarurat = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await kontakState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.nama || '',
+ nomorTelepon: data.nomorTelepon || '',
+ icon: data.icon || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading kontak darurat:', error);
+ toast.error('Gagal memuat data kontak darurat');
+ }
+ };
+
+ loadKontakDarurat();
+ }, [params?.id]);
+
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state sekali pas submit
+ kontakState.update.form = {
+ ...kontakState.update.form,
+ nama: formData.name,
+ nomorTelepon: formData.nomorTelepon,
+ icon: formData.icon,
+ };
+
+ await kontakState.update.update();
+ toast.success('Kontak Darurat berhasil diperbarui!');
+ router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item');
+ } catch (error) {
+ console.error('Error updating kontak darurat:', error);
+ toast.error('Terjadi kesalahan saat memperbarui kontak darurat');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Kontak Darurat Item
+
+
+
+ {/* Form */}
+
+
+ handleChange('name', e.target.value)}
+ required
+ />
+
+ handleChange('nomorTelepon', e.target.value)}
+ required
+ />
+
+
+
+ Ikon Program Kreatif Desa
+
+ handleChange('icon', value)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKontakItem;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx
new file mode 100644
index 00000000..93c858cd
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx
@@ -0,0 +1,138 @@
+'use client'
+import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailKontakDarurat() {
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+ const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
+
+ useShallowEffect(() => {
+ kontakState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ kontakState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/keamanan/kontak-darurat/kontak-darurat-item");
+ }
+ };
+
+ if (!kontakState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = kontakState.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Kontak Darurat Item
+
+
+
+
+ {/* Judul */}
+
+ Judul Kontak Darurat Item
+ {data?.nama || '-'}
+
+
+
+ Nomor Telepon Kontak Darurat Item
+ {data?.nomorTelepon || '-'}
+
+
+
+ Ikon Kontak Darurat
+ {data?.icon && (
+
+ )}
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-item/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus kontak darurat ini?"
+ />
+
+ );
+}
+
+export default DetailKontakDarurat;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx
new file mode 100644
index 00000000..3b962470
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx
@@ -0,0 +1,113 @@
+'use client'
+import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
+import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreateKontakItem() {
+ const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
+ const router = useRouter();
+ const resetForm = () => {
+ kontakState.create.form = {
+ nama: '',
+ icon: '',
+ nomorTelepon: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await kontakState.create.create();
+ resetForm();
+ router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kontak Darurat Item
+
+
+
+ {/* Form */}
+
+
+ {/* Input Nama Kategori */}
+ {
+ kontakState.create.form.nama = val.target.value;
+ }}
+ label={Nama Kontak Darurat }
+ placeholder="Masukkan nama kontak darurat"
+ required
+ />
+
+ Nomor Telepon Kontak}
+ placeholder="Masukkan nomor telepon"
+ defaultValue={kontakState.create.form.nomorTelepon}
+ onChange={(val) => {
+ kontakState.create.form.nomorTelepon = val.target.value;
+ }}
+ required
+ />
+
+
+ Ikon Kontak Darurat Item
+ kontakState.create.form.icon = value} />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKontakItem;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx
new file mode 100644
index 00000000..dab73629
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx
@@ -0,0 +1,165 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 kontakDarurat from '../../../_state/keamanan/kontak-darurat-keamanan';
+
+
+function KontakItem() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListKontakItem({ search }: { search: string }) {
+ const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = kontakState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Kontak Darurat Item
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Nama Kontak
+ Nomor Telepon
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+
+ {item.nomorTelepon || "-"}
+
+
+
+ router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-item/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data kontak darurat item yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default KontakItem;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx
new file mode 100644
index 00000000..7492194c
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx
@@ -0,0 +1,188 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import { IconKey } from "@/app/admin/(dashboard)/_com/iconMap";
+import SelectIconProgramEdit from "@/app/admin/(dashboard)/_com/selectIconEdit";
+import kontakDarurat from "@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan";
+import colors from "@/con/colors";
+import {
+ Box,
+ Button,
+ Group,
+ MultiSelect,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { IconArrowBack } from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+function EditKontakDaruratKeamanan() {
+ const router = useRouter();
+ const params = useParams();
+ const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
+
+ const [isLoading, setIsLoading] = useState(true);
+ // Remove the dependency on data in the initial state
+ const [formData, setFormData] = useState({
+ name: "",
+ icon: "" as IconKey | "",
+ kategoriId: [] as string[], // Initialize as empty array
+ });
+
+ // Load data dari backend
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ setIsLoading(true);
+ await kontakDarurat.kontakDaruratItem.findMany.load();
+
+ const id = params?.id as string;
+ if (id) {
+ const data = await kontakState.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.nama || "",
+ icon: (data.icon as IconKey) || "",
+ kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [],
+ });
+ }
+ }
+ } catch (error) {
+ console.error("Error loading data:", error);
+ toast.error("Gagal memuat data");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ // Handle submit
+ const handleSubmit = async () => {
+ try {
+ kontakState.update.form = {
+ ...kontakState.update.form,
+ nama: formData.name,
+ icon: formData.icon,
+ kategoriId: formData.kategoriId,
+ };
+
+ await kontakState.update.update();
+ toast.success("Kontak Darurat berhasil diperbarui!");
+ router.push("/admin/keamanan/kontak-darurat");
+ } catch (error) {
+ console.error("Error updating kontak darurat:", error);
+ toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Kontak Darurat Keamanan
+
+
+
+ {/* Form */}
+
+
+ {/* Nama Kontak */}
+
+ setFormData((prev) => ({ ...prev, name: e.target.value }))
+ }
+ label="Nama Kontak Darurat"
+ placeholder="Masukkan nama kontak darurat"
+ required
+ />
+
+ {/* MultiSelect */}
+
+ setFormData((prev) => ({ ...prev, kategoriId: val }))
+ }
+ label={Kontak Item }
+ placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"}
+ data={
+ Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data)
+ ? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({
+ value: v.id,
+ label: v.nama,
+ }))
+ : []
+ }
+ clearable
+ searchable
+ required
+ error={
+ !formData.kategoriId.length
+ ? "Pilih minimal satu kategori"
+ : undefined
+ }
+ disabled={isLoading}
+ />
+
+ {/* Icon Select */}
+
+
+ Ikon Program Kreatif Desa
+
+
+ setFormData((prev) => ({ ...prev, icon: value }))
+ }
+ />
+
+
+ {/* Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKontakDaruratKeamanan;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx
new file mode 100644
index 00000000..c25192d5
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx
@@ -0,0 +1,154 @@
+'use client'
+import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap';
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailKontakDaruratKeamanan() {
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+ const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
+
+ useShallowEffect(() => {
+ kontakDarurat.kontakDaruratItem.findMany.load();
+ kontakState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ kontakState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/keamanan/kontak-darurat/kontak-darurat-keamanan");
+ }
+ };
+
+ if (!kontakState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = kontakState.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Kontak Darurat Keamanan
+
+
+
+
+ {/* Judul */}
+
+ Judul Kontak Darurat
+ {data?.nama || '-'}
+
+
+
+ Ikon Program Kreatif Desa
+ {data?.icon && (
+
+ )}
+
+
+ {/* Kontak Items */}
+
+ Kontak
+
+ {data?.kontakItems.map((item, index) => (
+
+ {item.kontakItem.nama}
+ {item.kontakItem.nomorTelepon}
+ {item.kontakItem.icon && (
+
+ )}
+
+ ))}
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus kontak darurat ini?"
+ />
+
+ );
+}
+
+export default DetailKontakDaruratKeamanan;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx
new file mode 100644
index 00000000..afdbd448
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx
@@ -0,0 +1,125 @@
+'use client'
+import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
+import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ MultiSelect,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+function CreateKontakDaruratKeamanan() {
+ const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ kontakDarurat.kontakDaruratItem.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ kontakState.create.form = {
+ nama: '',
+ icon: '',
+ kategoriId: []
+ };
+ };
+
+ const handleSubmit = async () => {
+ await kontakState.create.create();
+ resetForm();
+ router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kontak Darurat Keamanan
+
+
+
+ {/* Form */}
+
+
+ {/* Input Nama Kategori */}
+ {
+ kontakState.create.form.nama = val.target.value;
+ }}
+ label={Nama Kategori Darurat }
+ placeholder="Masukkan nama kategori darurat"
+ required
+ />
+
+
+ Ikon Kontak Darurat
+ kontakState.create.form.icon = value} />
+
+
+ {
+ kontakState.create.form.kategoriId = val;
+ }}
+ label={Kontak Item }
+ placeholder='Pilih kontak item'
+ data={
+ kontakDarurat.kontakDaruratItem.findMany.data?.map((v) => ({
+ value: v.id,
+ label: v.nama
+ })) || []
+ }
+ />
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKontakDaruratKeamanan;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx
new file mode 100644
index 00000000..a5b6239e
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx
@@ -0,0 +1,171 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 kontakDarurat from '../../../_state/keamanan/kontak-darurat-keamanan';
+
+function KontakDaruratKeamanan() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListKontakDaruratKeamanan({ search }: { search: string }) {
+ const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = kontakState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ kontakDarurat.kontakDaruratItem.findMany.load();
+ }, [page, search]);
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Kontak Darurat Keamanan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Kontak Darurat
+ Nama Kontak
+ Nomor Telepon
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+
+ {item.kategori?.nama}
+
+
+
+
+ {item.kategori?.nomorTelepon || "-"}
+
+
+
+ router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data kontak darurat keamanan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default KontakDaruratKeamanan;
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx
new file mode 100644
index 00000000..4f25d331
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx
@@ -0,0 +1,13 @@
+'use client'
+import React from 'react';
+import LayoutTabs from './_lib/layoutTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx
new file mode 100644
index 00000000..f52bd6c1
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx
@@ -0,0 +1,198 @@
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { DateTimePicker } from '@mantine/dates';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+export type Status = "Selesai" | "Proses" | "Gagal";
+
+function EditLaporanPublik() {
+ const stateLaporan = useProxy(laporanPublikState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState<{
+ judul: string;
+ lokasi: string;
+ tanggalWaktu: string;
+ status: Status;
+ penanganan: string;
+ kronologi: string;
+ }>({
+ judul: '',
+ lokasi: '',
+ tanggalWaktu: '',
+ status: 'Proses', // Default status
+ penanganan: '',
+ kronologi: '',
+ });
+
+ useEffect(() => {
+ const loadLaporanPublik = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateLaporan.edit.load(id);
+ if (data) {
+ setFormData((prev) => ({
+ ...prev,
+ judul: data.judul ?? prev.judul,
+ lokasi: data.lokasi ?? prev.lokasi,
+ tanggalWaktu: data.tanggalWaktu ?? prev.tanggalWaktu,
+ status: (data.status as Status) ?? prev.status,
+ penanganan: data.penanganan?.[0]?.deskripsi ?? prev.penanganan,
+ kronologi: data.kronologi ?? prev.kronologi,
+ }));
+ }
+ } catch (error) {
+ console.error("Error loading laporan publik:", error);
+ toast.error("Gagal mengambil data laporan publik");
+ }
+ };
+
+ loadLaporanPublik();
+ }, [params?.id, stateLaporan.edit]);
+
+
+ const handleChange = (field: string, value: string | Status) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ stateLaporan.edit.form = {
+ ...stateLaporan.edit.form,
+ ...formData,
+ };
+
+ await stateLaporan.edit.update();
+ toast.success("Laporan Publik berhasil diperbarui!");
+ router.push("/admin/keamanan/laporan-publik");
+ } catch (error) {
+ console.error("Error updating laporan publik:", error);
+ toast.error("Terjadi kesalahan saat memperbarui laporan publik");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Laporan Publik
+
+
+
+ {/* Card */}
+
+
+ handleChange('judul', e.target.value)}
+ label={Judul Laporan Publik }
+ placeholder="Masukkan judul laporan publik"
+ required
+ />
+
+ handleChange('lokasi', e.target.value)}
+ label={Lokasi Laporan Publik }
+ placeholder="Masukkan lokasi laporan publik"
+ required
+ />
+
+ {
+ if (value) {
+ const date = new Date(value);
+ handleChange('tanggalWaktu', date.toISOString());
+ } else {
+ handleChange('tanggalWaktu', '');
+ }
+ }}
+ required
+ />
+
+ handleChange('status', val as Status)}
+ label={Status Laporan Publik }
+ placeholder="Pilih status laporan publik"
+ data={[
+ { value: "Selesai", label: "Selesai" },
+ { value: "Proses", label: "Proses" },
+ { value: "Gagal", label: "Gagal" },
+ ]}
+ required
+ />
+
+
+ Kronologi Laporan Publik
+ handleChange('kronologi', htmlContent)}
+ />
+
+
+
+ Penanganan Laporan Publik
+ handleChange('penanganan', htmlContent)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditLaporanPublik;
diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx
new file mode 100644
index 00000000..b5f83ee9
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx
@@ -0,0 +1,195 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import laporanPublikState from '../../../_state/keamanan/laporan-publik';
+import { useState } from 'react';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function DetailLaporanPublik() {
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const stateLaporan = useProxy(laporanPublikState);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ stateLaporan.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ stateLaporan.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/keamanan/laporan-publik');
+ }
+ };
+
+ if (!stateLaporan.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = stateLaporan.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Laporan Publik
+
+
+
+
+
+ Judul Laporan Publik
+ {data.judul || '-'}
+
+
+
+ Tanggal Laporan Publik
+
+ {data.tanggalWaktu
+ ? new Date(data.tanggalWaktu).toLocaleString('id-ID')
+ : '-'}
+
+
+
+
+ Lokasi
+ {data.lokasi || '-'}
+
+
+
+ Status
+
+ {data.status || '-'}
+
+
+
+
+ Kronologi
+
+
+
+
+ Penanganan
+ {data.penanganan?.length ? (
+ data.penanganan.map((item, index) => (
+
+
+
+ ))
+ ) : (
+
+ Belum ada penanganan
+
+ )}
+
+
+ {/* Tombol Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={stateLaporan.delete.loading}
+ >
+
+
+
+
+
+
+ router.push(`/admin/keamanan/laporan-publik/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus laporan publik ini?"
+ />
+
+ );
+}
+
+export default DetailLaporanPublik;
diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx
new file mode 100644
index 00000000..994af691
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx
@@ -0,0 +1,121 @@
+'use client';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { DateTimePicker } from '@mantine/dates';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import laporanPublikState from '../../../_state/keamanan/laporan-publik';
+
+export type Status = 'Selesai' | 'Proses' | 'Gagal';
+
+function CreateLaporanPublik() {
+ const stateLaporan = useProxy(laporanPublikState);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateLaporan.create.form = {
+ judul: '',
+ lokasi: '',
+ tanggalWaktu: '',
+ kronologi: '',
+ };
+ };
+
+ const handleSubmit = async () => {
+ await stateLaporan.create.create();
+ resetForm();
+ router.push('/admin/keamanan/laporan-publik');
+ };
+
+ return (
+
+ {/* Header with Back Button */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Laporan Publik
+
+
+
+ {/* Form Card */}
+
+
+ (stateLaporan.create.form.judul = e.target.value)}
+ label={Judul Laporan Publik }
+ placeholder="Masukkan judul laporan publik"
+ required
+ />
+
+ (stateLaporan.create.form.lokasi = e.target.value)}
+ label={Lokasi Laporan Publik }
+ placeholder="Masukkan lokasi laporan publik"
+ required
+ />
+
+ Tanggal Laporan Publik}
+ value={
+ stateLaporan.create.form.tanggalWaktu
+ ? new Date(stateLaporan.create.form.tanggalWaktu)
+ : null
+ }
+ onChange={(val) => {
+ stateLaporan.create.form.tanggalWaktu = val ? val.toString() : '';
+ }}
+ />
+
+ (stateLaporan.create.form.kronologi = e.target.value)}
+ label={Kronologi Laporan Publik }
+ placeholder="Masukkan kronologi laporan publik"
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateLaporanPublik;
diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx
new file mode 100644
index 00000000..68b5d055
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx
@@ -0,0 +1,188 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Pagination,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
+import HeaderSearch from '../../_com/header';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import laporanPublikState from '../../_state/keamanan/laporan-publik';
+import { useShallowEffect } from '@mantine/hooks';
+import { useState } from 'react';
+
+function LaporanPublik() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListLaporanPublik({ search }: { search: string }) {
+ const stateLaporan = useProxy(laporanPublikState);
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateLaporan.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Laporan Publik
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Judul Laporan Publik
+ Tanggal
+ Status
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.judul}
+
+
+
+
+ {new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
+
+
+
+
+ {item.status}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/keamanan/laporan-publik/${item.id}`)
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data laporan publik yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default LaporanPublik;
diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx
new file mode 100644
index 00000000..de2e39aa
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx
@@ -0,0 +1,202 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
+import { convertYoutubeUrlToEmbed } from '@/app/admin/(dashboard)/desa/gallery/lib/youtube-utils';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPencegahanKriminalitas() {
+ const router = useRouter();
+ const params = useParams();
+ const kriminalitasState = useProxy(pencegahanKriminalitasState);
+
+ const [formData, setFormData] = useState({
+ judul: '',
+ deskripsi: '',
+ deskripsiSingkat: '',
+ linkVideo: '',
+ });
+
+ // load data hanya sekali pas id berubah
+ useEffect(() => {
+ const loadKriminalitas = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await kriminalitasState.update.load(id);
+ if (data) {
+ setFormData({
+ judul: data.judul ?? '',
+ deskripsi: data.deskripsi ?? '',
+ deskripsiSingkat: data.deskripsiSingkat ?? '',
+ linkVideo: data.linkVideo ?? '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading pencegahan kriminalitas:', error);
+ toast.error('Gagal memuat data pencegahan kriminalitas');
+ }
+ };
+
+ loadKriminalitas();
+ }, [params?.id]);
+
+ const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
+
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (e: React.ChangeEvent) => {
+ setFormData((prev) => ({ ...prev, [field]: e.target.value }));
+ };
+
+ const handleSubmit = async () => {
+ const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
+ if (!converted) {
+ toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
+ return;
+ }
+
+ try {
+ // update global state saat submit
+ kriminalitasState.update.form = {
+ judul: formData.judul,
+ deskripsi: formData.deskripsi,
+ deskripsiSingkat: formData.deskripsiSingkat,
+ linkVideo: formData.linkVideo,
+ };
+ kriminalitasState.update.id = params?.id as string;
+
+ await kriminalitasState.update.update();
+ toast.success('Pencegahan Kriminalitas berhasil diperbarui!');
+ router.push('/admin/keamanan/pencegahan-kriminalitas');
+ } catch (error) {
+ console.error('Error updating pencegahan kriminalitas:', error);
+ toast.error('Terjadi kesalahan saat memperbarui data');
+ }
+ };
+
+ return (
+
+ {/* Back button + Title */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Pencegahan Kriminalitas
+
+
+
+ {/* Form container */}
+
+
+
+
+
+
+ Deskripsi
+
+
+ setFormData((prev) => ({ ...prev, deskripsiSingkat: val }))
+ }
+ />
+
+
+
+
+ Deskripsi Lengkap
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: val }))
+ }
+ />
+
+
+
+
+ {embedLink && (
+
+
+
+ )}
+
+
+ {/* Action button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPencegahanKriminalitas;
diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx
new file mode 100644
index 00000000..46ffedf0
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx
@@ -0,0 +1,166 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useRouter, useParams } from 'next/navigation';
+import { useState } from 'react';
+import { useShallowEffect } from '@mantine/hooks';
+import { useProxy } from 'valtio/utils';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas';
+
+function DetailPencegahanKriminalitas() {
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+ const kriminalitasState = useProxy(pencegahanKriminalitasState);
+
+ useShallowEffect(() => {
+ kriminalitasState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleDelete = () => {
+ if (selectedId) {
+ kriminalitasState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/keamanan/pencegahan-kriminalitas");
+ }
+ };
+
+ if (!kriminalitasState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = kriminalitasState.findUnique.data;
+
+ return (
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Detail */}
+
+
+
+ Detail Pencegahan Kriminalitas
+
+
+
+
+
+ Judul
+ {data?.judul || '-'}
+
+
+
+ Deskripsi Singkat
+ {data?.deskripsiSingkat ? (
+
+ ) : (
+ Tidak ada deskripsi singkat
+ )}
+
+
+
+ Deskripsi
+ {data?.deskripsi ? (
+
+ ) : (
+ Tidak ada deskripsi
+ )}
+
+
+
+ Video
+ {data?.linkVideo ? (
+
+ ) : (
+ Tidak ada video
+ )}
+
+
+ {/* Tombol Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/keamanan/pencegahan-kriminalitas/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleDelete}
+ text="Apakah anda yakin ingin menghapus pencegahan kriminalitas ini?"
+ />
+
+ );
+
+ function convertToEmbedUrl(youtubeUrl: string): string {
+ try {
+ const url = new URL(youtubeUrl);
+ const videoId = url.searchParams.get("v");
+ if (!videoId) return youtubeUrl;
+ return `https://www.youtube.com/embed/${videoId}`;
+ } catch (err) {
+ console.error("Error converting YouTube URL to embed:", err);
+ return youtubeUrl;
+ }
+ }
+}
+
+export default DetailPencegahanKriminalitas;
diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx
new file mode 100644
index 00000000..89b189fc
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx
@@ -0,0 +1,165 @@
+'use client'
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas';
+import { useState } from 'react';
+import { convertYoutubeUrlToEmbed } from '../../../desa/gallery/lib/youtube-utils';
+import { toast } from 'react-toastify';
+
+function CreatePencegahanKriminalitas() {
+ const router = useRouter();
+ const kriminalitasState = useProxy(pencegahanKriminalitasState);
+ const [link, setLink] = useState('');
+ const embedLink = convertYoutubeUrlToEmbed(link);
+
+ const resetForm = () => {
+ kriminalitasState.create.form = {
+ judul: "",
+ deskripsi: "",
+ deskripsiSingkat: "",
+ linkVideo: "",
+ };
+ setLink('');
+ };
+
+ const handleSubmit = async () => {
+ if (!embedLink) {
+ toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
+ return;
+ }
+
+ kriminalitasState.create.form.linkVideo = embedLink;
+ await kriminalitasState.create.create();
+ resetForm();
+ router.push('/admin/keamanan/pencegahan-kriminalitas');
+ };
+
+ return (
+
+ {/* Header Back Button + Title */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Pencegahan Kriminalitas
+
+
+
+ {/* Card Form */}
+
+
+ {/* Judul */}
+ {
+ kriminalitasState.create.form.judul = e.currentTarget.value;
+ }}
+ required
+ />
+
+ {/* Deskripsi Singkat */}
+
+
+ Deskripsi Singkat
+
+ {
+ kriminalitasState.create.form.deskripsiSingkat = val;
+ }}
+ />
+
+
+ {/* Deskripsi Panjang */}
+
+
+ Deskripsi
+
+ {
+ kriminalitasState.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Link YouTube */}
+ setLink(e.currentTarget.value)}
+ required
+ />
+
+ {/* Preview Video */}
+ {embedLink && (
+
+
+
+ )}
+
+ {/* Button Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePencegahanKriminalitas;
diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx
new file mode 100644
index 00000000..be9eefc9
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx
@@ -0,0 +1,185 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import HeaderSearch from '../../_com/header';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import pencegahanKriminalitasState from '../../_state/keamanan/pencegahan-kriminalitas';
+import { useShallowEffect } from '@mantine/hooks';
+import { useState } from 'react';
+
+function PencegahanKriminalitas() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListPencegahanKriminalitas({ search }: { search: string }) {
+ const kriminalitasState = useProxy(pencegahanKriminalitasState)
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = kriminalitasState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Pencegahan Kriminalitas
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/keamanan/pencegahan-kriminalitas/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Nama Pencegahan
+ Deskripsi
+ Deskripsi Singkat
+ Aksi
+
+
+
+ {data.length > 0 ? (
+ data.map((item) => (
+
+
+
+
+ {item.judul}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/keamanan/pencegahan-kriminalitas/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data pencegahan kriminalitas yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PencegahanKriminalitas;
diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx
new file mode 100644
index 00000000..bf826e3d
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx
@@ -0,0 +1,401 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import colors from "@/con/colors";
+import {
+ Box,
+ Button,
+ Card,
+ Group,
+ Modal,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import { IconArrowBack } from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useProxy } from "valtio/utils";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import polsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat";
+
+function EditPolsekTerdekat() {
+ const polsekState = useProxy(polsekTerdekat);
+ const params = useParams();
+ const router = useRouter();
+
+ const [layananOptions, setLayananOptions] = useState<
+ { value: string; label: string }[]
+ >([]);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [modalUpdateOpen, setModalUpdateOpen] = useState(false);
+ const [namaLayananBaru, setNamaLayananBaru] = useState("");
+ const [selectedLayananId, setSelectedLayananId] = useState(
+ null
+ );
+ const [namaLayananUpdate, setNamaLayananUpdate] = useState("");
+ const [formData, setFormData] = useState({
+ nama: "",
+ jarakKeDesa: "",
+ alamat: "",
+ nomorTelepon: "",
+ jamOperasional: "",
+ embedMapUrl: "",
+ namaTempatMaps: "",
+ alamatMaps: "",
+ linkPetunjukArah: "",
+ layananPolsekId: "",
+ });
+
+ // load data untuk form edit
+ useEffect(() => {
+ const loadPolsekTerdekat = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await polsekState.edit.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || "",
+ jarakKeDesa: data.jarakKeDesa || "",
+ alamat: data.alamat || "",
+ nomorTelepon: data.nomorTelepon || "",
+ jamOperasional: data.jamOperasional || "",
+ embedMapUrl: data.embedMapUrl || "",
+ namaTempatMaps: data.namaTempatMaps || "",
+ alamatMaps: data.alamatMaps || "",
+ linkPetunjukArah: data.linkPetunjukArah || "",
+ layananPolsekId: data.layananPolsekId || "",
+ });
+ }
+ } catch (error) {
+ console.error("Error loading polsek terdekat:", error);
+ toast.error("Gagal memuat data polsek terdekat");
+ }
+ };
+
+ loadPolsekTerdekat();
+ }, [params?.id]);
+
+ const fetchLayanan = async () => {
+ try {
+ const res = await fetch("/api/keamanan/layanan-polsek/find-many");
+ const data = await res.json();
+
+ if (data.success) {
+ const options = data.data.map((item: any) => ({
+ value: item.id,
+ label: item.nama,
+ }));
+ setLayananOptions(options);
+ }
+ } catch {
+ toast.error("Gagal memuat layanan polsek");
+ }
+ };
+
+ const handleTambahLayanan = async () => {
+ if (!namaLayananBaru.trim())
+ return toast.warn("Nama layanan tidak boleh kosong");
+
+ try {
+ const res = await fetch("/api/keamanan/layanan-polsek/create", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ nama: namaLayananBaru }),
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ const newLayanan = {
+ value: data.data.id,
+ label: data.data.nama,
+ };
+ setLayananOptions((prev) => [...prev, newLayanan]);
+ await fetchLayanan();
+ polsekState.create.form.layananPolsekId = data.data.id;
+ toast.success("Layanan baru ditambahkan!");
+ setModalOpen(false);
+ setNamaLayananBaru("");
+ } else {
+ toast.error(data.message || "Gagal menambah layanan");
+ }
+ } catch {
+ toast.error("Error menambah layanan");
+ }
+ };
+
+ const handleUpdateLayanan = async (id: string, namaBaru: string) => {
+ if (!namaBaru.trim())
+ return toast.warn("Nama layanan tidak boleh kosong");
+
+ try {
+ const res = await fetch(`/api/keamanan/layanan-polsek/update/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ nama: namaBaru }),
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ await fetchLayanan();
+ toast.success("Layanan berhasil diupdate!");
+ setModalUpdateOpen(false);
+ setNamaLayananUpdate("");
+ } else {
+ toast.error(data.message || "Gagal mengupdate layanan");
+ }
+ } catch {
+ toast.error("Error mengupdate layanan");
+ }
+ };
+
+ const handleDeleteLayanan = async (id: string) => {
+ const confirmDelete = confirm("Yakin ingin menghapus layanan ini?");
+ if (!confirmDelete) return;
+
+ try {
+ const res = await fetch(`/api/keamanan/layanan-polsek/del/${id}`, {
+ method: "DELETE",
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ await fetchLayanan();
+ setLayananOptions((prev) =>
+ prev.filter((layanan) => layanan.value !== id)
+ );
+ toast.success("Layanan berhasil dihapus!");
+ } else {
+ toast.error(data.message || "Gagal menghapus layanan");
+ }
+ } catch {
+ toast.error("Error menghapus layanan");
+ }
+ };
+
+ useEffect(() => {
+ fetchLayanan();
+ }, []);
+
+ const handleChange = (field: string, value: string) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ polsekState.edit.form = { ...formData }; // update global state hanya di sini
+ await polsekState.edit.update();
+ toast.success("Polsek terdekat berhasil diperbarui!");
+ router.push("/admin/keamanan/polsek-terdekat");
+ } catch (error) {
+ console.error("Error updating polsek terdekat:", error);
+ toast.error("Gagal memperbarui data polsek terdekat");
+ }
+ };
+
+ return (
+
+ {/* Modal Tambah */}
+ setModalOpen(false)}
+ title="Tambah Layanan Polsek"
+ centered
+ >
+
+ setNamaLayananBaru(e.currentTarget.value)}
+ />
+ Simpan
+
+
+
+ {/* Modal Update */}
+ setModalUpdateOpen(false)}
+ title="Update Layanan Polsek"
+ centered
+ >
+
+ setNamaLayananUpdate(e.currentTarget.value)}
+ />
+ {
+ if (!selectedLayananId)
+ return toast.warn("ID layanan tidak ditemukan");
+ handleUpdateLayanan(selectedLayananId, namaLayananUpdate);
+ }}
+ >
+ Simpan
+
+
+
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Polsek Terdekat
+
+
+
+ {/* Form utama */}
+
+
+ {/* Input fields */}
+ handleChange("nama", e.currentTarget.value)}
+ label="Nama Polsek Terdekat"
+ placeholder="Masukkan nama Polsek Terdekat"
+ required
+ />
+ handleChange("jarakKeDesa", e.currentTarget.value)}
+ label="Jarak Polsek Terdekat"
+ />
+ handleChange("alamat", e.currentTarget.value)}
+ label="Alamat Polsek Terdekat"
+ />
+ handleChange("nomorTelepon", e.currentTarget.value)}
+ label="Nomor Telepon"
+ />
+ handleChange("jamOperasional", e.currentTarget.value)}
+ label="Jam Operasional"
+ />
+ handleChange("embedMapUrl", e.currentTarget.value)}
+ label="Embed Map URL"
+ />
+ handleChange("namaTempatMaps", e.currentTarget.value)}
+ label="Nama Tempat Maps"
+ />
+ handleChange("alamatMaps", e.currentTarget.value)}
+ label="Alamat Maps"
+ />
+ handleChange("linkPetunjukArah", e.currentTarget.value)}
+ label="Link Petunjuk Arah"
+ />
+
+ handleChange("layananPolsekId", val || "")}
+ />
+ setModalOpen(true)}
+ >
+ + Tambah Layanan Baru
+
+
+ {/* List layanan */}
+
+ Daftar Layanan Polsek
+
+ {layananOptions.map((item) => (
+
+
+ {item.label}
+
+ {
+ setSelectedLayananId(item.value);
+ setNamaLayananUpdate(item.label);
+ setModalUpdateOpen(true);
+ }}
+ >
+ Edit
+
+ handleDeleteLayanan(item.value)}
+ >
+ Hapus
+
+
+
+
+ ))}
+
+ {/* Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPolsekTerdekat;
diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx
new file mode 100644
index 00000000..8cc1ec2a
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx
@@ -0,0 +1,203 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function DetailPolsekTerdekat() {
+ const router = useRouter();
+ const polsekState = useProxy(polsekTerdekat);
+ const [selectedId, setSelectedId] = useState(null);
+ const [modalHapus, setModalHapus] = useState(false);
+ const params = useParams();
+
+ useShallowEffect(() => {
+ polsekState.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ polsekState.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/keamanan/polsek-terdekat");
+ }
+ };
+
+ if (!polsekState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = polsekState.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Polsek Terdekat
+
+
+
+
+ {/* Nama */}
+
+ Nama Polsek Terdekat
+ {data?.nama || "-"}
+
+
+ {/* Jarak */}
+
+ Jarak Polsek ke Desa
+ {data?.jarakKeDesa || "-"}
+
+
+ {/* Alamat */}
+
+ Alamat Polsek
+ {data?.alamat || "-"}
+
+
+ {/* Nomor */}
+
+ Nomor Polsek
+ {data?.nomorTelepon || "-"}
+
+
+ {/* Jam Operasional */}
+
+ Jam Operasional
+ {data?.jamOperasional || "-"}
+
+
+ {/* Google Maps */}
+
+ Google Maps
+ {data?.embedMapUrl ? (
+
+
+
+ ) : (
+ Tidak ada maps
+ )}
+
+
+ {/* Nama Tempat Maps */}
+
+ Nama Tempat Maps
+ {data?.namaTempatMaps || "-"}
+
+
+ {/* Alamat Maps */}
+
+ Alamat Maps
+ {data?.alamatMaps || "-"}
+
+
+ {/* Link Petunjuk Arah */}
+
+ Link Petunjuk Arah
+
+
+ {data?.linkPetunjukArah || "Tidak ada link"}
+
+
+
+
+ {/* Layanan Polsek */}
+
+ Layanan Polsek
+ {data?.layananPolsek?.nama || "-"}
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/keamanan/polsek-terdekat/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus polsek terdekat ini?"
+ />
+
+ );
+}
+
+export default DetailPolsekTerdekat;
diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/_com/converttoEmbed.ts b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/_com/converttoEmbed.ts
new file mode 100644
index 00000000..71328dcb
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/_com/converttoEmbed.ts
@@ -0,0 +1,9 @@
+function convertGoogleMapsToEmbed(url: string): string | null {
+ const match = url.match(/https:\/\/www\.google\.com\/maps\/place\/([^/]+)/);
+
+ if (!match) return null;
+
+ const place = match[1]; // misal: Polsek+Denpasar+Selatan
+ return `https://www.google.com/maps/embed/v1/place?key=YOUR_API_KEY&q=${place}`;
+ }
+export default convertGoogleMapsToEmbed
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx
new file mode 100644
index 00000000..1db2d012
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/create/page.tsx
@@ -0,0 +1,246 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Modal,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+
+function CreatePolsekTerdekat() {
+ const polsekState = useProxy(polsekTerdekat);
+ const router = useRouter();
+ const [layananOptions, setLayananOptions] = useState<{ value: string; label: string }[]>([]);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [namaLayananBaru, setNamaLayananBaru] = useState("");
+
+ const resetForm = () => {
+ polsekState.create.form = {
+ nama: "",
+ jarakKeDesa: "",
+ alamat: "",
+ nomorTelepon: "",
+ jamOperasional: "",
+ embedMapUrl: "",
+ namaTempatMaps: "",
+ alamatMaps: "",
+ linkPetunjukArah: "",
+ layananPolsekId: "",
+ };
+ };
+
+ const handleSubmit = async () => {
+ await polsekState.create.create();
+ resetForm();
+ router.push("/admin/keamanan/polsek-terdekat");
+ };
+
+ const fetchLayanan = async () => {
+ try {
+ const res = await fetch("/api/keamanan/layanan-polsek/find-many");
+ const data = await res.json();
+
+ if (data.success) {
+ const options = data.data.map((item: any) => ({
+ value: item.id,
+ label: item.nama,
+ }));
+ setLayananOptions(options);
+ }
+ } catch {
+ toast.error("Gagal memuat layanan polsek");
+ }
+ };
+
+ const handleTambahLayanan = async () => {
+ if (!namaLayananBaru.trim()) return toast.warn("Nama layanan tidak boleh kosong");
+
+ try {
+ const res = await fetch("/api/keamanan/layanan-polsek/create", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ nama: namaLayananBaru }),
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ const newLayanan = {
+ value: data.data.id,
+ label: data.data.nama,
+ };
+ setLayananOptions((prev) => [...prev, newLayanan]);
+ await fetchLayanan();
+ polsekState.create.form.layananPolsekId = data.data.id;
+ toast.success("Layanan baru ditambahkan!");
+ setModalOpen(false);
+ setNamaLayananBaru("");
+ } else {
+ toast.error(data.message || "Gagal menambah layanan");
+ }
+ } catch {
+ toast.error("Error menambah layanan");
+ }
+ };
+
+ useEffect(() => {
+ fetchLayanan();
+ }, []);
+
+ return (
+
+ {/* Modal Tambah Layanan */}
+ setModalOpen(false)}
+ title="Tambah Layanan Polsek"
+ centered
+ >
+
+ setNamaLayananBaru(e.currentTarget.value)}
+ />
+ Simpan
+
+
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Polsek Terdekat
+
+
+
+ {/* Form */}
+
+
+ (polsekState.create.form.nama = val.target.value)}
+ label={Nama Polsek Terdekat }
+ placeholder="Masukkan nama Polsek Terdekat"
+ required
+ />
+ (polsekState.create.form.jarakKeDesa = val.target.value)}
+ label={Jarak Polsek Terdekat }
+ placeholder="Masukkan jarak Polsek Terdekat"
+ required
+ />
+ (polsekState.create.form.alamat = val.target.value)}
+ label={Alamat Polsek Terdekat }
+ placeholder="Masukkan alamat Polsek Terdekat"
+ required
+ />
+ (polsekState.create.form.nomorTelepon = val.target.value)}
+ label={Nomor Telepon Polsek Terdekat }
+ placeholder="Masukkan nomor telepon Polsek Terdekat"
+ required
+ />
+ (polsekState.create.form.jamOperasional = val.target.value)}
+ label={Jam Operasional Polsek Terdekat }
+ placeholder="Masukkan jam operasional Polsek Terdekat"
+ />
+ (polsekState.create.form.embedMapUrl = val.target.value)}
+ label={Embed Map URL }
+ placeholder="Masukkan embed map url"
+ />
+ (polsekState.create.form.namaTempatMaps = val.target.value)}
+ label={Nama Tempat Maps }
+ placeholder="Masukkan nama tempat maps"
+ />
+ (polsekState.create.form.alamatMaps = val.target.value)}
+ label={Alamat Maps }
+ placeholder="Masukkan alamat maps"
+ />
+ (polsekState.create.form.linkPetunjukArah = val.target.value)}
+ label={Link Petunjuk Arah }
+ placeholder="Masukkan link petunjuk arah"
+ />
+
+ {/* Dropdown Select */}
+ Layanan Polsek}
+ placeholder="Pilih layanan polsek"
+ data={layananOptions}
+ value={polsekState.create.form.layananPolsekId}
+ onChange={(val) => (polsekState.create.form.layananPolsekId = val || "")}
+ />
+ setModalOpen(true)}
+ >
+ + Tambah Layanan Baru
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePolsekTerdekat;
diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx
new file mode 100644
index 00000000..22aa1d19
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx
@@ -0,0 +1,165 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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 polsekTerdekat from '../../_state/keamanan/polsek-terdekat';
+
+function PolsekTerdekat() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListPolsekTerdekat({ search }: { search: string }) {
+ const polsekState = useProxy(polsekTerdekat)
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = polsekState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ Daftar Polsek Terdekat
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/keamanan/polsek-terdekat/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Polsek
+ Jarak
+ Alamat
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.nama}
+
+
+
+ {item.jarakKeDesa}
+
+
+ {item.alamat}
+
+
+
+ router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data Polsek yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PolsekTerdekat;
diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx
new file mode 100644
index 00000000..2de3db48
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx
@@ -0,0 +1,255 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from "@mantine/core";
+import {
+ IconArrowBack,
+ IconImageInPicture,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from "@tabler/icons-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { useProxy } from "valtio/utils";
+
+import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
+import colors from "@/con/colors";
+import ApiFetch from "@/lib/api-fetch";
+import { Dropzone } from "@mantine/dropzone";
+import tipsKeamananState from "../../../../_state/keamanan/tips-keamanan";
+
+function EditTipsKeamanan() {
+ const keamananState = useProxy(tipsKeamananState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ judul: "",
+ deskripsi: "",
+ imageId: "",
+ });
+
+ // Load data saat pertama kali
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await keamananState.update.load(id);
+ if (data) {
+ setFormData({
+ judul: data.judul || "",
+ deskripsi: data.deskripsi || "",
+ imageId: data.imageId || "",
+ });
+
+ if (data?.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (error) {
+ console.error("Error loading tips keamanan:", error);
+ toast.error("Gagal memuat data tips keamanan");
+ }
+ };
+
+ loadData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ let imageId = formData.imageId;
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) return toast.error("Gagal upload gambar");
+ imageId = uploaded.id;
+ }
+
+ keamananState.update.form = {
+ ...formData,
+ imageId,
+ };
+
+ await keamananState.update.update();
+ toast.success("Tips Keamanan berhasil diperbarui!");
+ router.push("/admin/keamanan/tips-keamanan");
+ } catch (error) {
+ console.error("Error updating tips keamanan:", error);
+ toast.error("Terjadi kesalahan saat memperbarui tips keamanan");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Tips Keamanan
+
+
+
+ {/* Form Card */}
+
+
+ {/* Dropzone Upload */}
+
+
+ Gambar Tips Keamanan
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() =>
+ toast.error("File tidak valid, gunakan format gambar")
+ }
+ maxSize={5 * 1024 ** 2}
+ accept={{ "image/*": [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Input Judul */}
+
+ setFormData((prev) => ({ ...prev, judul: e.target.value }))
+ }
+ required
+ />
+
+ {/* Input Deskripsi */}
+
+
+ Deskripsi
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+ {/* Button Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditTipsKeamanan;
diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx
new file mode 100644
index 00000000..61bb32c9
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx
@@ -0,0 +1,145 @@
+'use client'
+import { useProxy } from 'valtio/utils';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+import colors from '@/con/colors';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
+
+function DetailTipsKeamanan() {
+ const stateKeamanan = useProxy(tipsKeamananState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ stateKeamanan.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateKeamanan.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/keamanan/tips-keamanan");
+ }
+ };
+
+ if (!stateKeamanan.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = stateKeamanan.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Tips Keamanan
+
+
+
+
+
+ Nama Tips Keamanan
+ {data.judul || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={stateKeamanan.delete.loading}
+ >
+
+
+
+
+
+ router.push(`/admin/keamanan/tips-keamanan/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus tips keamanan ini?"
+ />
+
+ );
+}
+
+export default DetailTipsKeamanan;
diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx
new file mode 100644
index 00000000..6f82636a
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx
@@ -0,0 +1,180 @@
+'use client';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
+
+function CreateKeamananLingkungan() {
+ const stateKeamanan = useProxy(tipsKeamananState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateKeamanan.create.form = {
+ judul: '',
+ deskripsi: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ stateKeamanan.create.form.imageId = uploaded.id;
+
+ await stateKeamanan.create.create();
+
+ resetForm();
+ router.push('/admin/keamanan/tips-keamanan');
+ };
+
+ return (
+
+ {/* Header Back + Title */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Tips Keamanan
+
+
+
+ {/* Form Card */}
+
+
+ {/* Upload Image */}
+
+
+ Gambar Tips Keamanan
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Input Judul */}
+ (stateKeamanan.create.form.judul = e.target.value)}
+ required
+ />
+
+ {/* Editor Deskripsi */}
+
+
+ Deskripsi Tips Keamanan
+
+ {
+ stateKeamanan.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Submit Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKeamananLingkungan;
diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx
new file mode 100644
index 00000000..7305d94c
--- /dev/null
+++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx
@@ -0,0 +1,156 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import { useState } from 'react';
+import HeaderSearch from '../../_com/header';
+import tipsKeamananState from '../../_state/keamanan/tips-keamanan';
+
+function TipsKeamanan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListTipsKeamanan({ search }: { search: string }) {
+ const stateKeamanan = useProxy(tipsKeamananState)
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateKeamanan.findMany
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ Daftar Tips Keamanan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/keamanan/tips-keamanan/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Judul
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.judul}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data tips keamanan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10)
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default TipsKeamanan;
diff --git a/src/app/admin/(dashboard)/kesehatan/_com/kesehatanEditor.tsx b/src/app/admin/(dashboard)/kesehatan/_com/kesehatanEditor.tsx
new file mode 100644
index 00000000..a4989a83
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/_com/kesehatanEditor.tsx
@@ -0,0 +1,93 @@
+'use client'
+import colors from '@/con/colors';
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+const content =
+ 'Welcome to Mantine rich text editor RichTextEditor component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. RichTextEditor is based on Tiptap.dev and supports all of its features:
General text formatting: bold , italic , underline , strike-through Headings (h1-h6) Sub and super scripts (<sup /> and <sub /> tags) Ordered and bullet lists Text align And all other extensions ';
+
+export function KesehatanEditor({showSubmit = true} : {
+ showSubmit: boolean
+}) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+
+ Submit
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/_com/kesehatanEditorText.tsx b/src/app/admin/(dashboard)/kesehatan/_com/kesehatanEditorText.tsx
new file mode 100644
index 00000000..c044bf51
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/_com/kesehatanEditorText.tsx
@@ -0,0 +1,95 @@
+'use client'
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+
+function KesehatanEditorText({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
+ onSubmit?: (val: string) => void,
+ onChange: (val: string) => void,
+ showSubmit?: boolean,
+ initialContent?: string }) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content: initialContent,
+ onUpdate : ({editor}) => {
+ onChange(editor.getHTML())
+ }
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+ {
+ if (!editor) return
+ onSubmit?.(editor?.getHTML())
+ }}>Submit
+ )}
+
+ );
+}
+
+export default KesehatanEditorText;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_com/TextEditor.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_com/TextEditor.tsx
new file mode 100644
index 00000000..c52391d2
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_com/TextEditor.tsx
@@ -0,0 +1,80 @@
+'use client'
+import { RichTextEditor, Link } from '@mantine/tiptap';
+import { useEditor } from '@tiptap/react';
+import Highlight from '@tiptap/extension-highlight';
+import StarterKit from '@tiptap/starter-kit';
+import Underline from '@tiptap/extension-underline';
+import TextAlign from '@tiptap/extension-text-align';
+import Superscript from '@tiptap/extension-superscript';
+import SubScript from '@tiptap/extension-subscript';
+
+const content =
+ 'Welcome to Mantine rich text editor RichTextEditor component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. RichTextEditor is based on Tiptap.dev and supports all of its features:
General text formatting: bold , italic , underline , strike-through Headings (h1-h6) Sub and super scripts (<sup /> and <sub /> tags) Ordered and bullet lists Text align And all other extensions ';
+
+function TextEditor() {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+export default TextEditor
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_com/kesehatanEditor.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_com/kesehatanEditor.tsx
new file mode 100644
index 00000000..b9740692
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_com/kesehatanEditor.tsx
@@ -0,0 +1,94 @@
+'use client'
+import { Button, Stack } from '@mantine/core';
+import { Link, RichTextEditor } from '@mantine/tiptap';
+import Highlight from '@tiptap/extension-highlight';
+import SubScript from '@tiptap/extension-subscript';
+import Superscript from '@tiptap/extension-superscript';
+import TextAlign from '@tiptap/extension-text-align';
+import Underline from '@tiptap/extension-underline';
+import { useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
+
+const content =
+ 'Welcome to Mantine rich text editor RichTextEditor component focuses on usability and is designed to be as simple as possible to bring a familiar editing experience to regular users. RichTextEditor is based on Tiptap.dev and supports all of its features:
General text formatting: bold , italic , underline , strike-through Headings (h1-h6) Sub and super scripts (<sup /> and <sub /> tags) Ordered and bullet lists Text align And all other extensions ';
+
+export function KesehatanEditor({ onSubmit, onChange, showSubmit = true }: {
+ onSubmit?: (val: string) => void,
+ onChange: (val: string) => void,
+ showSubmit?: boolean }) {
+ const editor = useEditor({
+ extensions: [
+ StarterKit,
+ Underline,
+ Link,
+ Superscript,
+ SubScript,
+ Highlight,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
+ ],
+ immediatelyRender: false,
+ content,
+ onUpdate : ({editor}) => {
+ onChange(editor.getHTML())
+ }
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSubmit && (
+ {
+ if (!editor) return
+ onSubmit?.(editor?.getHTML())
+ }}>Submit
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..8bcb314a
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx
@@ -0,0 +1,147 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+
+ const tabs = [
+ {
+ label: "Presentase Kelahiran & Kematian",
+ value: "presentasekelahiran&kematian",
+ href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
+ icon: ,
+ tooltip: "Lihat data kelahiran dan kematian"
+ },
+ {
+ label: "Grafik Hasil Kepuasan Masyarakat",
+ value: "grafikhasilkepuasan",
+ href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
+ icon: ,
+ tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
+ },
+ {
+ label: "Fasilitas Kesehatan",
+ value: "fasilitaskesehatan",
+ href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
+ icon: ,
+ tooltip: "Data fasilitas kesehatan desa"
+ },
+ {
+ label: "Jadwal Kegiatan",
+ value: "jadwalkegiatan",
+ href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
+ icon: ,
+ tooltip: "Atur jadwal kegiatan kesehatan"
+ },
+ {
+ label: "Artikel Kesehatan",
+ value: "artikelkesehatan",
+ href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
+ icon: ,
+ tooltip: "Artikel & informasi seputar kesehatan"
+ },
+ ];
+
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+
+ Data Kesehatan Warga
+
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+
+export default LayoutTabs;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx
new file mode 100644
index 00000000..227c8c1f
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx
@@ -0,0 +1,366 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+interface ArtikelKesehatanFormBase {
+ title: string;
+ content: string;
+ imageId: string;
+ introduction: { content: string };
+ symptom: { title: string; content: string };
+ prevention: { title: string; content: string };
+ firstAid: { title: string; content: string };
+ mythVsFact: { title: string; mitos: string; fakta: string };
+ doctorSign: { content: string };
+}
+
+function EditArtikelKesehatan() {
+ const stateArtikelKesehatan = useProxy(artikelKesehatanState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ title: '',
+ content: '',
+ imageId: '',
+ introduction: { content: '' },
+ symptom: { title: '', content: '' },
+ prevention: { title: '', content: '' },
+ firstAid: { title: '', content: '' },
+ mythVsFact: { title: '', mitos: '', fakta: '' },
+ doctorSign: { content: '' },
+ });
+
+ // Load data artikel
+ useEffect(() => {
+ const loadArtikelKesehatan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ await stateArtikelKesehatan.edit.load(id);
+ const { form } = stateArtikelKesehatan.edit;
+ if (!form) return;
+
+ setFormData({
+ title: form.title || '',
+ content: form.content || '',
+ imageId: form.imageId || '',
+ introduction: { content: form.introduction?.content || '' },
+ symptom: { title: form.symptom?.title || '', content: form.symptom?.content || '' },
+ prevention: { title: form.prevention?.title || '', content: form.prevention?.content || '' },
+ firstAid: { title: form.firstAid?.title || '', content: form.firstAid?.content || '' },
+ mythVsFact: {
+ title: form.mythVsFact?.title || '',
+ mitos: form.mythVsFact?.mitos || '',
+ fakta: form.mythVsFact?.fakta || '',
+ },
+ doctorSign: { content: form.doctorSign?.content || '' },
+ });
+
+ if (form.imageId) {
+ setPreviewImage(`${process.env.NEXT_PUBLIC_API_URL}/file/${form.imageId}`);
+ }
+ } catch (error) {
+ console.error('Error loading artikel kesehatan:', error);
+ toast.error('Gagal memuat data artikel kesehatan');
+ }
+ };
+
+ loadArtikelKesehatan();
+ }, [params?.id]);
+
+ const handleFileChange = (files: File[]) => {
+ const selectedFile = files[0];
+ if (!selectedFile) return;
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Copy formData ke global state
+ stateArtikelKesehatan.edit.form = { ...formData };
+
+ // Upload gambar kalau ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ stateArtikelKesehatan.edit.form.imageId = uploaded.id;
+ }
+
+ const success = await stateArtikelKesehatan.edit.submit();
+ if (success) {
+ toast.success('Artikel kesehatan berhasil diperbarui!');
+ router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
+ }
+ } catch (error) {
+ console.error('Error updating artikel kesehatan:', error);
+ toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data artikel kesehatan');
+ }
+ };
+
+ const InputText = ({
+ label,
+ value,
+ onChange,
+ placeholder,
+ required,
+ }: {
+ label: string;
+ value: string;
+ onChange: (v: string) => void;
+ placeholder?: string;
+ required?: boolean;
+ }) => (
+ onChange(e.target.value)}
+ required={required}
+ />
+ );
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Artikel Kesehatan
+
+
+
+ {/* Form */}
+
+
+ {/* Judul */}
+ setFormData((prev) => ({ ...prev, title: value }))}
+ placeholder="Masukkan judul artikel"
+ required
+ />
+
+ {/* Gambar */}
+
+
+ Gambar Artikel Kesehatan
+
+ toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Konten */}
+ setFormData((prev) => ({ ...prev, content: value }))}
+ placeholder="Masukkan deskripsi artikel"
+ required
+ />
+
+ {/* Pendahuluan */}
+
+ Pendahuluan
+
+ setFormData((prev) => ({ ...prev, introduction: { ...prev.introduction, content: value } }))
+ }
+ />
+
+ {/* Gejala */}
+
+ Gejala
+
+
+ setFormData((prev) => ({ ...prev, symptom: { ...prev.symptom, title: value } }))
+ }
+ />
+
+ setFormData((prev) => ({ ...prev, symptom: { ...prev.symptom, content: value } }))
+ }
+ />
+
+
+
+ {/* Pencegahan */}
+
+ Pencegahan
+
+ setFormData((prev) => ({ ...prev, prevention: { ...prev.prevention, title: value } }))
+ }
+ />
+
+ setFormData((prev) => ({ ...prev, prevention: { ...prev.prevention, content: value } }))
+ }
+ />
+
+
+ {/* Pertolongan Pertama */}
+
+ Pertolongan Pertama
+
+ setFormData((prev) => ({ ...prev, firstAid: { ...prev.firstAid, title: value } }))
+ }
+ />
+
+ setFormData((prev) => ({ ...prev, firstAid: { ...prev.firstAid, content: value } }))
+ }
+ />
+
+
+ {/* Mitos vs Fakta */}
+
+ Mitos vs Fakta
+
+ setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, title: value } }))
+ }
+ />
+ Mitos
+
+ setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, mitos: value } }))
+ }
+ />
+ Fakta
+
+ setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, fakta: value } }))
+ }
+ />
+
+
+ {/* Dokter */}
+
+ Kapan Harus Ke Dokter
+
+ setFormData((prev) => ({ ...prev, doctorSign: { content: value } }))
+ }
+ />
+
+
+ {/* Save button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditArtikelKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx
new file mode 100644
index 00000000..e03b527f
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx
@@ -0,0 +1,208 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailArtikelKesehatan() {
+ const params = useParams();
+ const router = useRouter();
+ const state = useProxy(artikelKesehatanState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Artikel Kesehatan
+
+
+
+
+ {/* Judul */}
+
+ Judul
+ {data.title}
+
+ {/* Gambar */}
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+ {/* Deskripsi */}
+
+ Deskripsi
+
+
+
+ {/* Pendahuluan */}
+
+ Pendahuluan
+
+
+
+ {/* Gejala */}
+
+ Gejala
+ Judul
+ {data.symptom?.title}
+ Deskripsi
+
+
+
+ {/* Pencegahan */}
+
+ Pencegahan
+ Judul
+ {data.prevention?.title}
+ Deskripsi
+
+
+
+ {/* Pertolongan Pertama */}
+
+ Pertolongan Pertama
+ Judul
+ {data.firstaid?.title}
+ Deskripsi
+
+
+
+ {/* Mitos vs Fakta */}
+
+ Mitos dan Fakta
+ Judul
+ {data.mythvsfact?.title}
+ Mitos
+
+ Fakta
+
+
+
+ {/* Kapan ke Dokter */}
+
+ Kapan Harus ke Dokter
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus artikel kesehatan ini?"
+ />
+
+ );
+}
+
+export default DetailArtikelKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx
new file mode 100644
index 00000000..7289cd05
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx
@@ -0,0 +1,325 @@
+'use client'
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreateArtikelKesehatan() {
+ const stateArtikelKesehatan = useProxy(artikelKesehatanState);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateArtikelKesehatan.create.form = {
+ title: '',
+ content: '',
+ imageId: '',
+ introduction: {
+ content: '',
+ },
+ symptom: {
+ title: '',
+ content: '',
+ },
+ prevention: {
+ title: '',
+ content: '',
+ },
+ firstAid: {
+ title: '',
+ content: '',
+ },
+ mythVsFact: {
+ title: '',
+ mitos: '',
+ fakta: '',
+ },
+ doctorSign: {
+ content: '',
+ },
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async (e?: React.FormEvent) => {
+ e?.preventDefault();
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ stateArtikelKesehatan.create.form.imageId = uploaded.id;
+ await stateArtikelKesehatan.create.submit();
+ toast.success('Data berhasil disimpan');
+ resetForm();
+ router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Artikel Kesehatan
+
+
+
+ {/* Form */}
+
+
+
+
+ Gambar Artikel Kesehatan
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {
+ stateArtikelKesehatan.create.form.title = e.target.value;
+ }}
+ required
+ />
+ {
+ stateArtikelKesehatan.create.form.content = e.target.value;
+ }}
+ required
+ />
+
+ Pendahuluan
+ {
+ stateArtikelKesehatan.create.form.introduction.content = e;
+ }}
+ />
+
+ {/* Gejala */}
+
+ Gejala
+
+ {
+ stateArtikelKesehatan.create.form.symptom.title = e.target.value;
+ }}
+ />
+
+ Deskripsi Gejala
+ {
+ stateArtikelKesehatan.create.form.symptom.content = e;
+ }}
+ />
+
+
+
+
+ {/* Pencegahan */}
+
+ Pencegahan
+ {
+ stateArtikelKesehatan.create.form.prevention.title = e.target.value;
+ }}
+ />
+ Deskripsi Pencegahan
+ {
+ stateArtikelKesehatan.create.form.prevention.content = e;
+ }}
+ />
+
+
+ {/* Pertolongan Pertama */}
+
+ Pertolongan Pertama
+ {
+ stateArtikelKesehatan.create.form.firstAid.title = e.target.value;
+ }}
+ />
+ Deskripsi Pertolongan Pertama
+ {
+ stateArtikelKesehatan.create.form.firstAid.content = e;
+ }}
+ />
+
+
+ {/* Mitos vs Fakta */}
+
+ Mitos dan Fakta
+ {
+ stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value;
+ }}
+ />
+
+ Mitos
+ {
+ stateArtikelKesehatan.create.form.mythVsFact.mitos = e;
+ }}
+ />
+
+
+ Fakta
+ {
+ stateArtikelKesehatan.create.form.mythVsFact.fakta = e;
+ }}
+ />
+
+
+
+ {/* Kapan Harus ke Dokter */}
+
+ Kapan Harus ke Dokter
+ {
+ stateArtikelKesehatan.create.form.doctorSign.content = e;
+ }}
+ />
+
+
+ {/* Submit Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateArtikelKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx
new file mode 100644
index 00000000..961c6630
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx
@@ -0,0 +1,156 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
+
+function ArtikelKesehatan() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListArtikelKesehatan({ search }: { search: string }) {
+ const stateArtikel = useProxy(artikelKesehatanState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = stateArtikel.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Artikel Kesehatan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Judul
+ Konten
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.title}
+
+
+
+
+ {item.content}
+
+
+
+ router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada artikel yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default ArtikelKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx
new file mode 100644
index 00000000..8a25e7ba
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx
@@ -0,0 +1,257 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+interface FasilitasKesehatanFormBase {
+ name: string;
+ informasiUmum: {
+ fasilitas: string;
+ alamat: string;
+ jamOperasional: string;
+ };
+ layananUnggulan: { content: string };
+ dokterdanTenagaMedis: {
+ name: string;
+ specialist: string;
+ jadwal: string;
+ };
+ fasilitasPendukung: { content: string };
+ prosedurPendaftaran: { content: string };
+ tarifDanLayanan: {
+ layanan: string;
+ tarif: string;
+ };
+}
+
+function EditFasilitasKesehatan() {
+ const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
+ layananUnggulan: { content: '' },
+ dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
+ fasilitasPendukung: { content: '' },
+ prosedurPendaftaran: { content: '' },
+ tarifDanLayanan: { layanan: '', tarif: '' },
+ });
+
+ // Helper untuk update nested state
+ const updateForm = (
+ key: K,
+ value: FasilitasKesehatanFormBase[K]
+ ) => setFormData(prev => ({ ...prev, [key]: value }));
+
+ const updateNested = <
+ K extends keyof FasilitasKesehatanFormBase,
+ N extends keyof FasilitasKesehatanFormBase[K]
+ >(key: K, nestedKey: N, value: FasilitasKesehatanFormBase[K][N]) =>
+ setFormData(prev => ({
+ ...prev,
+ [key]: { ...prev[key] as object, [nestedKey]: value },
+ }));
+
+ // Load data
+ useEffect(() => {
+ const load = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+ try {
+ await state.edit.load(id);
+ const form = state.edit.form;
+ if (form) setFormData(form as FasilitasKesehatanFormBase);
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memuat data fasilitas kesehatan');
+ }
+ };
+ load();
+ }, [params?.id]);
+
+ // Submit
+ const handleSubmit = async () => {
+ try {
+ state.edit.form = { ...state.edit.form, ...formData };
+ const success = await state.edit.submit();
+ if (success) {
+ toast.success('Fasilitas kesehatan berhasil diperbarui!');
+ router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan');
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Fasilitas Kesehatan
+
+
+
+ {/* Form */}
+
+
+ updateForm('name', e.target.value)}
+ required
+ />
+
+ {/* Informasi Umum */}
+
+
+ Informasi Umum
+
+ updateNested('informasiUmum', 'fasilitas', e.target.value)}
+ />
+ updateNested('informasiUmum', 'alamat', e.target.value)}
+ />
+ updateNested('informasiUmum', 'jamOperasional', e.target.value)}
+ />
+
+
+ {/* Layanan Unggulan */}
+
+
+ Layanan Unggulan
+
+ updateNested('layananUnggulan', 'content', v)}
+ />
+
+
+ {/* Dokter dan Tenaga Medis */}
+
+
+ Dokter dan Tenaga Medis
+
+ updateNested('dokterdanTenagaMedis', 'name', e.target.value)}
+ />
+
+ updateNested('dokterdanTenagaMedis', 'specialist', e.target.value)
+ }
+ />
+ updateNested('dokterdanTenagaMedis', 'jadwal', e.target.value)}
+ />
+
+
+ {/* Fasilitas Pendukung */}
+
+
+ Fasilitas Pendukung
+
+ updateNested('fasilitasPendukung', 'content', v)}
+ />
+
+
+ {/* Prosedur Pendaftaran */}
+
+
+ Prosedur Pendaftaran
+
+ updateNested('prosedurPendaftaran', 'content', v)}
+ />
+
+
+ {/* Tarif dan Layanan */}
+
+
+ Tarif dan Layanan
+
+ updateNested('tarifDanLayanan', 'tarif', e.target.value)}
+ />
+ updateNested('tarifDanLayanan', 'layanan', e.target.value)}
+ />
+
+
+ {/* Tombol Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditFasilitasKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx
new file mode 100644
index 00000000..d6640e65
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx
@@ -0,0 +1,178 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailFasilitasKesehatan() {
+ const params = useParams();
+ const router = useRouter();
+ const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'
+ );
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Fasilitas Kesehatan
+
+
+
+
+
+ Nama Fasilitas
+ {data.name || '-'}
+
+
+
+ Informasi Umum
+ Fasilitas
+ {data.informasiumum?.fasilitas || '-'}
+ Alamat
+ {data.informasiumum?.alamat || '-'}
+ Jam Operasional
+ {data.informasiumum?.jamOperasional || '-'}
+
+
+
+ Layanan Unggulan
+
+
+
+
+ Fasilitas Pendukung
+
+
+
+
+ Prosedur Pendaftaran
+
+
+
+
+ Dokter & Tenaga Medis
+ Nama
+ {data.dokterdantenagamedis?.name || '-'}
+ Spesialis
+ {data.dokterdantenagamedis?.specialist || '-'}
+ Jadwal
+ {data.dokterdantenagamedis?.jadwal || '-'}
+
+
+
+ Tarif & Layanan
+ Layanan
+ {data.tarifdanlayanan?.layanan || '-'}
+ Tarif
+ {data.tarifdanlayanan?.tarif || '-'}
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus fasilitas kesehatan ini?"
+ />
+
+ );
+}
+
+export default DetailFasilitasKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx
new file mode 100644
index 00000000..530039a9
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx
@@ -0,0 +1,219 @@
+'use client'
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+
+function CreateFasilitasKesehatan() {
+ const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateFasilitasKesehatan.create.form = {
+ name: '',
+ informasiUmum: {
+ fasilitas: '',
+ alamat: '',
+ jamOperasional: '',
+ },
+ layananUnggulan: {
+ content: '',
+ },
+ dokterdanTenagaMedis: {
+ name: '',
+ specialist: '',
+ jadwal: '',
+ },
+ fasilitasPendukung: {
+ content: '',
+ },
+ prosedurPendaftaran: {
+ content: '',
+ },
+ tarifDanLayanan: {
+ layanan: '',
+ tarif: '',
+ },
+ };
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ await stateFasilitasKesehatan.create.submit();
+ toast.success('Data berhasil disimpan');
+ resetForm();
+ router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Data Fasilitas Kesehatan
+
+
+
+ {/* Form */}
+
+
+ (stateFasilitasKesehatan.create.form.name = e.target.value)}
+ required
+ />
+
+ {/* Informasi Umum */}
+
+ Informasi Umum
+ (stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value)}
+ required
+ />
+ (stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value)}
+ required
+ />
+ (stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value)}
+ required
+ />
+
+
+ {/* Layanan Unggulan */}
+
+ Layanan Unggulan
+ (stateFasilitasKesehatan.create.form.layananUnggulan.content = val)}
+ />
+
+
+ {/* Dokter dan Tenaga Medis */}
+
+ Dokter dan Tenaga Medis
+ (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)}
+ required
+ />
+ (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)}
+ required
+ />
+ (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)}
+ required
+ />
+
+
+ {/* Fasilitas Pendukung */}
+
+ Fasilitas Pendukung
+ (stateFasilitasKesehatan.create.form.fasilitasPendukung.content = val)}
+ />
+
+
+ {/* Prosedur Pendaftaran */}
+
+ Prosedur Pendaftaran
+ (stateFasilitasKesehatan.create.form.prosedurPendaftaran.content = val)}
+ />
+
+
+ {/* Tarif dan Layanan */}
+
+ Tarif dan Layanan
+ (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)}
+ required
+ />
+ (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)}
+ required
+ />
+
+
+ {/* Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateFasilitasKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx
new file mode 100644
index 00000000..69da2f21
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+function Page() {
+ return (
+
+ Page
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx
new file mode 100644
index 00000000..69da2f21
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+function Page() {
+ return (
+
+ Page
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx
new file mode 100644
index 00000000..71bf934b
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx
@@ -0,0 +1,75 @@
+'use client'
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+
+function CreateDokter() {
+ const params = useParams()
+ const createState = useProxy(fasilitasKesehatanState.dokter)
+ const router = useRouter();
+
+ const resetForm = () => {
+ createState.create.create.form = {
+ name: "",
+ specialist: "",
+ jadwal: "",
+ };
+ };
+
+ const handleSubmit = async () => {
+ await createState.create.create.create();
+ resetForm();
+ router.push(`/admin/kesehatan/fasilitas-kesehatan/${params?.id}/dokter-tenaga-medis`)
+ };
+ return (
+
+
+ router.back()}>
+
+
+
+
+
+
+ Create Dokter
+ Nama Dokter}
+ placeholder="masukkan nama dokter"
+ defaultValue={createState.create.create.form.name}
+ onChange={(e) => {
+ createState.create.create.form.name = e.target.value;
+ }}
+ />
+ Specialist
+ Specialist}
+ placeholder="masukkan specialist"
+ defaultValue={createState.create.create.form.specialist}
+ onChange={(e) => {
+ createState.create.create.form.specialist = e.target.value;
+ }}
+ />
+
+ Jadwal
+ {
+ createState.create.create.form.jadwal = htmlContent;
+ }}
+ />
+
+
+ Simpan
+
+
+
+
+ );
+}
+
+export default CreateDokter;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx
new file mode 100644
index 00000000..94a1ad90
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx
@@ -0,0 +1,112 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
+import JudulList from '@/app/admin/(dashboard)/_com/judulList';
+import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+import { useState } from 'react';
+
+
+function DokterTenagaMedis() {
+ const [search, setSearch] = useState("");
+ const router = useRouter();
+ return (
+
+
+ router.back()}>
+
+
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListDokterTenagaMedis({ search }: { search: string }) {
+ const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter)
+ const router = useRouter();
+ const {
+ data,
+ loading,
+ load,
+ page,
+ totalPages
+ } = stateFasilitasKesehatan.findMany
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+
+
+
+
+
+
+
+ Fasilitas Kesehatan
+ Alamat
+ Jam Operasional
+ Detail
+
+
+
+ {filteredData.map((item) => (
+
+ {item.name}
+ {item.specialist}
+
+
+
+
+ router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
+
+
+
+
+ ))}
+
+
+
+
+
+
+ load(newPage)} // ini penting!
+ total={totalPages}
+ mt="md"
+ mb="md"
+ />
+
+
+ )
+}
+
+export default DokterTenagaMedis;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx
new file mode 100644
index 00000000..8c554c6a
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx
@@ -0,0 +1,186 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Pagination,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, 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 fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+
+
+function FasilitasKesehatan() {
+ const router = useRouter();
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Tombol Back */}
+
+ router.back()}>
+
+
+
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+
+function ListFasilitasKesehatan({ search }: { search: string }) {
+ const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan)
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Fasilitas Kesehatan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Fasilitas Kesehatan
+ Dokter
+ Layanan
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+ {item.dokterdantenagamedis?.name || '-'}
+
+
+
+
+
+ {item.tarifdanlayanan?.layanan || '-'}
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada fasilitas kesehatan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ )
+}
+
+export default FasilitasKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx
new file mode 100644
index 00000000..eb82a5f9
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx
@@ -0,0 +1,112 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
+import JudulList from '@/app/admin/(dashboard)/_com/judulList';
+import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
+import { useState } from 'react';
+
+
+function TarifLayanan() {
+ const [search, setSearch] = useState("");
+ const router = useRouter();
+ return (
+
+
+ router.back()}>
+
+
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListTarifLayanan({ search }: { search: string }) {
+ const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter)
+ const router = useRouter();
+ const {
+ data,
+ loading,
+ load,
+ page,
+ totalPages
+ } = stateFasilitasKesehatan.findMany
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+
+
+
+
+
+
+
+ Fasilitas Kesehatan
+ Alamat
+ Jam Operasional
+ Detail
+
+
+
+ {filteredData.map((item) => (
+
+ {item.name}
+ {item.specialist}
+
+
+
+
+ router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
+
+
+
+
+ ))}
+
+
+
+
+
+
+ load(newPage)} // ini penting!
+ total={totalPages}
+ mt="md"
+ mb="md"
+ />
+
+
+ )
+}
+
+export default TarifLayanan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx
new file mode 100644
index 00000000..f6501312
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx
@@ -0,0 +1,137 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditGrafikHasilKepuasan() {
+ const editState = useProxy(grafikkepuasan);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ tanggal: '',
+ jenisKelamin: '',
+ alamat: '',
+ penyakit: '',
+ });
+
+ // Load data once
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await editState.update.load(id);
+ if (data) setFormData({
+ nama: data.nama || '',
+ tanggal: data.tanggal || '',
+ jenisKelamin: data.jenisKelamin || '',
+ alamat: data.alamat || '',
+ penyakit: data.penyakit || '',
+ });
+ } catch (err) {
+ console.error("Error loading grafik hasil kepuasan:", err);
+ toast.error("Gagal memuat data grafik hasil kepuasan");
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ // Generic handler for controlled inputs
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ editState.update.form = { ...editState.update.form, ...formData };
+ await editState.update.submit();
+ toast.success('Grafik hasil kepuasan berhasil diperbarui!');
+ router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
+ } catch (err) {
+ console.error('Error updating grafik hasil kepuasan:', err);
+ toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Edit Grafik Hasil Kepuasan
+
+
+
+ {/* Form */}
+
+
+ {(['nama','tanggal','jenisKelamin','alamat','penyakit'] as const).map((field) => (
+ handleChange(field, e.target.value)}
+ type={field === 'tanggal' ? 'date' : 'text'}
+ label={field === 'jenisKelamin' ? 'Jenis Kelamin' : field.charAt(0).toUpperCase() + field.slice(1)}
+ placeholder={`Masukkan ${field}`}
+ required
+ />
+ ))}
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditGrafikHasilKepuasan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx
new file mode 100644
index 00000000..c80da5f8
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx
@@ -0,0 +1,151 @@
+'use client'
+import { useProxy } from 'valtio/utils';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
+import colors from '@/con/colors';
+
+function DetailGrafikHasilKepuasan() {
+ const state = useProxy(grafikkepuasan);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Data Grafik Hasil Kepuasan
+
+
+
+
+
+ Nama
+ {data.nama || '-'}
+
+
+
+ Tanggal
+
+ {new Date(data.tanggal).toLocaleDateString("id-ID", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ })}
+
+
+
+
+ Jenis Kelamin
+ {data.jenisKelamin || '-'}
+
+
+
+ Alamat
+ {data.alamat || '-'}
+
+
+
+ Penyakit
+ {data.penyakit || '-'}
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus data ini?"
+ />
+
+ );
+}
+
+export default DetailGrafikHasilKepuasan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx
new file mode 100644
index 00000000..e33b6e1a
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx
@@ -0,0 +1,129 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function CreateGrafikHasilKepuasanMasyarakat() {
+ const stateGrafikKepuasan = useProxy(grafikkepuasan);
+ const [chartData, setChartData] = useState([]);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateGrafikKepuasan.create.form = {
+ nama: "",
+ tanggal: "",
+ jenisKelamin: "",
+ alamat: "",
+ penyakit: "",
+ };
+ };
+
+ const handleSubmit = async () => {
+ await stateGrafikKepuasan.create.create();
+ resetForm();
+ router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Grafik Hasil Kepuasan Masyarakat
+
+
+
+ {/* Form */}
+
+
+ (stateGrafikKepuasan.create.form.nama = e.target.value)}
+ required
+ />
+ (stateGrafikKepuasan.create.form.tanggal = e.target.value)}
+ required
+ />
+ (stateGrafikKepuasan.create.form.jenisKelamin = e.target.value)}
+ required
+ />
+ (stateGrafikKepuasan.create.form.alamat = e.target.value)}
+ required
+ />
+ (stateGrafikKepuasan.create.form.penyakit = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateGrafikHasilKepuasanMasyarakat;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx
new file mode 100644
index 00000000..1897aba3
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx
@@ -0,0 +1,258 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Bar, BarChart, Tooltip as ChartTooltip, Legend, XAxis, YAxis } from 'recharts';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
+
+function GrafikHasilKepuasanMasyarakat() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
+ type PDKMGrafik = {
+ id: string;
+ nama: string;
+ tanggal: string | Date;
+ jenisKelamin: string;
+ alamat: string;
+ penyakit: string;
+ };
+
+ const stateGrafikKepuasan = useProxy(grafikkepuasan);
+ const [chartData, setChartData] = useState([]);
+ const [mounted, setMounted] = useState(false);
+ const isTablet = useMediaQuery('(max-width: 1024px)');
+ const isMobile = useMediaQuery('(max-width: 768px)');
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = stateGrafikKepuasan.findMany;
+
+ useShallowEffect(() => {
+ setMounted(true);
+ load(page, 10, search);
+ }, [page, search]);
+
+ useEffect(() => {
+ if (data) {
+ setChartData(data.map((item) => ({
+ ...item,
+ tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal
+ })));
+ }
+ }, [data]);
+
+ const processDiseaseData = (data: PDKMGrafik[]) => {
+ const diseaseCount: Record = {};
+ data.forEach(item => {
+ const penyakit = item.penyakit.trim();
+ if (penyakit) {
+ diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1;
+ }
+ });
+ return Object.entries(diseaseCount).map(([name, count]) => ({ name, count }));
+ };
+
+ const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]);
+
+ useEffect(() => {
+ if (data && data.length > 0) {
+ setDiseaseChartData(processDiseaseData(data));
+ }
+ }, [data]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Grafik Hasil Kepuasan Masyarakat
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Nama
+ Tanggal
+ Jenis Kelamin
+ Penyakit
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.nama}
+
+
+
+
+ {new Date(item.tanggal).toLocaleDateString('id-ID', {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ })}
+
+
+
+
+ {item.jenisKelamin}
+
+
+
+
+ {item.penyakit}
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data kepuasan masyarakat yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ {/* Chart */}
+
+
+ Grafik Hasil Kepuasan Masyarakat
+ {mounted && diseaseChartData.length > 0 ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
+
+
+
+ );
+}
+
+export default GrafikHasilKepuasanMasyarakat;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx
new file mode 100644
index 00000000..de1b5922
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx
@@ -0,0 +1,214 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+interface JadwalKegiatanFormBase {
+ content: string;
+ informasiJadwalKegiatan: {
+ name: string;
+ tanggal: string;
+ waktu: string;
+ lokasi: string;
+ };
+ deskripsiJadwalKegiatan: { deskripsi: string };
+ layananJadwalKegiatan: { content: string };
+ syaratKetentuanJadwalKegiatan: { content: string };
+ dokumenJadwalKegiatan: { content: string };
+}
+
+const emptyForm = (): JadwalKegiatanFormBase => ({
+ content: '',
+ informasiJadwalKegiatan: { name: '', tanggal: '', waktu: '', lokasi: '' },
+ deskripsiJadwalKegiatan: { deskripsi: '' },
+ layananJadwalKegiatan: { content: '' },
+ syaratKetentuanJadwalKegiatan: { content: '' },
+ dokumenJadwalKegiatan: { content: '' },
+});
+
+function EditJadwalKegiatan() {
+ const stateJadwalKegiatan = useProxy(jadwalKegiatanState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState(emptyForm());
+
+ // Helper untuk update nested state
+ const updateNested = <
+ K extends keyof JadwalKegiatanFormBase,
+ N extends keyof JadwalKegiatanFormBase[K]
+ >(
+ key: K,
+ subKey: N,
+ value: string
+ ) => {
+ setFormData(prev => ({
+ ...prev,
+ [key]: {
+ ...(prev[key] as Record),
+ [subKey]: value
+ }
+ }));
+ };
+
+ const updateSimple = (key: keyof JadwalKegiatanFormBase, value: string) => {
+ setFormData(prev => ({ ...prev, [key]: value }));
+ };
+
+ useEffect(() => {
+ const loadJadwalKegiatan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ await stateJadwalKegiatan.edit.load(id);
+ const { form } = stateJadwalKegiatan.edit;
+ if (form) {
+ setFormData({
+ content: form.content || '',
+ informasiJadwalKegiatan: {
+ name: form.informasiJadwalKegiatan?.name || '',
+ tanggal: form.informasiJadwalKegiatan?.tanggal || '',
+ waktu: form.informasiJadwalKegiatan?.waktu || '',
+ lokasi: form.informasiJadwalKegiatan?.lokasi || '',
+ },
+ deskripsiJadwalKegiatan: { deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '' },
+ layananJadwalKegiatan: { content: form.layananJadwalKegiatan?.content || '' },
+ syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' },
+ dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' },
+ });
+ }
+ } catch (error) {
+ console.error("Error loading jadwal kegiatan:", error);
+ toast.error("Gagal memuat data jadwal kegiatan");
+ }
+ };
+ loadJadwalKegiatan();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ stateJadwalKegiatan.edit.form = { ...stateJadwalKegiatan.edit.form, ...formData };
+ const success = await stateJadwalKegiatan.edit.submit();
+ if (success) {
+ toast.success("Jadwal kegiatan berhasil diperbarui!");
+ router.push("/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan");
+ }
+ } catch (error) {
+ console.error("Error updating jadwal kegiatan:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan");
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Jadwal Kegiatan
+
+
+
+ {/* Form */}
+
+
+ {/* Nama Jadwal */}
+ updateSimple('content', e.target.value)}
+ />
+
+ {/* Deskripsi */}
+
+ Deskripsi Jadwal Kegiatan
+ updateNested('deskripsiJadwalKegiatan', 'deskripsi', val)}
+ />
+
+
+ {/* Informasi Jadwal */}
+
+ Informasi Jadwal Kegiatan
+ {(['name', 'tanggal', 'waktu', 'lokasi'] as const).map((field) => (
+ updateNested('informasiJadwalKegiatan', field, e.target.value)}
+ />
+ ))}
+
+
+ {/* Layanan */}
+
+ Layanan Jadwal Kegiatan
+ updateNested('layananJadwalKegiatan', 'content', val)}
+ />
+
+
+ {/* Syarat */}
+
+ Syarat dan Ketentuan
+ updateNested('syaratKetentuanJadwalKegiatan', 'content', val)}
+ />
+
+
+ {/* Dokumen */}
+
+ Dokumen Yang Perlu Dibawa
+ updateNested('dokumenJadwalKegiatan', 'content', val)}
+ />
+
+
+ {/* Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditJadwalKegiatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx
new file mode 100644
index 00000000..383cc54c
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx
@@ -0,0 +1,160 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+function DetailJadwalKegiatan() {
+ const params = useParams()
+ const router = useRouter();
+ const stateJadwalKegiatan = useProxy(jadwalKegiatanState)
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null)
+
+ useShallowEffect(() => {
+ stateJadwalKegiatan.findUnique.load(params?.id as string)
+ }, [])
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateJadwalKegiatan.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan")
+ }
+ }
+
+ if (!stateJadwalKegiatan.findUnique.data) {
+ return (
+
+
+
+ )
+ }
+
+ const data = stateJadwalKegiatan.findUnique.data
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Jadwal Kegiatan
+
+
+
+
+ {/* Nama Kegiatan */}
+
+ Nama Kegiatan
+ {data.content || '-'}
+
+
+ {/* Informasi */}
+
+ Informasi
+ Nama
+ {data.informasijadwalkegiatan.name || '-'}
+ Tanggal
+ {data.informasijadwalkegiatan.tanggal || '-'}
+ Waktu
+ {data.informasijadwalkegiatan.waktu || '-'}
+ Lokasi
+ {data.informasijadwalkegiatan.lokasi || '-'}
+
+
+ {/* Deskripsi */}
+
+ Deskripsi
+
+
+
+ {/* Layanan */}
+
+ Layanan
+
+
+
+ {/* Syarat Ketentuan */}
+
+ Syarat Ketentuan
+
+
+
+ {/* Dokumen */}
+
+ Dokumen
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id)
+ setModalHapus(true)
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus jadwal kegiatan ini?"
+ />
+
+ );
+}
+
+export default DetailJadwalKegiatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx
new file mode 100644
index 00000000..55121d4a
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx
@@ -0,0 +1,197 @@
+'use client'
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function CreateJadwalKegiatan() {
+ const stateJadwalKegiatan = useProxy(jadwalKegiatanState);
+ const router = useRouter();
+
+ const resetForm = () => {
+ stateJadwalKegiatan.create.form = {
+ content: '',
+ informasiJadwalKegiatan: {
+ name: '',
+ tanggal: '',
+ waktu: '',
+ lokasi: '',
+ },
+ deskripsiJadwalKegiatan: {
+ deskripsi: '',
+ },
+ layananJadwalKegiatan: {
+ content: '',
+ },
+ syaratKetentuanJadwalKegiatan: {
+ content: '',
+ },
+ dokumenJadwalKegiatan: {
+ content: '',
+ }
+ };
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ await stateJadwalKegiatan.create.submit();
+
+ toast.success('Data berhasil disimpan');
+ resetForm();
+ router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Jadwal Kegiatan
+
+
+
+ {/* Form */}
+
+
+ {
+ stateJadwalKegiatan.create.form.content = e.target.value;
+ }}
+ required
+ />
+
+
+ Deskripsi Jadwal Kegiatan
+ {
+ stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi = e;
+ }}
+ />
+
+
+
+ Informasi Jadwal Kegiatan
+ {
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name = e.target.value;
+ }}
+ />
+ {
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal = e.target.value;
+ }}
+ />
+ {
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu = e.target.value;
+ }}
+ />
+ {
+ stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi = e.target.value;
+ }}
+ />
+
+
+
+ Layanan Jadwal Kegiatan
+ {
+ stateJadwalKegiatan.create.form.layananJadwalKegiatan.content = e;
+ }}
+ />
+
+
+
+ Syarat & Ketentuan
+ {
+ stateJadwalKegiatan.create.form.syaratKetentuanJadwalKegiatan.content = e;
+ }}
+ />
+
+
+
+ Dokumen Yang Perlu Dibawa
+ {
+ stateJadwalKegiatan.create.form.dokumenJadwalKegiatan.content = e;
+ }}
+ />
+
+ {/* Save Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateJadwalKegiatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx
new file mode 100644
index 00000000..2114f56f
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx
@@ -0,0 +1,195 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Pagination,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import jadwalKegiatanState from '../../../_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
+import { useState } from 'react';
+
+function JadwalKegiatan() {
+ const router = useRouter();
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Tombol Back */}
+
+ router.back()}>
+
+
+
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListJadwalKegiatan({ search }: { search: string }) {
+ const state = useProxy(jadwalKegiatanState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Jadwal Kegiatan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create')
+ }
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Nama
+ Tanggal
+ Waktu
+ Lokasi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.informasijadwalkegiatan.name}
+
+
+
+
+
+ {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString(
+ 'id-ID',
+ {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ }
+ )}
+
+
+
+
+ {item.informasijadwalkegiatan.waktu}
+
+
+
+
+
+ {item.informasijadwalkegiatan.lokasi}
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada jadwal kegiatan yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default JadwalKegiatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx
new file mode 100644
index 00000000..9ebad0e4
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+import LayoutTabs from "./_lib/layoutTabs"
+
+
+export default function Layout({children} : {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx
new file mode 100644
index 00000000..ac5e2805
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx
@@ -0,0 +1,148 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKelahiran() {
+ const editState = useProxy(persentaseKelahiranKematian.kelahiran);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ tanggal: '',
+ jenisKelamin: '',
+ alamat: '',
+ });
+
+ // Load data saat mount atau params.id berubah
+ useEffect(() => {
+ const loadKelahiran = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await editState.edit.load(id);
+ if (data) setFormData({
+ nama: data.nama || '',
+ tanggal: data.tanggal || '',
+ jenisKelamin: data.jenisKelamin || '',
+ alamat: data.alamat || ''
+ });
+ } catch (error) {
+ console.error('Error loading data kelahiran:', error);
+ toast.error('Gagal memuat data kelahiran');
+ }
+ };
+
+ loadKelahiran();
+ }, [params?.id]);
+
+ const handleChange = (key: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state hanya saat submit
+ editState.edit.form = { ...editState.edit.form, ...formData };
+ await editState.edit.update();
+ toast.success('Data kelahiran berhasil diperbarui!');
+ router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran');
+ } catch (error) {
+ console.error('Error updating data kelahiran:', error);
+ toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Data Kelahiran
+
+
+
+ {/* Form */}
+
+
+ handleChange('nama', e.target.value)}
+ label="Nama"
+ placeholder="Masukkan nama"
+ required
+ />
+ handleChange('tanggal', e.target.value)}
+ label="Tanggal"
+ placeholder="Masukkan tanggal"
+ required
+ />
+ handleChange('jenisKelamin', e.target.value)}
+ label="Jenis Kelamin"
+ placeholder="Masukkan jenis kelamin"
+ required
+ />
+ handleChange('alamat', e.target.value)}
+ label="Alamat"
+ placeholder="Masukkan alamat"
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKelahiran;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx
new file mode 100644
index 00000000..18a2e587
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx
@@ -0,0 +1,164 @@
+'use client'
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+
+
+function DetailKelahiran() {
+ const state = useProxy(persentaseKelahiranKematian.kelahiran);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push(
+ "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran"
+ );
+ }
+ };
+
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+
+ const data = state.findUnique.data;
+
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Data Kelahiran
+
+
+
+
+
+
+ Nama
+ {data.nama || '-'}
+
+
+
+
+ Tanggal
+
+ {new Date(data.tanggal).toLocaleDateString("id-ID", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ })}
+
+
+
+
+
+ Jenis Kelamin
+ {data.jenisKelamin || '-'}
+
+
+
+
+ Alamat
+ {data.alamat || '-'}
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus data ini?"
+ />
+
+ );
+}
+
+
+export default DetailKelahiran;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx
new file mode 100644
index 00000000..b8476a66
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx
@@ -0,0 +1,126 @@
+'use client';
+import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+
+
+function CreateKelahiran() {
+ const createState = useProxy(persentaseKelahiranKematian.kelahiran);
+ const router = useRouter();
+
+
+ const resetForm = () => {
+ createState.create.form = {
+ nama: '',
+ tanggal: '',
+ jenisKelamin: '',
+ alamat: '',
+ };
+ };
+
+
+ const handleSubmit = async () => {
+ await createState.create.create();
+ resetForm();
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran'
+ );
+ };
+
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Data Kelahiran
+
+
+
+
+ {/* Form */}
+
+
+ Nama}
+ placeholder="Masukkan nama"
+ defaultValue={createState.create.form.nama}
+ onChange={(e) => (createState.create.form.nama = e.target.value)}
+ required
+ />
+ Tanggal}
+ placeholder="Masukkan tanggal"
+ defaultValue={createState.create.form.tanggal}
+ onChange={(e) => (createState.create.form.tanggal = e.target.value)}
+ required
+ />
+ Jenis Kelamin}
+ placeholder="Masukkan jenis kelamin"
+ defaultValue={createState.create.form.jenisKelamin}
+ onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)}
+ required
+ />
+ Alamat}
+ placeholder="Masukkan alamat"
+ defaultValue={createState.create.form.alamat}
+ onChange={(e) => (createState.create.form.alamat = e.target.value)}
+ required
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+
+export default CreateKelahiran;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx
new file mode 100644
index 00000000..255ed68d
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx
@@ -0,0 +1,207 @@
+'use client'
+import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
+import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Paper,
+ Pagination,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+
+function Kelahiran() {
+ const router = useRouter();
+ const [search, setSearch] = useState("");
+
+
+ return (
+
+ {/* Tombol Back */}
+
+ router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian')}>
+
+
+
+
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+
+ );
+}
+
+
+function ListKelahiran({ search }: { search: string }) {
+ const statePersentase = useProxy(persentasekelahiran.kelahiran);
+ const router = useRouter();
+
+
+ const { data, page, totalPages, loading, load } = statePersentase.findMany;
+
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+
+ const filteredData = data || [];
+
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Data Kelahiran
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Nama
+ Tanggal
+ Jenis Kelamin
+ Alamat
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.nama}
+
+
+
+
+
+ {new Date(item.tanggal).toLocaleDateString('id-ID', {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ })}
+
+
+
+
+ {item.jenisKelamin}
+
+
+
+
+
+ {item.alamat}
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data kelahiran yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+
+export default Kelahiran;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx
new file mode 100644
index 00000000..9f7ada02
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx
@@ -0,0 +1,169 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKematian() {
+ const editState = useProxy(persentaseKelahiranKematian.kematian);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ nama: '',
+ tanggal: '',
+ jenisKelamin: '',
+ alamat: '',
+ penyebab: '',
+ });
+
+ // Load data saat mount
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await editState.edit.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || '',
+ tanggal: data.tanggal || '',
+ jenisKelamin: data.jenisKelamin || '',
+ alamat: data.alamat || '',
+ penyebab: data.penyebab || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading data kematian:', error);
+ toast.error('Gagal memuat data kematian');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ const handleChange = (key: keyof typeof formData, value: string) => {
+ setFormData(prev => ({ ...prev, [key]: value }));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state saat submit
+ editState.edit.form = { ...editState.edit.form, ...formData };
+ await editState.edit.update();
+ toast.success('Data kematian berhasil diperbarui!');
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian'
+ );
+ } catch (error) {
+ console.error('Error updating data kematian:', error);
+ toast.error('Terjadi kesalahan saat memperbarui data kematian');
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Data Kematian
+
+
+
+ {/* Form Card */}
+
+
+ handleChange('nama', e.target.value)}
+ required
+ />
+
+ handleChange('tanggal', e.target.value)}
+ required
+ />
+
+ handleChange('jenisKelamin', e.target.value)}
+ required
+ />
+
+ handleChange('alamat', e.target.value)}
+ required
+ />
+
+
+
+ Penyebab
+
+ handleChange('penyebab', htmlContent)}
+ />
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKematian;
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx
new file mode 100644
index 00000000..41b4fa96
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx
@@ -0,0 +1,163 @@
+'use client'
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+
+
+function DetailKematian() {
+ const state = useProxy(persentaseKelahiranKematian.kematian);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
+ const router = useRouter();
+
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran");
+ }
+ };
+
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+
+ const data = state.findUnique.data;
+
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+
+ Detail Data Kematian
+
+
+
+
+
+
+ Nama
+ {data?.nama || '-'}
+
+
+
+
+ Tanggal
+
+ {new Date(data.tanggal).toLocaleDateString("id-ID", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ })}
+
+
+
+
+
+ Jenis Kelamin
+ {data?.jenisKelamin || '-'}
+
+
+
+
+ Alamat
+ {data?.alamat || '-'}
+
+
+
+
+ Penyebab
+
+
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${data.id}/edit`
+ )}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus data ini?"
+ />
+
+ );
+}
+
+
+export default DetailKematian;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx
new file mode 100644
index 00000000..2f1c92b7
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx
@@ -0,0 +1,146 @@
+'use client';
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+
+
+
+
+
+function CreateKematian() {
+ const createState = useProxy(persentaseKelahiranKematian.kematian);
+ const router = useRouter();
+
+
+ const resetForm = () => {
+ createState.create.form = {
+ nama: '',
+ tanggal: '',
+ jenisKelamin: '',
+ alamat: '',
+ penyebab: '',
+ };
+ };
+
+
+ const handleSubmit = async () => {
+ if (!createState.create.form.nama) {
+ return toast.warn('Nama wajib diisi');
+ }
+ if (!createState.create.form.tanggal) {
+ return toast.warn('Tanggal wajib diisi');
+ }
+
+
+ await createState.create.create();
+ resetForm();
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian'
+ );
+ };
+
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Data Kematian
+
+
+
+
+ {/* Form Card */}
+
+
+ (createState.create.form.nama = e.target.value)}
+ required
+ />
+ (createState.create.form.tanggal = e.target.value)}
+ required
+ />
+ (createState.create.form.jenisKelamin = e.target.value)}
+ required
+ />
+ (createState.create.form.alamat = e.target.value)}
+ required
+ />
+
+
+ Penyebab
+
+ {
+ createState.create.form.penyebab = htmlContent;
+ }}
+ />
+
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+
+export default CreateKematian;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx
new file mode 100644
index 00000000..b9ab8ec7
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx
@@ -0,0 +1,205 @@
+'use client'
+import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
+import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconPlus, IconSearch } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+
+function Kematian() {
+ const [search, setSearch] = useState("");
+ const router = useRouter();
+
+
+ return (
+
+ {/* Tombol Back */}
+
+ router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian')}>
+
+
+
+
+
+ {/* Header dengan Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+
+ );
+}
+
+
+function ListKematian({ search }: { search: string }) {
+ const statePersentase = useProxy(persentasekelahiran.kematian);
+ const router = useRouter();
+
+
+ const { data, page, totalPages, loading, load } = statePersentase.findMany;
+
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+
+ const filteredData = data || [];
+
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+
+ return (
+
+
+
+ Daftar Data Kematian
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create'
+ )
+ }
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+
+ Nama
+ Tanggal
+ Jenis Kelamin
+ Alamat
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.nama}
+
+
+
+
+
+ {new Date(item.tanggal).toLocaleDateString('id-ID', {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ })}
+
+
+
+
+ {item.jenisKelamin}
+
+
+
+
+
+ {item.alamat}
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data kematian yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+
+export default Kematian;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx
new file mode 100644
index 00000000..97f198bd
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx
@@ -0,0 +1,277 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client'
+import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
+import colors from '@/con/colors';
+import { ActionIcon, Badge, Box, Center, Flex, Tooltip as MantineTooltip, Paper, Select, Skeleton, Stack, Table, Text, Title } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconBabyCarriage, IconGrave2 } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from 'recharts';
+import { useProxy } from 'valtio/utils';
+
+
+type TooltipPayload = {
+ name: string;
+ value: number;
+ payload: any;
+ color: string;
+ dataKey: string;
+};
+
+
+type CustomTooltipProps = TooltipProps & {
+ active?: boolean;
+ payload?: TooltipPayload[];
+ label?: string;
+};
+
+
+function PersentaseDataKelahiranKematian() {
+ return (
+
+
+
+ );
+}
+
+
+function GrafikPersentaseKelahiranKematian() {
+ const router = useRouter();
+
+
+ type DataTahunan = {
+ tahun: string;
+ totalKelahiran: number;
+ totalKematian: number;
+ data: Array<{
+ id: string;
+ bulan: string;
+ kelahiran: number;
+ kematian: number;
+ }>;
+ };
+
+
+ // ✅ Fungsi hitung tahunan + bulanan
+ const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => {
+ const dataTahunan: Record = {};
+
+
+ const namaBulan = [
+ 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
+ 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
+ ];
+
+
+ // Proses kelahiran
+ kelahiran?.forEach((item: any) => {
+ const date = new Date(item.tanggal);
+ const tahun = date.getFullYear().toString();
+ const bulanIndex = date.getMonth();
+
+
+ if (!dataTahunan[tahun]) {
+ dataTahunan[tahun] = {
+ tahun,
+ totalKelahiran: 0,
+ totalKematian: 0,
+ data: namaBulan.map((nama, idx) => ({
+ id: `${tahun}-${idx + 1}`,
+ bulan: nama,
+ kelahiran: 0,
+ kematian: 0
+ }))
+ };
+ }
+
+
+ dataTahunan[tahun].totalKelahiran += 1;
+ dataTahunan[tahun].data[bulanIndex].kelahiran += 1;
+ });
+
+
+ // Proses kematian
+ kematian?.forEach((item: any) => {
+ const date = new Date(item.tanggal);
+ const tahun = date.getFullYear().toString();
+ const bulanIndex = date.getMonth();
+
+
+ if (!dataTahunan[tahun]) {
+ dataTahunan[tahun] = {
+ tahun,
+ totalKelahiran: 0,
+ totalKematian: 0,
+ data: namaBulan.map((nama, idx) => ({
+ id: `${tahun}-${idx + 1}`,
+ bulan: nama,
+ kelahiran: 0,
+ kematian: 0
+ }))
+ };
+ }
+
+
+ dataTahunan[tahun].totalKematian += 1;
+ dataTahunan[tahun].data[bulanIndex].kematian += 1;
+ });
+
+
+ return Object.values(dataTahunan).sort((a, b) => parseInt(a.tahun) - parseInt(b.tahun));
+ };
+
+
+ const statePersentase = useProxy(persentasekelahiran);
+ const [chartData, setChartData] = useState([]);
+ const [selectedYear, setSelectedYear] = useState(null);
+
+
+ const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num);
+
+
+ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
+ if (active && payload && payload.length) {
+ return (
+
+ Tahun {label}
+ Kelahiran: {formatNumber(payload[0].value)}
+ Kematian: {formatNumber(payload[1].value)}
+
+ );
+ }
+ return null;
+ };
+
+
+ useShallowEffect(() => {
+ statePersentase.kelahiran.findMany.load(1, 1000);
+ statePersentase.kematian.findMany.load(1, 1000);
+ }, []);
+
+
+ useEffect(() => {
+ if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
+ const hasil = countByYearAndMonth(
+ statePersentase.kelahiran.findMany.data,
+ statePersentase.kematian.findMany.data
+ );
+
+
+ setChartData(hasil);
+ setSelectedYear(hasil[0]?.tahun || null);
+ }
+ }, [statePersentase.kelahiran.findMany.data, statePersentase.kematian.findMany.data]);
+
+
+ if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
+ return ;
+ }
+
+
+ const selectedYearData = chartData.find(d => d.tahun === selectedYear);
+
+
+ return (
+
+
+
+ Statistik Kelahiran & Kematian
+
+
+ router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}>
+
+
+
+
+ router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')}>
+
+
+
+
+
+
+
+ {chartData.length === 0 ? (
+
+ Belum ada data untuk ditampilkan
+
+ ) : (
+ <>
+
+ ({ value: item.tahun, label: item.tahun }))}
+ value={selectedYear}
+ onChange={(value) => setSelectedYear(value || null)}
+ size="sm"
+ radius="md"
+ />
+
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+ {selectedYearData && (
+
+
+ Rincian Tahun {selectedYear}
+ {formatNumber(selectedYearData.totalKelahiran)} kelahiran
+ {formatNumber(selectedYearData.totalKematian)} kematian
+
+
+
+
+ Bulan
+ Kelahiran
+ Kematian
+
+
+
+ {selectedYearData.data.length > 0 ? (
+ <>
+ {selectedYearData.data.map((item) => (
+
+ {item.bulan}
+ {formatNumber(item.kelahiran)}
+ {formatNumber(item.kematian)}
+
+ ))}
+
+ Total
+ {formatNumber(selectedYearData.totalKelahiran)}
+ {formatNumber(selectedYearData.totalKematian)}
+
+ >
+ ) : (
+
+ Tidak ada rincian bulanan
+
+ )}
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+
+export default PersentaseDataKelahiranKematian;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx
new file mode 100644
index 00000000..6d751dd5
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx
@@ -0,0 +1,233 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, ChangeEvent } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditInfoWabahPenyakit() {
+ const infoWabahPenyakitState = useProxy(infoWabahPenyakit);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsiSingkat: '',
+ deskripsi: '',
+ imageId: '',
+ });
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ // Helper untuk update field formData
+ const updateField = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ // Load data edit
+ useEffect(() => {
+ const loadInfoWabahPenyakit = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await infoWabahPenyakitState.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsiSingkat: data.deskripsiSingkat || '',
+ deskripsi: data.deskripsiLengkap || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data.image?.link) setPreviewImage(data.image.link);
+ }
+ } catch (error) {
+ console.error(error);
+ toast.error('Gagal memuat data info wabah penyakit');
+ }
+ };
+
+ loadInfoWabahPenyakit();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ let uploadedImageId = formData.imageId;
+
+ // Upload file kalau ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ uploadedImageId = uploaded.id;
+ }
+
+ // Update global state
+ infoWabahPenyakitState.edit.form = {
+ ...infoWabahPenyakitState.edit.form,
+ name: formData.name,
+ deskripsiSingkat: formData.deskripsiSingkat,
+ deskripsiLengkap: formData.deskripsi,
+ imageId: uploadedImageId,
+ };
+
+ await infoWabahPenyakitState.edit.update();
+ toast.success('Info wabah penyakit berhasil diperbarui!');
+ router.push('/admin/kesehatan/info-wabah-penyakit');
+ } catch (error) {
+ console.error(error);
+ toast.error('Terjadi kesalahan saat memperbarui info wabah penyakit');
+ }
+ };
+
+ const handleDrop = (files: File[]) => {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Info Wabah Penyakit
+
+
+
+ {/* Form */}
+
+
+ ) => updateField('name', e.target.value)}
+ label="Judul"
+ placeholder="Masukkan judul"
+ required
+ />
+
+
+
+ Deskripsi Singkat
+
+ updateField('deskripsiSingkat', val)}
+ />
+
+
+
+
+ Deskripsi
+
+ updateField('deskripsi', val)}
+ />
+
+
+
+
+ Gambar
+
+ toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditInfoWabahPenyakit;
diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx
new file mode 100644
index 00000000..03fda087
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx
@@ -0,0 +1,164 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Paper,
+ Stack,
+ Text,
+ Skeleton,
+ Tooltip,
+ Group,
+ Image,
+} from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function DetailInfoWabahPenyakit() {
+ const state = useProxy(infoWabahPenyakit);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push('/admin/kesehatan/info-wabah-penyakit');
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Info Wabah Penyakit
+
+
+
+
+
+ Judul
+ {data.name || '-'}
+
+
+
+ Deskripsi Singkat
+
+
+
+
+ Deskripsi Lengkap
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ -
+ )}
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(
+ `/admin/kesehatan/info-wabah-penyakit/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus info wabah penyakit ini?"
+ />
+
+ );
+}
+
+export default DetailInfoWabahPenyakit;
diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx
new file mode 100644
index 00000000..06a0d39c
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx
@@ -0,0 +1,197 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
+import { Dropzone } from '@mantine/dropzone';
+
+function CreateInfoWabahPenyakit() {
+ const router = useRouter();
+ const infoWabahPenyakitState = useProxy(infoWabahPenyakit)
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const resetForm = () => {
+ infoWabahPenyakitState.create.form = {
+ name: "",
+ deskripsiSingkat: "",
+ deskripsiLengkap: "",
+ imageId: "",
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn("Pilih file gambar terlebih dahulu");
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+
+ infoWabahPenyakitState.create.form.imageId = uploaded.id;
+ await infoWabahPenyakitState.create.create();
+
+ resetForm();
+ router.push("/admin/kesehatan/info-wabah-penyakit")
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Info Wabah Penyakit
+
+
+
+ {/* Form */}
+
+
+ {
+ infoWabahPenyakitState.create.form.name = val.target.value;
+ }}
+ label={Judul }
+ placeholder="Masukkan judul"
+ required
+ />
+
+
+ Deskripsi Singkat
+ {
+ infoWabahPenyakitState.create.form.deskripsiSingkat = val;
+ }}
+ />
+
+
+
+ Deskripsi
+ {
+ infoWabahPenyakitState.create.form.deskripsiLengkap = val;
+ }}
+ />
+
+
+
+ Gambar
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateInfoWabahPenyakit;
diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx
new file mode 100644
index 00000000..e4215822
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx
@@ -0,0 +1,164 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
+
+function InfoWabahPenyakit() {
+ const [search, setSearch] = useState("");
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListInfoWabahPenyakit({ search }: { search: string }) {
+ const infoWabahPenyakitState = useProxy(infoWabahPenyakit)
+ const router = useRouter()
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = infoWabahPenyakitState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Info Wabah Penyakit
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/info-wabah-penyakit/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Judul
+ Deskripsi Singkat
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+
+ Tidak ada data info wabah penyakit yang cocok
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10)
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ )
+}
+
+export default InfoWabahPenyakit;
diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx
new file mode 100644
index 00000000..8ede9489
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx
@@ -0,0 +1,215 @@
+'use client';
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKontakDarurat() {
+ const kontakDaruratState = useProxy(kontakDarurat);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ whatsapp: '',
+ });
+ const [loading, setLoading] = useState(true);
+
+ // Load data sekali saat mount
+ useEffect(() => {
+ const loadKontakDarurat = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await kontakDaruratState.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ whatsapp: data.whatsapp || '',
+ });
+ if (data?.image?.link) setPreviewImage(data.image.link);
+ }
+ } catch (error) {
+ console.error("Error loading kontak darurat:", error);
+ toast.error("Gagal memuat data kontak darurat");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadKontakDarurat();
+ }, [params?.id, kontakDaruratState.edit]);
+
+ const handleSubmit = async () => {
+ try {
+ let imageId = formData.imageId;
+
+ // Upload file baru jika ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error("Gagal upload gambar");
+ imageId = uploaded.id;
+ }
+
+ // Update global state sekaligus submit
+ kontakDaruratState.edit.form = {
+ ...kontakDaruratState.edit.form,
+ ...formData,
+ imageId,
+ };
+
+ await kontakDaruratState.edit.update();
+ toast.success("Kontak darurat berhasil diperbarui!");
+ router.push("/admin/kesehatan/kontak-darurat");
+ } catch (error) {
+ console.error("Error updating kontak darurat:", error);
+ toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
+ }
+ };
+
+ if (loading) return Loading... ;
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Kontak Darurat
+
+
+
+
+
+ {/* Controlled Input */}
+ setFormData(prev => ({ ...prev, name: e.target.value }))}
+ label="Judul"
+ placeholder="Masukkan judul"
+ required
+ />
+
+ setFormData(prev => ({ ...prev, whatsapp: e.target.value }))}
+ label="Whatsapp"
+ placeholder="Masukkan whatsapp"
+ required
+ />
+
+
+ Deskripsi
+ setFormData(prev => ({ ...prev, deskripsi: val }))}
+ />
+
+
+
+ Gambar
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+ Maksimal 5MB dan format gambar
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKontakDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx
new file mode 100644
index 00000000..628338cb
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx
@@ -0,0 +1,153 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
+
+function DetailKontakDarurat() {
+ const state = useProxy(kontakDarurat);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/kontak-darurat");
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Kontak Darurat
+
+
+
+
+
+ Judul
+ {data.name || '-'}
+
+
+
+ Whatsapp
+ {data.whatsapp || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ -
+ )}
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={state.delete.loading}
+ >
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/kontak-darurat/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus kontak darurat ini?"
+ />
+
+ );
+}
+
+export default DetailKontakDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx
new file mode 100644
index 00000000..65a84277
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx
@@ -0,0 +1,221 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import {
+ IconArrowBack,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
+import { Dropzone } from '@mantine/dropzone';
+
+function CreateKontakDarurat() {
+ const router = useRouter();
+ const kontakDaruratState = useProxy(kontakDarurat);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const resetForm = () => {
+ kontakDaruratState.create.form = {
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ whatsapp: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ kontakDaruratState.create.form.imageId = uploaded.id;
+
+ await kontakDaruratState.create.create();
+
+ resetForm();
+ router.push('/admin/kesehatan/kontak-darurat');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Kontak Darurat
+
+
+
+ {/* Form */}
+
+
+ {
+ kontakDaruratState.create.form.name = val.target.value;
+ }}
+ label={Judul }
+ placeholder="Masukkan judul"
+ required
+ />
+
+ {
+ kontakDaruratState.create.form.whatsapp = val.target.value;
+ }}
+ label={Whatsapp }
+ placeholder="Masukkan whatsapp"
+ required
+ />
+
+
+ Deskripsi
+ {
+ kontakDaruratState.create.form.deskripsi = val;
+ }}
+ />
+
+
+
+ Gambar
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKontakDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx
new file mode 100644
index 00000000..2c341cf0
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx
@@ -0,0 +1,160 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 kontakDarurat from '../../_state/kesehatan/kontak-darurat/kontakDarurat';
+
+function KontakDarurat() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListKontakDarurat({ search }: { search: string }) {
+ const kontakDaruratState = useProxy(kontakDarurat)
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = kontakDaruratState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+
+ Daftar Kontak Darurat
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/kontak-darurat/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Judul
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/kontak-darurat/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data kontak darurat yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ )
+}
+
+export default KontakDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx
new file mode 100644
index 00000000..3d1d0da1
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx
@@ -0,0 +1,224 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPenangananDarurat() {
+ const penangananDaruratState = useProxy(penangananDarurat)
+ const router = useRouter();
+ const params = useParams()
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ });
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ // Load data satu kali saat component mount
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await penangananDaruratState.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data.image?.link) {
+ setPreviewImage(data.image.link);
+ }
+ }
+ } catch (err) {
+ console.error('Error loading penanganan darurat:', err);
+ toast.error('Gagal memuat data penanganan darurat');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ loadData();
+ }, [params?.id]);
+
+ const handleChange = (key: keyof typeof formData, value: string) => {
+ setFormData(prev => ({ ...prev, [key]: value }));
+ };
+
+ const handleDrop = (files: File[]) => {
+ const selected = files[0];
+ if (!selected) return;
+
+ setFile(selected);
+ setPreviewImage(URL.createObjectURL(selected));
+ };
+
+ const handleSubmit = async () => {
+ try {
+ let imageId = formData.imageId;
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) return toast.error("Gagal upload gambar");
+
+ imageId = uploaded.id;
+ }
+
+ // update global state sekali saat submit
+ penangananDaruratState.edit.form = {
+ ...penangananDaruratState.edit.form,
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ imageId,
+ };
+
+ await penangananDaruratState.edit.update();
+ toast.success("Penanganan darurat berhasil diperbarui!");
+ router.push("/admin/kesehatan/penanganan-darurat");
+ } catch (err) {
+ console.error("Error updating penanganan darurat:", err);
+ toast.error("Gagal memperbarui data penanganan darurat");
+ }
+ };
+
+ if (loading) return Loading... ;
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Penanganan Darurat
+
+
+
+ {/* Form */}
+
+
+ handleChange('name', e.target.value)}
+ label="Judul"
+ placeholder="Masukkan judul"
+ required
+ />
+
+
+ Deskripsi
+ handleChange('deskripsi', val)}
+ />
+
+
+
+ Gambar
+ toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPenangananDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx
new file mode 100644
index 00000000..36967755
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx
@@ -0,0 +1,144 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useRouter, useParams } from 'next/navigation';
+import React, { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
+
+function DetailPenangananDarurat() {
+ const state = useProxy(penangananDarurat);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/penanganan-darurat");
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol Back */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Wrapper Detail */}
+
+
+
+ Detail Penanganan Darurat
+
+
+
+
+
+ Nama Penanganan Darurat
+ {data.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/penanganan-darurat/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
+ />
+
+ );
+}
+
+export default DetailPenangananDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx
new file mode 100644
index 00000000..f28e0a92
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx
@@ -0,0 +1,215 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import {
+ IconArrowBack,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
+
+function CreatePenangananDarurat() {
+ const router = useRouter();
+ const penangananDaruratState = useProxy(penangananDarurat);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const resetForm = () => {
+ penangananDaruratState.create.form = {
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ penangananDaruratState.create.form.imageId = uploaded.id;
+
+ await penangananDaruratState.create.create();
+
+ resetForm();
+ router.push('/admin/kesehatan/penanganan-darurat');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+
+ Tambah Penanganan Darurat
+
+
+
+ {/* Form */}
+
+
+ {/* Judul */}
+ Judul}
+ placeholder="Masukkan judul"
+ defaultValue={penangananDaruratState.create.form.name}
+ onChange={(val) => {
+ penangananDaruratState.create.form.name = val.target.value;
+ }}
+ required
+ />
+
+ {/* Deskripsi */}
+
+ Deskripsi
+ {
+ penangananDaruratState.create.form.deskripsi = val;
+ }}
+ />
+
+
+ {/* Upload Gambar */}
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+ {/* Button Simpan */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePenangananDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx
new file mode 100644
index 00000000..7d397e4d
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx
@@ -0,0 +1,165 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat';
+
+function PenangananDarurat() {
+ const [search, setSearch] = useState("");
+ return (
+
+ {/* Header Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListPenangananDarurat({ search }: { search: string }) {
+ const state = useProxy(penangananDarurat);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Judul + Tombol Tambah */}
+
+ Daftar Penanganan Darurat
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/penanganan-darurat/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Judul
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/penanganan-darurat/${item.id}`)
+ }
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data penanganan darurat
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default PenangananDarurat;
diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx
new file mode 100644
index 00000000..1a47aefa
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx
@@ -0,0 +1,234 @@
+/* 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';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditPosyandu() {
+ const statePosyandu = useProxy(posyandustate);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ nomor: '',
+ deskripsi: '',
+ imageId: '',
+ jadwalPelayanan: '',
+ });
+
+ // Load data posyandu
+ useEffect(() => {
+ const loadPosyandu = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await statePosyandu.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ nomor: data.nomor || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ jadwalPelayanan: data.jadwalPelayanan || '',
+ });
+ if (data?.image?.link) setPreviewImage(data.image.link);
+ }
+ } catch (error) {
+ console.error('Error loading posyandu:', error);
+ toast.error('Gagal memuat data posyandu');
+ }
+ };
+ loadPosyandu();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state hanya saat submit
+ const updatedForm = { ...statePosyandu.edit.form, ...formData };
+
+ // Upload file jika ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ updatedForm.imageId = uploaded.id;
+ }
+
+ statePosyandu.edit.form = updatedForm;
+ await statePosyandu.edit.update();
+
+ toast.success('Posyandu berhasil diperbarui!');
+ router.push('/admin/kesehatan/posyandu');
+ } catch (error) {
+ console.error('Error updating posyandu:', error);
+ toast.error('Terjadi kesalahan saat memperbarui posyandu');
+ }
+ };
+
+ return (
+
+ {/* Tombol Back */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Posyandu
+
+
+
+ {/* Card utama */}
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar Posyandu
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar wajib
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Input Form */}
+ setFormData({ ...formData, name: e.target.value })}
+ required
+ />
+
+ setFormData({ ...formData, nomor: e.target.value })}
+ required
+ />
+
+
+
+ Deskripsi Posyandu
+
+ setFormData({ ...formData, deskripsi: htmlContent })}
+ />
+
+
+
+
+ Jadwal Pelayanan
+
+
+ setFormData({ ...formData, jadwalPelayanan: htmlContent })
+ }
+ />
+
+
+ {/* Tombol Submit */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPosyandu;
diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx
new file mode 100644
index 00000000..75af2397
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx
@@ -0,0 +1,178 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
+import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import posyanduState from '../../../_state/kesehatan/posyandu/posyandu';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+
+function DetailPosyandu() {
+ const statePosyandu = useProxy(posyanduState);
+ const params = useParams();
+ const router = useRouter();
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+
+ useShallowEffect(() => {
+ statePosyandu.findUnique.load(params?.id as string);
+ }, []);
+
+
+ const handleHapus = () => {
+ if (selectedId) {
+ statePosyandu.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/posyandu");
+ }
+ };
+
+
+ if (!statePosyandu.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+
+ const data = statePosyandu.findUnique.data;
+
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+ {/* Card utama */}
+
+
+
+ Detail Posyandu
+
+
+
+
+
+
+ Nama Posyandu
+ {data.name || '-'}
+
+
+
+
+ Nomor Posyandu
+ {data.nomor || '-'}
+
+
+
+
+ Deskripsi
+
+
+
+
+
+ Jadwal Pelayanan
+
+
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+ {/* Aksi */}
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/posyandu/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+
+ {/* Modal konfirmasi hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus posyandu ini?"
+ />
+
+ );
+}
+
+
+export default DetailPosyandu;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx
new file mode 100644
index 00000000..9542eefe
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx
@@ -0,0 +1,215 @@
+'use client';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
+
+
+function CreatePosyandu() {
+ const statePosyandu = useProxy(posyandustate);
+ const router = useRouter();
+ const [file, setFile] = useState(null);
+ const [previewImage, setPreviewImage] = useState(null);
+
+
+ const resetForm = () => {
+ statePosyandu.create.form = {
+ name: '',
+ nomor: '',
+ deskripsi: '',
+ imageId: '',
+ jadwalPelayanan: '',
+ };
+ setFile(null);
+ setPreviewImage(null);
+ };
+
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+
+ // Upload gambar dulu
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+
+ statePosyandu.create.form.imageId = uploaded.id;
+
+
+ await statePosyandu.create.create();
+
+
+ resetForm();
+ router.push('/admin/kesehatan/posyandu');
+ };
+
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Posyandu
+
+
+
+
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar Posyandu
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file (maks 5MB)
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+ {/* Input Form */}
+ (statePosyandu.create.form.name = e.target.value)}
+ required
+ />
+ (statePosyandu.create.form.nomor = e.target.value)}
+ required
+ />
+
+
+ Deskripsi Posyandu
+
+ {
+ statePosyandu.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+
+ Jadwal Pelayanan
+
+ {
+ statePosyandu.create.form.jadwalPelayanan = htmlContent;
+ }}
+ />
+
+
+
+ {/* Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+
+export default CreatePosyandu;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx
new file mode 100644
index 00000000..b8528787
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx
@@ -0,0 +1,177 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImac, 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';
+
+
+function Posyandu() {
+ const [search, setSearch] = useState("");
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+
+function ListPosyandu({ search }: { search: string }) {
+ const statePosyandu = useProxy(posyandustate)
+ const router = useRouter();
+
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = statePosyandu.findMany;
+
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+
+ const filteredData = data || [];
+
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+
+ return (
+
+
+
+ Daftar Posyandu
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/posyandu/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Posyandu
+ Nomor Posyandu
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ {item.nomor || '-'}
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/posyandu/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data posyandu yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+
+export default Posyandu;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx
new file mode 100644
index 00000000..1f0dcbec
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx
@@ -0,0 +1,226 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditProgramKesehatan() {
+ const programKesehatanState = useProxy(programKesehatan);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsiSingkat: '',
+ deskripsi: '',
+ imageId: '',
+ });
+
+ // Load data awal
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await programKesehatanState.edit.load(id);
+ if (!data) return;
+
+ setFormData({
+ name: data.name || '',
+ deskripsiSingkat: data.deskripsiSingkat || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ });
+
+ if (data?.image?.link) setPreviewImage(data.image.link);
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memuat data program kesehatan');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ // Handler input controlled
+ const handleChange = (key: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [key]: value }));
+ };
+
+ // Submit form
+ const handleSubmit = async () => {
+ try {
+ const updatedForm = { ...programKesehatanState.edit.form, ...formData };
+
+ // Upload file kalau ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+
+ updatedForm.imageId = uploaded.id;
+ }
+
+ programKesehatanState.edit.form = updatedForm;
+ await programKesehatanState.edit.update();
+ toast.success('Program kesehatan berhasil diperbarui!');
+ router.push('/admin/kesehatan/program-kesehatan');
+ } catch (err) {
+ console.error(err);
+ toast.error('Terjadi kesalahan saat memperbarui program kesehatan');
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Program Kesehatan
+
+
+
+
+
+ {[
+ { label: 'Judul', key: 'name', placeholder: 'Masukkan judul' },
+ ].map((field) => (
+ handleChange(field.key as keyof typeof formData, e.target.value)}
+ required
+ />
+ ))}
+
+
+
+ Deskripsi Singkat
+
+ handleChange('deskripsiSingkat', val)}
+ />
+
+
+
+
+ Deskripsi
+
+ handleChange('deskripsi', val)}
+ />
+
+
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (!selectedFile) return;
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditProgramKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx
new file mode 100644
index 00000000..5a36bca6
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx
@@ -0,0 +1,137 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function DetailProgramKesehatan() {
+ const state = useProxy(programKesehatan);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const router = useRouter();
+ const params = useParams();
+
+ useShallowEffect(() => {
+ state.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ state.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/program-kesehatan");
+ }
+ };
+
+ if (!state.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = state.findUnique.data;
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Program Kesehatan
+
+
+
+
+
+ Judul
+ {data?.name || '-'}
+
+
+
+ Deskripsi Singkat
+
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data?.image?.link ? (
+
+ ) : (
+ -
+ )}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/kesehatan/program-kesehatan/${data?.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus program kesehatan ini?"
+ />
+
+ );
+}
+
+export default DetailProgramKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx
new file mode 100644
index 00000000..35d642df
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx
@@ -0,0 +1,204 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
+import { Dropzone } from '@mantine/dropzone';
+
+function CreateProgramKesehatan() {
+ const router = useRouter();
+ const programKesehatanState = useProxy(programKesehatan);
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+
+ const resetForm = () => {
+ programKesehatanState.create.form = {
+ name: "",
+ deskripsiSingkat: "",
+ deskripsi: "",
+ imageId: "",
+ };
+ setPreviewImage(null);
+ setFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!programKesehatanState.create.form.name) {
+ return toast.warn("Judul wajib diisi");
+ }
+ if (!programKesehatanState.create.form.deskripsiSingkat) {
+ return toast.warn("Deskripsi singkat wajib diisi");
+ }
+ if (!file) {
+ return toast.warn("Pilih file gambar terlebih dahulu");
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+
+ programKesehatanState.create.form.imageId = uploaded.id;
+ await programKesehatanState.create.create();
+
+ resetForm();
+ router.push("/admin/kesehatan/program-kesehatan");
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Program Kesehatan
+
+
+
+ {/* Form Card */}
+
+
+ {
+ programKesehatanState.create.form.name = val.target.value;
+ }}
+ label="Judul"
+ placeholder="Masukkan judul"
+ required
+ />
+
+
+
+ Deskripsi Singkat
+
+ {
+ programKesehatanState.create.form.deskripsiSingkat = val;
+ }}
+ />
+
+
+
+
+ Deskripsi
+
+ {
+ programKesehatanState.create.form.deskripsi = val;
+ }}
+ />
+
+
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateProgramKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx
new file mode 100644
index 00000000..55ce2a2b
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx
@@ -0,0 +1,160 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
+
+function ProgramKesehatan() {
+ const [search, setSearch] = useState("");
+ return (
+
+ {/* Header dengan Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListProgramKesehatan({ search }: { search: string }) {
+ const stateProgram = useProxy(programKesehatan);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = stateProgram.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header List + Tombol Tambah */}
+
+ Daftar Program Kesehatan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/program-kesehatan/create')}
+ >
+ Tambah Baru
+
+
+
+
+ {/* Tabel */}
+
+
+
+
+ Judul
+ Deskripsi Singkat
+ Deskripsi
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/program-kesehatan/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada program kesehatan yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default ProgramKesehatan;
diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx
new file mode 100644
index 00000000..9dd41e85
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx
@@ -0,0 +1,320 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { ChangeEvent, useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+interface PuskesmasFormBase {
+ name: string;
+ alamat: string;
+ jam: {
+ workDays: string;
+ weekDays: string;
+ holiday: string;
+ };
+ kontak: {
+ kontakPuskesmas: string;
+ email: string;
+ facebook: string;
+ kontakUGD: string;
+ };
+ imageId: string;
+}
+
+interface PuskesmasFormData extends PuskesmasFormBase {
+ image?: {
+ link: string;
+ };
+}
+
+function EditPuskesmas() {
+ const statePuskesmas = useProxy(puskesmasState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: '',
+ alamat: '',
+ jam: { workDays: '', weekDays: '', holiday: '' },
+ kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' },
+ imageId: '',
+ });
+
+ useEffect(() => {
+ const loadPuskesmas = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ await statePuskesmas.edit.load(id);
+ const { form } = statePuskesmas.edit;
+ if (form) {
+ setFormData({
+ name: form.name,
+ alamat: form.alamat,
+ jam: {
+ workDays: form.jam.workDays,
+ weekDays: form.jam.weekDays,
+ holiday: form.jam.holiday,
+ },
+ kontak: {
+ kontakPuskesmas: form.kontak.kontakPuskesmas,
+ email: form.kontak.email,
+ facebook: form.kontak.facebook,
+ kontakUGD: form.kontak.kontakUGD,
+ },
+ imageId: form.imageId,
+ });
+
+ const formWithImage = form as PuskesmasFormData;
+ if (formWithImage.image?.link) {
+ setPreviewImage(formWithImage.image.link);
+ }
+ }
+ } catch (error) {
+ console.error("Error loading puskesmas:", error);
+ toast.error("Gagal memuat data puskesmas");
+ }
+ };
+ loadPuskesmas();
+ }, [params?.id]);
+
+ const handleSubmit = async () => {
+ try {
+ statePuskesmas.edit.form = {
+ ...statePuskesmas.edit.form,
+ name: formData.name,
+ alamat: formData.alamat,
+ jam: { ...formData.jam },
+ kontak: { ...formData.kontak },
+ imageId: formData.imageId,
+ };
+
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ toast.error("Gagal upload gambar");
+ return;
+ }
+
+ statePuskesmas.edit.form.imageId = uploaded.id;
+ }
+
+ const success = await statePuskesmas.edit.submit();
+ if (success) {
+ toast.success("Puskesmas berhasil diperbarui!");
+ router.push("/admin/kesehatan/puskesmas");
+ }
+ } catch (error) {
+ console.error("Error updating puskesmas:", error);
+ toast.error(error instanceof Error ? error.message : "Gagal memperbarui data puskesmas");
+ }
+ };
+
+ const handleInputChange = (e: ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleNestedChange = (section: 'jam' | 'kontak', field: string, value: string) => {
+ setFormData(prev => ({
+ ...prev,
+ [section]: { ...prev[section], [field]: value }
+ }));
+ };
+
+ return (
+
+ {/* Header dengan tombol back */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Puskesmas
+
+
+
+ {/* Card Form */}
+
+
+
+
+
+
+ handleNestedChange('jam', 'workDays', e.target.value)}
+ required
+ />
+
+ handleNestedChange('jam', 'weekDays', e.target.value)}
+ required
+ />
+
+ handleNestedChange('jam', 'holiday', e.target.value)}
+ required
+ />
+
+ handleNestedChange('kontak', 'kontakPuskesmas', e.target.value)}
+ />
+
+ handleNestedChange('kontak', 'email', e.target.value)}
+ />
+
+ handleNestedChange('kontak', 'facebook', e.target.value)}
+ />
+
+ handleNestedChange('kontak', 'kontakUGD', e.target.value)}
+ />
+
+ {/* Upload Gambar */}
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditPuskesmas;
diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx
new file mode 100644
index 00000000..2384ae36
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx
@@ -0,0 +1,154 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import React, { useState } from 'react';
+import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+
+function DetailPuskesmas() {
+ const params = useParams();
+ const router = useRouter();
+ const statePuskesmas = useProxy(puskesmasState);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+
+ useShallowEffect(() => {
+ statePuskesmas.findUnique.load(params?.id as string);
+ }, []);
+
+ const handleHapus = () => {
+ if (selectedId) {
+ statePuskesmas.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/kesehatan/puskesmas");
+ }
+ };
+
+ if (!statePuskesmas.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = statePuskesmas.findUnique.data;
+
+ return (
+
+ {/* Tombol kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Puskesmas
+
+
+
+
+
+ Nama Puskesmas
+ {data?.name || '-'}
+
+
+
+ Alamat
+ {data?.alamat || '-'}
+
+
+
+ Jam Operasional
+ Senin - Jumat
+ {data?.jam?.workDays || '-'} - {data?.jam?.weekDays || '-'}
+ Sabtu - Minggu / Hari Libur
+ {data?.jam?.holiday || '-'}
+
+
+
+ Gambar
+ {data?.image?.link ? (
+
+ ) : (
+ -
+ )}
+
+
+
+ Kontak
+ Kontak Puskesmas
+ {data?.kontak?.kontakPuskesmas || '-'}
+ Email
+ {data?.kontak?.email || '-'}
+ Facebook
+ {data?.kontak?.facebook || '-'}
+ Kontak UGD
+ {data?.kontak?.kontakUGD || '-'}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+ router.push(`/admin/kesehatan/puskesmas/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus data ini?"
+ />
+
+ );
+}
+
+export default DetailPuskesmas;
diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx
new file mode 100644
index 00000000..871b1235
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx
@@ -0,0 +1,237 @@
+'use client'
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
+
+function CreatePuskesmas() {
+ const statePuskesmas = useProxy(puskesmasState);
+ const router = useRouter();
+ const [file, setFile] = useState(null);
+ const [previewImage, setPreviewImage] = useState(null);
+
+ const resetForm = () => {
+ statePuskesmas.create.form = {
+ name: '',
+ alamat: '',
+ jam: {
+ workDays: '',
+ weekDays: '',
+ holiday: '',
+ },
+ kontak: {
+ kontakPuskesmas: '',
+ email: '',
+ facebook: '',
+ kontakUGD: '',
+ },
+ imageId: '',
+ image: undefined,
+ };
+ setFile(null);
+ setPreviewImage(null);
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ statePuskesmas.create.form.imageId = uploaded.id;
+ await statePuskesmas.create.submit();
+
+ toast.success('Data berhasil disimpan');
+ resetForm();
+ router.push('/admin/kesehatan/puskesmas');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Data Puskesmas
+
+
+
+ {/* Form Card */}
+
+
+ (statePuskesmas.create.form.name = e.target.value)}
+ required
+ />
+ (statePuskesmas.create.form.alamat = e.target.value)}
+ required
+ />
+ (statePuskesmas.create.form.jam.workDays = e.target.value)}
+ />
+ (statePuskesmas.create.form.jam.weekDays = e.target.value)}
+ />
+ (statePuskesmas.create.form.jam.holiday = e.target.value)}
+ />
+
+
+ (statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value)
+ }
+ />
+ (statePuskesmas.create.form.kontak.email = e.target.value)}
+ />
+ (statePuskesmas.create.form.kontak.facebook = e.target.value)}
+ />
+ (statePuskesmas.create.form.kontak.kontakUGD = e.target.value)}
+ />
+
+
+
+ Gambar
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid.')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drag gambar ke sini atau klik untuk pilih file
+
+
+ Maksimal 5MB dan harus format gambar
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Action Button */}
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreatePuskesmas;
diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx
new file mode 100644
index 00000000..44e4b74c
--- /dev/null
+++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx
@@ -0,0 +1,166 @@
+'use client'
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+ Tooltip
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 puskesmasState from '../../_state/kesehatan/puskesmas/puskesmas';
+
+function Puskesmas() {
+ const [search, setSearch] = useState("");
+
+ return (
+
+ {/* Header dengan Search */}
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+ );
+}
+
+function ListPuskesmas({ search }: { search: string }) {
+ const statePuskesmas = useProxy(puskesmasState);
+ const router = useRouter();
+
+ const { data, page, totalPages, loading, load } = statePuskesmas.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Puskesmas
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/kesehatan/puskesmas/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Puskesmas
+ Alamat
+ Kontak
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ {item.alamat}
+
+
+
+
+
+
+ {item.kontak.kontakPuskesmas}
+
+
+
+
+ router.push(`/admin/kesehatan/puskesmas/${item.id}`)}
+ >
+
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data puskesmas yang cocok
+
+
+
+ )}
+
+
+
+
+
+ {/* Pagination */}
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+
+ );
+}
+
+export default Puskesmas;
diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
new file mode 100644
index 00000000..4327ad4e
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx
@@ -0,0 +1,231 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditAPBDes() {
+ const apbdesState = useProxy(apbdes);
+ const router = useRouter();
+ const params = useParams();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ jumlah: '',
+ imageId: '',
+ fileId: ''
+ });
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [previewDoc, setPreviewDoc] = useState(null);
+ const [imageFile, setImageFile] = useState(null);
+ const [docFile, setDocFile] = useState(null);
+
+ // Load data on mount
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await apbdesState.edit.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ jumlah: data.jumlah || '',
+ imageId: data.imageId || '',
+ fileId: data.fileId || ''
+ });
+ setPreviewImage(data.image?.link || null);
+ setPreviewDoc(data.file?.link || null);
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memuat data APBDes');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ // Generic Dropzone handler
+ const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => {
+ const file = files[0];
+ if (!file) return;
+
+ if (fileType === 'image') {
+ setImageFile(file);
+ setPreviewImage(URL.createObjectURL(file));
+ } else {
+ setDocFile(file);
+ setPreviewDoc(URL.createObjectURL(file));
+ }
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // Update global state with local form data first
+ apbdesState.edit.form = { ...apbdesState.edit.form, ...formData };
+
+ // Helper function for uploading file
+ const uploadFile = async (file: File | null) => {
+ if (!file) return null;
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) throw new Error('Upload gagal');
+ return uploaded.id;
+ };
+
+ // Upload files if selected
+ const uploadedImageId = await uploadFile(imageFile);
+ const uploadedDocId = await uploadFile(docFile);
+
+ if (uploadedImageId) apbdesState.edit.form.imageId = uploadedImageId;
+ if (uploadedDocId) apbdesState.edit.form.fileId = uploadedDocId;
+
+ await apbdesState.edit.update();
+ toast.success('APBDes berhasil diperbarui!');
+ router.push('/admin/landing-page/apbdes');
+ } catch (err) {
+ console.error(err);
+ toast.error('Terjadi kesalahan saat memperbarui APBDes');
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit APBDes
+
+
+
+
+
+ {/* Controlled Inputs */}
+ setFormData({ ...formData, name: e.target.value })}
+ required
+ />
+
+ setFormData({ ...formData, jumlah: e.target.value })}
+ required
+ />
+
+ {/* Image Dropzone */}
+
+ Gambar APBDes
+ toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+ Maksimal 5MB, format gambar wajib
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Document Dropzone */}
+
+ Dokumen APBDes
+ toast.error('File tidak valid, gunakan format dokumen')}
+ maxSize={10 * 1024 ** 2}
+ accept={{
+ 'application/pdf': ['.pdf'],
+ 'application/msword': ['.doc'],
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
+ 'application/vnd.ms-excel': ['.xls'],
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
+ }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+ Seret dokumen atau klik untuk memilih file
+ Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX
+
+
+
+ {previewDoc && (
+
+ Dokumen terpilih: {docFile?.name || 'Dokumen'}
+ } size="sm">
+ Lihat Dokumen
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditAPBDes;
diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx
new file mode 100644
index 00000000..ce342456
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx
@@ -0,0 +1,163 @@
+'use client'
+import { useProxy } from 'valtio/utils';
+
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconFile, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+import colors from '@/con/colors';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import apbdes from '../../../_state/landing-page/apbdes';
+
+function DetailAPBDes() {
+ const apbdesState = useProxy(apbdes)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const params = useParams()
+ const router = useRouter()
+
+ useShallowEffect(() => {
+ apbdesState.findUnique.load(params?.id as string)
+ }, [])
+
+
+ const handleHapus = () => {
+ if (selectedId) {
+ apbdesState.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/landing-page/apbdes")
+ }
+ }
+
+ if (!apbdesState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = apbdesState.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail APBDes
+
+
+
+
+
+ Nama APBDes
+ {data.name || '-'}
+
+
+
+ Jumlah Anggaran
+ Rp. {data.jumlah || '-'}
+
+
+
+ Gambar
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+
+ Dokumen
+ {data.file?.link ? (
+ }
+ size="sm"
+ mt="xs"
+ >
+ Lihat Dokumen
+
+ ) : (
+ Tidak ada dokumen
+ )}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ disabled={apbdesState.delete.loading}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+ router.push(`/admin/landing-page/apbdes/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus APBDes ini?"
+ />
+
+ );
+}
+
+export default DetailAPBDes;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
new file mode 100644
index 00000000..1925e0a5
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx
@@ -0,0 +1,254 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import apbdes from '../../../_state/landing-page/apbdes';
+
+
+function CreateAPBDes() {
+ const router = useRouter();
+ const stateAPBDes = useProxy(apbdes)
+ const [previewImage, setPreviewImage] = useState(null);
+ const [previewDoc, setPreviewDoc] = useState(null);
+ const [imageFile, setImageFile] = useState(null);
+ const [docFile, setDocFile] = useState(null);
+
+
+
+ useEffect(() => {
+ stateAPBDes.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ stateAPBDes.create.form = {
+ name: "",
+ jumlah: "",
+ imageId: "",
+ fileId: "",
+ };
+ setImageFile(null);
+ setDocFile(null);
+ setPreviewImage(null);
+ };
+ const handleSubmit = async () => {
+ if (!imageFile || !docFile) {
+ return toast.warn("Pilih gambar dan dokumen terlebih dahulu");
+ }
+
+ try {
+ const [uploadImageRes, uploadDocRes] = await Promise.all([
+ ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }),
+ ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }),
+ ]);
+
+ const imageId = uploadImageRes?.data?.data?.id;
+ const fileId = uploadDocRes?.data?.data?.id;
+
+ if (!imageId || !fileId) {
+ return toast.error("Gagal mengupload file");
+ }
+
+ stateAPBDes.create.form.imageId = imageId;
+ stateAPBDes.create.form.fileId = fileId;
+
+ await stateAPBDes.create.create();
+
+ toast.success("Berhasil menambahkan APBDes");
+ resetForm();
+ router.push("/admin/landing-page/apbdes");
+ } catch (error) {
+ console.error("Gagal submit:", error);
+ toast.error("Gagal menyimpan data");
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah APBDes
+
+
+
+
+
+ {/* Gambar APBDes */}
+
+
+ Gambar APBDes
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setImageFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB (format: JPEG, JPG, PNG, GIF, WEBP, SVG)
+
+
+
+
+
+ {previewImage && (
+
+
+
+ )}
+
+
+ {/* Dokumen APBDes */}
+
+
+ Dokumen APBDes
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setDocFile(selectedFile);
+ setPreviewDoc(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format PDF, DOC, atau DOCX')}
+ maxSize={5 * 1024 ** 2}
+ accept={{
+ 'application/pdf': ['.pdf'],
+ 'application/msword': ['.doc'],
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
+ }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret dokumen atau klik untuk memilih file
+
+
+ Maksimal 5MB (format: PDF, DOC, DOCX)
+
+
+
+
+
+ {previewDoc && (
+
+
+ Pratinjau Dokumen
+
+
+
+ )}
+
+
+ {/* Form Input */}
+ (stateAPBDes.create.form.name = e.target.value)}
+ required
+ />
+ (stateAPBDes.create.form.jumlah = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateAPBDes;
diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx
new file mode 100644
index 00000000..a2062280
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/apbdes/page.tsx
@@ -0,0 +1,156 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconDeviceImacCog, IconFile, 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 apbdes from '../../_state/landing-page/apbdes';
+
+
+function APBDes() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListAPBDes({ search }: { search: string }) {
+ const listState = useProxy(apbdes)
+ const router = useRouter();
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = listState.findMany
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar APBDes
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/landing-page/apbdes/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama APBDes
+ Jumlah
+ Dokumen
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+ {item.name}
+
+
+
+ Rp. {item.jumlah}
+
+
+
+ {item.file?.link ? (
+ }
+ size="sm"
+ >
+ Lihat Dokumen
+
+ ) : (
+ Tidak ada dokumen
+ )}
+
+
+ }
+ onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
+ fullWidth
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data APBDes yang cocok
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ color="blue"
+ radius="md"
+ />
+
+
+ )
+}
+
+export default APBDes;
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx
new file mode 100644
index 00000000..b0fc00e7
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/_lib/layouTabs.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+"use client";
+
+import { usePathname, useRouter } from "next/navigation";
+import React, { useEffect, useState } from "react";
+import {
+ Stack,
+ Tabs,
+ TabsList,
+ TabsPanel,
+ TabsTab,
+ Title,
+ Tooltip,
+ ScrollArea,
+} from "@mantine/core";
+import { IconList, IconCategory } from "@tabler/icons-react";
+import colors from "@/con/colors";
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "List Desa Anti Korupsi",
+ value: "listDesaAntiKorupsi",
+ href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi",
+ icon: ,
+ tooltip: "Kelola daftar program desa anti korupsi",
+ },
+ {
+ label: "Kategori Desa Anti Korupsi",
+ value: "kategoriDesaAntiKorupsi",
+ href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi",
+ icon: ,
+ tooltip: "Kelola kategori desa anti korupsi",
+ },
+ ];
+
+ const currentTab = tabs.find((tab) => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(
+ 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 (
+
+
+ Desa Anti Korupsi
+
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx
new file mode 100644
index 00000000..48af5204
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx
@@ -0,0 +1,126 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, useCallback } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+export default function EditKategoriDesaAntiKorupsi() {
+ const router = useRouter();
+ const params = useParams();
+ const id = params?.id as string;
+ const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
+
+ // state lokal untuk form
+ const [formData, setFormData] = useState({ name: '' });
+ const [isLoading, setIsLoading] = useState(false);
+
+ // load data kategori saat mount atau id berubah
+ useEffect(() => {
+ if (!id) return;
+
+ const loadKategori = async () => {
+ setIsLoading(true);
+ try {
+ const data = await stateKategori.edit.load(id);
+ if (data) {
+ stateKategori.edit.id = id;
+ setFormData({ name: data.name || '' });
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memuat data kategori desa anti korupsi');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadKategori();
+ }, [id]);
+
+ // handler controlled input
+ const handleChange = useCallback(
+ (field: keyof typeof formData, value: string) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ // submit form
+ const handleSubmit = useCallback(async () => {
+ if (!formData.name.trim()) return toast.error('Nama kategori tidak boleh kosong');
+
+ try {
+ setIsLoading(true);
+
+ // update global state hanya saat submit
+ stateKategori.edit.form = { name: formData.name.trim() };
+ if (!stateKategori.edit.id) stateKategori.edit.id = id;
+
+ await stateKategori.edit.update();
+ toast.success('Kategori berhasil diperbarui');
+ router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi');
+ } catch (err) {
+ console.error(err);
+ toast.error(err instanceof Error ? err.message : 'Gagal memperbarui kategori');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [formData.name, id, router, stateKategori.edit]);
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Kategori Desa Anti Korupsi
+
+
+
+
+
+ handleChange('name', e.currentTarget.value)}
+ required
+ disabled={isLoading}
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx
new file mode 100644
index 00000000..15e3645c
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx
@@ -0,0 +1,83 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useProxy } from 'valtio/utils';
+import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
+
+export default function CreateKategoriDesaAntiKorupsi() {
+ const router = useRouter();
+ const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
+
+ useEffect(() => {
+ stateKategori.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ stateKategori.create.form = {
+ name: "",
+ };
+ };
+
+ const handleSubmit = async () => {
+ if (!stateKategori.create.form.name) {
+ return alert('Nama kategori harus diisi');
+ }
+
+ await stateKategori.create.create();
+ resetForm();
+ router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Kategori Desa Anti Korupsi
+
+
+
+
+
+ (stateKategori.create.form.name = e.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/page.tsx
new file mode 100644
index 00000000..19a507a2
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/page.tsx
@@ -0,0 +1,167 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
+
+
+function KategoriDesaAntiKorupsi() {
+ const [search, setSearch] = useState("")
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKategoriKegiatan({ search }: { search: string }) {
+ const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const router = useRouter()
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateKategori.findMany;
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateKategori.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ }
+ }
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Daftar Kategori Kegiatan
+
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Kategori
+ Edit
+ Hapus
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.name}
+
+
+
+
+ router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
+ >
+
+
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ada data kategori yang ditemukan
+
+
+
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ total={totalPages}
+ mt="md"
+ mb="md"
+ color="blue"
+ radius="md"
+ />
+
+ {/* Modal Konfirmasi Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text='Apakah anda yakin ingin menghapus kategori kegiatan ini?'
+ />
+
+ );
+}
+
+export default KategoriDesaAntiKorupsi
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/layout.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/layout.tsx
new file mode 100644
index 00000000..97e4e3a9
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import LayoutTabs from './_lib/layouTabs';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx
new file mode 100644
index 00000000..d60eb719
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx
@@ -0,0 +1,247 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+
+import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState, ChangeEvent } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import { Dropzone } from '@mantine/dropzone';
+import { useShallowEffect } from '@mantine/hooks';
+
+import colors from '@/con/colors';
+import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
+import ApiFetch from '@/lib/api-fetch';
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+
+interface FormDesaAntiKorupsi {
+ name: string;
+ deskripsi: string;
+ kategoriId: string;
+ fileId: string;
+}
+
+export default function EditDesaAntiKorupsi() {
+ const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi);
+ const params = useParams();
+ const router = useRouter();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ kategoriId: '',
+ fileId: '',
+ });
+
+ const [previewFile, setPreviewFile] = useState(null);
+ const [file, setFile] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Load kategori
+ useShallowEffect(() => {
+ korupsiState.kategoriDesaAntiKorupsi.findMany.load();
+ }, []);
+
+ // Load data existing
+ useEffect(() => {
+ const loadDesaAntiKorupsi = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await desaAntiKorupsiState.edit.load(id);
+ if (!data) return;
+
+ desaAntiKorupsiState.edit.id = id;
+ desaAntiKorupsiState.edit.form = { ...data };
+
+ setFormData({
+ name: data.name,
+ deskripsi: data.deskripsi,
+ kategoriId: data.kategoriId,
+ fileId: data.fileId,
+ });
+
+ if (data.file?.link) setPreviewFile(data.file.link);
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memuat data Desa Anti Korupsi');
+ }
+ };
+
+ loadDesaAntiKorupsi();
+ }, [params?.id]);
+
+ // Generic handler input
+ const handleInputChange = (key: keyof FormDesaAntiKorupsi) => (e: ChangeEvent | string | null) => {
+ const value = typeof e === 'string' ? e : e?.target?.value ?? '';
+ setFormData((prev) => ({ ...prev, [key]: value || '' }));
+ };
+
+ // Special handler for Select component
+ const handleSelectChange = (key: keyof FormDesaAntiKorupsi) => (value: string | null) => {
+ setFormData((prev) => ({ ...prev, [key]: value || '' }));
+ };
+
+ const handleDrop = (files: File[]) => {
+ if (!files.length) return;
+ const selectedFile = files[0];
+ setFile(selectedFile);
+ setPreviewFile(URL.createObjectURL(selectedFile));
+ };
+
+ const handleSubmit = async () => {
+ if (!formData.name) return toast.warn('Masukkan judul dokumen');
+ if (!formData.kategoriId) return toast.warn('Pilih kategori dokumen');
+
+ setIsLoading(true);
+ try {
+ // Update global state
+ desaAntiKorupsiState.edit.form = { ...desaAntiKorupsiState.edit.form, ...formData };
+
+ // Upload file jika ada
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) throw new Error('Gagal mengunggah dokumen');
+
+ desaAntiKorupsiState.edit.form.fileId = uploaded.id;
+ }
+
+ await desaAntiKorupsiState.edit.update();
+ toast.success('Data berhasil diperbarui');
+ router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
+ } catch (err) {
+ console.error(err);
+ toast.error('Terjadi kesalahan saat memperbarui data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Desa Anti Korupsi
+
+
+
+
+
+
+
+
+
+ Deskripsi
+
+
+
+
+ ({
+ value: v.id,
+ label: v.name,
+ })) || []}
+ required
+ searchable
+ clearable
+ />
+
+
+
+ Dokumen
+
+ toast.error('File tidak valid, gunakan format dokumen')}
+ maxSize={5 * 1024 ** 2}
+ accept={{
+ 'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
+ }}
+ radius="md"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret dokumen ke sini atau klik untuk memilih file
+
+
+ Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
+
+
+
+
+
+ {previewFile && (
+
+
+ Pratinjau Dokumen
+
+
+
+
+
+ )}
+
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/page.tsx
new file mode 100644
index 00000000..919eb39d
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/page.tsx
@@ -0,0 +1,157 @@
+'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+
+
+export default function DetailKegiatanDesa() {
+ const detailState = useProxy(korupsiState.desaAntikorupsi)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const params = useParams()
+ const router = useRouter()
+
+ useShallowEffect(() => {
+ detailState.findUnique.load(params?.id as string)
+ }, [])
+
+
+ const handleHapus = () => {
+ if (selectedId) {
+ detailState.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
+ }
+ }
+
+ if (!detailState.findUnique.data) {
+ return (
+
+
+
+ );
+ }
+
+ const data = detailState.findUnique.data;
+
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Desa Anti Korupsi
+
+
+
+
+
+ Judul
+ {data.name || '-'}
+
+
+
+ Kategori
+ {data.kategori?.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Dokumen
+ {data.file?.link ? (
+
+
+
+ ) : (
+ Tidak ada dokumen tersedia
+ )}
+
+
+
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={detailState.delete.loading}
+ >
+
+
+
+
+
+ router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
+
+
+
+
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah Anda yakin ingin menghapus data Desa Anti Korupsi ini?"
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx
new file mode 100644
index 00000000..1756842b
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create/page.tsx
@@ -0,0 +1,218 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
+import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
+import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
+import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Select,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
+import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+
+export default function CreateDesaAntiKorupsi() {
+ const router = useRouter();
+ const stateKorupsi = useProxy(korupsiState.desaAntikorupsi);
+ const [previewFile, setPreviewFile] = useState(null);
+ const [file, setFile] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ stateKorupsi.findMany.load();
+ korupsiState.kategoriDesaAntiKorupsi.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ stateKorupsi.create.form = {
+ name: '',
+ deskripsi: '',
+ kategoriId: '',
+ fileId: '',
+ };
+ setFile(null);
+ setPreviewFile(null);
+ };
+
+ const handleSubmit = async () => {
+ if (!file) {
+ return toast.warn('Pilih file dokumen terlebih dahulu');
+ }
+ if (!stateKorupsi.create.form.name) {
+ return toast.warn('Masukkan judul dokumen');
+ }
+ if (!stateKorupsi.create.form.kategoriId) {
+ return toast.warn('Pilih kategori dokumen');
+ }
+
+ setIsLoading(true);
+ try {
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ throw new Error('Gagal mengunggah dokumen');
+ }
+
+ stateKorupsi.create.form.fileId = uploaded.id;
+ await stateKorupsi.create.create();
+
+ toast.success('Data berhasil disimpan');
+ resetForm();
+ router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
+ } catch (error) {
+ console.error('Error:', error);
+ toast.error('Terjadi kesalahan saat menyimpan data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Dokumen Desa Anti Korupsi
+
+
+
+
+
+
+
+ Dokumen
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewFile(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
+ maxSize={5 * 1024 ** 2}
+ accept={{
+ 'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
+ }}
+ radius="md"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret dokumen ke sini atau klik untuk memilih file
+
+
+ Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
+
+
+
+
+
+ {previewFile && (
+
+
+
+ )}
+
+
+ (stateKorupsi.create.form.name = e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi
+
+ (stateKorupsi.create.form.deskripsi = val)}
+ />
+
+
+ (stateKorupsi.create.form.kategoriId = val || '')}
+ data={
+ korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
+ value: v.id,
+ label: v.name,
+ })) || []
+ }
+ required
+ searchable
+ clearable
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/page.tsx
new file mode 100644
index 00000000..097722fd
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/page.tsx
@@ -0,0 +1,157 @@
+'use client'
+
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+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 korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
+
+function DesaAntiKorupsi() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListDesaAntiKorupsi({ search }: { search: string }) {
+ const router = useRouter();
+ const listState = useProxy(korupsiState.desaAntikorupsi);
+ const { data, page, totalPages, loading, load } = listState.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10, search);
+ }, [page, search]);
+
+ const filteredData = data || [];
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+ Data Program Desa Anti Korupsi
+
+ Belum ada data program yang tersedia
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Daftar Program Desa Anti Korupsi
+
+ } color="blue" variant="light"
+ onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
+ >
+ Tambah Baru
+
+
+
+
+
+
+
+ Nama Program
+ Kategori
+ Aksi
+
+
+
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+
+
+ {item.name || '-'}
+
+
+
+
+
+ {item.kategori?.name || '-'}
+
+
+
+
+ }
+ onClick={() =>
+ router.push(
+ `/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`
+ )
+ }
+ >
+ Detail
+
+
+
+ ))
+ ) : (
+
+
+
+ Tidak ditemukan data dengan kata kunci pencarian
+
+
+
+ )}
+
+
+
+
+
+
+ {
+ load(newPage, 10, search);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ size="md"
+ radius="md"
+ mt="md"
+ />
+
+
+
+ );
+}
+
+export default DesaAntiKorupsi;
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/_lib/layoutTab.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/_lib/layoutTab.tsx
new file mode 100644
index 00000000..687c7c48
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/_lib/layoutTab.tsx
@@ -0,0 +1,98 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import colors from '@/con/colors';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { IconChartBar, IconUsers } from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+
+function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const tabs = [
+ {
+ label: "Grafik Kepuasan",
+ description: "Lihat visualisasi grafik kepuasan masyarakat",
+ value: "grafik",
+ href: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat",
+ icon:
+ },
+ {
+ label: "Responden",
+ description: "Kelola dan tinjau data responden",
+ value: "responden",
+ href: "/admin/landing-page/indeks-kepuasan-masyarakat/responden",
+ icon:
+ },
+ ];
+
+ const curentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(curentTab?.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 (
+
+
+ Indeks Kepuasan Masyarakat
+
+
+ {/* ✅ Scroll horizontal wrapper */}
+
+
+ {tabs.map((e, i) => (
+
+
+ {e.label}
+
+
+ ))}
+
+
+ {tabs.map((e, i) => (
+
+ <>>
+
+ ))}
+
+ {children}
+
+ );
+}
+
+export default LayoutTabsKepuasan;
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat/page.tsx
new file mode 100644
index 00000000..28e5d820
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat/page.tsx
@@ -0,0 +1,240 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+'use client';
+import colors from '@/con/colors';
+import { PieChart, BarChart } from '@mantine/charts';
+import {
+ Box,
+ Center,
+ Flex,
+ Paper,
+ SimpleGrid,
+ Skeleton,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
+
+interface ChartDataItem {
+ name: string;
+ value: number;
+ color: string;
+}
+
+function Page() {
+ const state = useProxy(indeksKepuasanState.responden);
+ const { data, loading } = state.findMany;
+ const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState([]);
+ const [donutDataRating, setDonutDataRating] = useState([]);
+ const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState([]);
+ const [barChartData, setBarChartData] = useState>([]);
+
+ useShallowEffect(() => {
+ if (!data && !loading) {
+ state.findMany.load();
+ return;
+ }
+ if (data) {
+ const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
+ const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
+
+ const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
+ const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
+ const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
+ const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
+
+ const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
+ const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
+ const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
+
+ setDonutDataJenisKelamin([
+ { name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
+ { name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
+ ]);
+
+ setDonutDataRating([
+ { name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
+ { name: 'Baik', value: totalBaik, color: '#10A85AFF' },
+ { name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
+ { name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
+ ]);
+
+ setDonutDataKelompokUmur([
+ { name: 'Muda', value: totalMuda, color: colors['blue-button'] },
+ { name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
+ { name: 'Lansia', value: totalLansia, color: '#FFA500' },
+ ]);
+
+ const monthYearMap = new Map();
+
+ data.forEach((item: any) => {
+ const dateValue = item.tanggal || item.createdAt;
+ if (!dateValue) return;
+
+ const parsedDate = new Date(dateValue);
+ if (isNaN(parsedDate.getTime())) return;
+
+ const month = parsedDate.getMonth() + 1;
+ const year = parsedDate.getFullYear();
+ const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
+
+ monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
+ });
+
+ const barData = Array.from(monthYearMap.entries())
+ .map(([key, count]) => {
+ const [year, month] = key.split('-');
+ const monthName = new Date(Number(year), Number(month) - 1, 1)
+ .toLocaleString('id-ID', { month: 'long' });
+ return {
+ month: `${monthName} ${year}`,
+ count,
+ sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10),
+ };
+ })
+ .sort((a, b) => a.sortKey - b.sortKey)
+ .map(({ month, count }) => ({ month, count }));
+
+ setBarChartData(barData);
+ }
+ }, [data]);
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+ Belum ada data yang tersedia
+
+
+ );
+ }
+
+ return (
+
+
+ Tren Jumlah Responden
+
+
+
+
+
+
+
+
+ Distribusi Jenis Kelamin
+ {donutDataJenisKelamin.every(item => item.value === 0) ? (
+
+ Tidak ada data
+
+ ) : (
+
+
+
+
+
+ {donutDataJenisKelamin.map((entry) => (
+
+
+ {entry.name}: {entry.value}
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ Distribusi Penilaian
+ {donutDataRating.every(item => item.value === 0) ? (
+
+ Tidak ada data
+
+ ) : (
+
+
+
+
+
+ {donutDataRating.map((entry) => (
+
+
+ {entry.name}: {entry.value}
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ Distribusi Kelompok Umur
+ {donutDataKelompokUmur.every(item => item.value === 0) ? (
+
+ Tidak ada data
+
+ ) : (
+
+
+
+
+
+ {donutDataKelompokUmur.map((entry) => (
+
+
+ {entry.name}: {entry.value}
+
+ ))}
+
+
+ )}
+
+
+
+
+ );
+}
+
+export default Page;
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/layout.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/layout.tsx
new file mode 100644
index 00000000..0b93e229
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/layout.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import LayoutTabsKepuasan from './_lib/layoutTab';
+
+function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default Layout;
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx
new file mode 100644
index 00000000..98785c88
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/edit/page.tsx
@@ -0,0 +1,211 @@
+'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
+import React, { useEffect, useState, useCallback } from 'react';
+import { useRouter, useParams } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+ Stack,
+ Title,
+ TextInput,
+ Text,
+ Select,
+ Tooltip,
+} from '@mantine/core';
+import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react';
+import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
+import { toast } from 'react-toastify';
+
+interface FormResponden {
+ name: string;
+ tanggal: string;
+ jenisKelaminId: string;
+ ratingId: string;
+ kelompokUmurId: string;
+}
+
+function EditResponden() {
+ const router = useRouter();
+ const params = useParams() as { id: string };
+ const state = useProxy(indeksKepuasanState.responden);
+ const id = params.id;
+
+ const [formData, setFormData] = useState({
+ name: '',
+ tanggal: '',
+ jenisKelaminId: '',
+ ratingId: '',
+ kelompokUmurId: '',
+ });
+
+ // Helper untuk load pilihan select
+ const loadSelectOptions = useCallback(() => {
+ indeksKepuasanState.jenisKelaminResponden.findMany.load();
+ indeksKepuasanState.pilihanRatingResponden.findMany.load();
+ indeksKepuasanState.kelompokUmurResponden.findMany.load();
+ }, []);
+
+ // Load data responden
+ const loadResponden = useCallback(async () => {
+ if (!id) return;
+
+ try {
+ const data = await state.update.load(id);
+ if (!data) return;
+
+ setFormData({
+ name: data.name,
+ tanggal: data.tanggal,
+ jenisKelaminId: data.jenisKelaminId,
+ ratingId: data.ratingId,
+ kelompokUmurId: data.kelompokUmurId,
+ });
+ } catch (error) {
+ console.error("Error loading responden:", error);
+ toast.error("Gagal memuat data responden");
+ }
+ }, [id]);
+
+ useEffect(() => {
+ loadSelectOptions();
+ loadResponden();
+ }, [loadSelectOptions, loadResponden]);
+
+ const handleSubmit = async () => {
+ state.update.id = id;
+ state.update.form = { ...formData }; // sinkronisasi manual
+ await state.update.submit();
+ router.push('/admin/landing-page/indeks-kepuasan-masyarakat/responden');
+ };
+
+ // Reusable Select component
+ const ControlledSelect = ({
+ label,
+ value,
+ onChange,
+ options,
+ error,
+ placeholder = 'Pilih',
+ loading = false,
+ }: {
+ label: string;
+ value: string;
+ onChange: (val: string) => void;
+ options: { value: string; label: string }[];
+ error?: string;
+ placeholder?: string;
+ loading?: boolean;
+ }) => {
+ return (
+ {label}}
+ value={value}
+ onChange={(val) => onChange(val || '')}
+ data={options}
+ placeholder={placeholder}
+ disabled={loading}
+ clearable
+ searchable
+ required
+ radius="md"
+ error={error}
+ />
+ );
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Responden
+
+
+
+
+
+ setFormData({ ...formData, name: e.currentTarget.value })}
+ radius="md"
+ required
+ />
+ setFormData({ ...formData, tanggal: e.currentTarget.value })}
+ radius="md"
+ required
+ />
+
+ setFormData({ ...formData, jenisKelaminId: val })}
+ options={(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
+ .filter(Boolean)
+ .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
+ loading={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
+ error={!formData.jenisKelaminId ? 'Pilih jenis kelamin' : undefined}
+ />
+
+ setFormData({ ...formData, ratingId: val })}
+ options={(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
+ .filter(Boolean)
+ .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
+ loading={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
+ error={!formData.ratingId ? 'Pilih rating' : undefined}
+ />
+
+ setFormData({ ...formData, kelompokUmurId: val })}
+ options={(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
+ .filter(Boolean)
+ .map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
+ loading={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
+ error={!formData.kelompokUmurId ? 'Pilih kelompok umur' : undefined}
+ />
+
+
+ router.back()}>
+ Batal
+
+ }
+ onClick={handleSubmit}
+ loading={state.update.loading}
+ color={colors['blue-button']}
+ >
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditResponden;
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/page.tsx
new file mode 100644
index 00000000..7bb65dc6
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/[id]/page.tsx
@@ -0,0 +1,138 @@
+'use client'
+
+import { ModalKonfirmasiHapus } from "@/app/admin/(dashboard)/_com/modalKonfirmasiHapus"
+import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"
+import colors from "@/con/colors"
+import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from "@mantine/core"
+import { useShallowEffect } from "@mantine/hooks"
+import { IconArrowBack, IconEdit, IconTrash } from "@tabler/icons-react"
+import { useParams, useRouter } from "next/navigation"
+import { useState } from "react"
+import { useProxy } from "valtio/utils"
+
+export default function DetailResponden() {
+ const [modalHapus, setModalHapus] = useState(false)
+ const stateDetail = useProxy(indeksKepuasanState.responden)
+ const router = useRouter()
+ const params = useParams()
+ const [selectedId, setSelectedId] = useState(null)
+
+ useShallowEffect(() => {
+ stateDetail.findUnique.load(params?.id as string)
+ }, [params?.id])
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateDetail.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ router.push("/admin/landing-page/indeks-kepuasan-masyarakat/responden")
+ }
+ }
+
+ if (!stateDetail.findUnique.data) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+
+
+
+ Detail Responden
+
+
+
+
+
+ Nama Responden
+ {stateDetail.findUnique.data?.name || '-'}
+
+
+ Tanggal
+ {
+ stateDetail.findUnique.data?.tanggal
+ ? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
+ : '-'
+ }
+
+
+ Jenis Kelamin
+ {stateDetail.findUnique.data?.jenisKelamin?.name || '-'}
+
+
+ Rating
+ {stateDetail.findUnique.data?.rating?.name || '-'}
+
+
+ Kelompok Umur
+ {stateDetail.findUnique.data?.kelompokUmur?.name || '-'}
+
+
+
+
+ {
+ if (stateDetail.findUnique.data) {
+ setSelectedId(stateDetail.findUnique.data.id);
+ setModalHapus(true);
+ }
+ }}
+ disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
+ leftSection={ }
+ >
+ Hapus
+
+
+
+ {
+ if (stateDetail.findUnique.data) {
+ router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${stateDetail.findUnique.data.id}/edit`);
+ }
+ }}
+ disabled={!stateDetail.findUnique.data}
+ leftSection={ }
+ >
+ Edit
+
+
+
+
+
+
+
+
+ {/* Modal Hapus */}
+ setModalHapus(false)}
+ onConfirm={handleHapus}
+ text="Apakah anda yakin ingin menghapus responden ini?"
+ />
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx
new file mode 100644
index 00000000..7056c869
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/create/page.tsx
@@ -0,0 +1,148 @@
+'use client'
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import React from 'react';
+import { useRouter } from 'next/navigation';
+import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
+import { useProxy } from 'valtio/utils';
+import { useState } from 'react';
+import colors from '@/con/colors';
+import { Box, Button, Paper, Stack, Title, TextInput, Select, Text } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
+import { useShallowEffect } from '@mantine/hooks';
+
+function RespondenCreate() {
+ const router = useRouter();
+ const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden)
+ const [donutData, setDonutData] = useState([]);
+
+ const resetForm = () => {
+ stategrafikBerdasarkanResponden.create.form = {
+ ...stategrafikBerdasarkanResponden.create.form,
+ name: "",
+ tanggal: "",
+ jenisKelaminId: "",
+ ratingId: "",
+ kelompokUmurId: "",
+ }
+ }
+
+ useShallowEffect(() => {
+ indeksKepuasanState.jenisKelaminResponden.findMany.load()
+ indeksKepuasanState.pilihanRatingResponden.findMany.load()
+ indeksKepuasanState.kelompokUmurResponden.findMany.load()
+ })
+
+ const handleSubmit = async () => {
+ try {
+ const id = await stategrafikBerdasarkanResponden.create.create();
+ if (typeof id !== 'undefined') {
+ const idStr = String(id);
+ await stategrafikBerdasarkanResponden.findUnique.load(idStr);
+ if (stategrafikBerdasarkanResponden.findUnique.data) {
+ setDonutData([stategrafikBerdasarkanResponden.findUnique.data]);
+ }
+ }
+ resetForm();
+ router.push("/admin/ppid/ikm-desa-darmasaba/responden");
+ } catch (error) {
+ console.error('Error submitting form:', error);
+ }
+ }
+ return (
+
+
+ router.back()}>
+
+
+
+
+
+ Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik
+ {
+ stategrafikBerdasarkanResponden.create.form.name = val.currentTarget.value;
+ }}
+ />
+ {
+ stategrafikBerdasarkanResponden.create.form.tanggal = val.currentTarget.value;
+ }}
+ />
+ {
+ stategrafikBerdasarkanResponden.create.form.jenisKelaminId = val ?? "";
+ }}
+ data={
+ (indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
+ .filter(Boolean) // Hapus null, undefined, dll
+ .map((item) => ({
+ value: item.id,
+ label: item.name || 'Tanpa Nama',
+ }))
+ }
+ disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
+ />
+ {
+ stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
+ }}
+ data={
+ (indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
+ .filter(Boolean) // Hapus null, undefined, dll
+ .map((item) => ({
+ value: item.id,
+ label: item.name || 'Tanpa Nama',
+ }))
+ }
+ disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
+ />
+ {
+ stategrafikBerdasarkanResponden.create.form.kelompokUmurId = val ?? "";
+ }}
+ data={
+ (indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
+ .filter(Boolean) // Hapus null, undefined, dll
+ .map((item) => ({
+ value: item.id,
+ label: item.name || 'Tanpa Nama',
+ }))
+ }
+ disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
+ />
+
+ Submit
+
+
+
+
+ );
+}
+
+export default RespondenCreate;
diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx
new file mode 100644
index 00000000..eadaee3d
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/responden/page.tsx
@@ -0,0 +1,178 @@
+'use client';
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useProxy } from 'valtio/utils';
+import { useShallowEffect } from '@mantine/hooks';
+import {
+ Box,
+ Button,
+ Center,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+} from '@mantine/core';
+import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
+import HeaderSearch from '../../../_com/header';
+import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
+
+function Responden() {
+ const [search, setSearch] = useState('');
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+interface ListRespondenProps {
+ search: string;
+}
+
+function ListResponden({ search }: ListRespondenProps) {
+ const state = useProxy(indeksKepuasanState.responden);
+ const router = useRouter();
+ const { data, page, totalPages, loading, load } = state.findMany;
+
+ useShallowEffect(() => {
+ load(page, 10);
+ }, [page]);
+
+ const filteredData = (data || []).filter((item) => {
+ const keyword = search.toLowerCase();
+ return item.name.toLowerCase().includes(keyword);
+ });
+
+ if (loading || !data) {
+ return (
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+ Data Responden
+
+ Belum ada data responden yang tersedia
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Daftar Responden
+
+
+
+
+
+ No
+ Nama
+ Tanggal
+ Jenis Kelamin
+ Aksi
+
+
+
+ {filteredData.length === 0 ? (
+
+
+
+ Tidak ditemukan data dengan kata kunci pencarian
+
+
+
+ ) : (
+ filteredData.map((item, index) => (
+
+ {index + 1}
+ {item.name}
+
+
+ {item.tanggal
+ ? new Date(item.tanggal).toLocaleDateString('id-ID', {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ })
+ : '-'}
+
+
+
+
+ {item.jenisKelamin.name}
+
+
+
+ }
+ onClick={() =>
+ router.push(
+ `/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
+ )
+ }
+ >
+ Detail
+
+
+
+ ))
+ )}
+
+
+
+
+
+ {
+ load(newPage, 10);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }}
+ size="md"
+ radius="md"
+ mt="md"
+ />
+
+
+
+ );
+}
+
+export default Responden;
diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/_lib/layoutTabs.tsx
new file mode 100644
index 00000000..53da55c1
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/_lib/layoutTabs.tsx
@@ -0,0 +1,107 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { IconCategory, IconListDetails } from '@tabler/icons-react';
+import colors from '@/con/colors';
+
+function LayoutTabs({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const tabs = [
+ {
+ label: "Daftar Prestasi",
+ value: "listPrestasiDesa",
+ href: "/admin/landing-page/prestasi-desa/list-prestasi-desa",
+ icon: ,
+ tooltip: "Kelola daftar prestasi desa",
+ },
+ {
+ label: "Kategori Prestasi",
+ value: "kategoriPrestasiDesa",
+ href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa",
+ icon: ,
+ tooltip: "Kelola kategori prestasi desa",
+ },
+ ];
+
+ const currentTab = tabs.find(tab => tab.href === pathname);
+ const [activeTab, setActiveTab] = useState(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 (
+
+
+ Prestasi Desa
+
+
+
+
+ {tabs.map((tab, i) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {tabs.map((tab, i) => (
+
+ {children}
+
+ ))}
+
+
+ );
+}
+
+export default LayoutTabs;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx
new file mode 100644
index 00000000..40f7b19c
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/[id]/page.tsx
@@ -0,0 +1,119 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+function EditKategoriPrestasi() {
+ const router = useRouter();
+ const params = useParams();
+ const id = params?.id as string;
+ const stateKategori = useProxy(prestasiState.kategoriPrestasi);
+
+ const [formData, setFormData] = useState({ name: '' });
+ const [loading, setLoading] = useState(false);
+
+ // Load data kategori prestasi saat component mount
+ useEffect(() => {
+ if (!id) return;
+
+ const loadKategori = async () => {
+ setLoading(true);
+ try {
+ const data = await stateKategori.edit.load(id);
+ if (data) {
+ stateKategori.edit.id = id;
+ setFormData({ name: data.name || '' });
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memuat data kategori prestasi desa');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadKategori();
+ }, [id]);
+
+ // Submit: update global state hanya saat submit
+ const handleSubmit = async () => {
+ if (!formData.name.trim()) {
+ toast.error('Nama kategori prestasi tidak boleh kosong');
+ return;
+ }
+
+ try {
+ stateKategori.edit.form = { name: formData.name.trim() };
+ stateKategori.edit.id ||= id; // fallback jika id belum ada
+
+ const success = await stateKategori.edit.update();
+ if (success) {
+ toast.success('Kategori prestasi berhasil diperbarui');
+ router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa');
+ }
+ } catch (err) {
+ console.error(err);
+ toast.error('Gagal memperbarui kategori prestasi desa');
+ }
+ };
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Edit Kategori Prestasi
+
+
+
+
+
+ setFormData({ ...formData, name: e.target.value })}
+ required
+ disabled={loading}
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default EditKategoriPrestasi;
diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx
new file mode 100644
index 00000000..c03a0ba6
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/create/page.tsx
@@ -0,0 +1,82 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
+import colors from '@/con/colors';
+import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useProxy } from 'valtio/utils';
+
+
+function CreateKategoriPrestasi() {
+ const router = useRouter();
+ const stateKategori = useProxy(prestasiState.kategoriPrestasi)
+
+ useEffect(() => {
+ stateKategori.findMany.load();
+ }, []);
+
+ const resetForm = () => {
+ stateKategori.create.form = {
+ name: "",
+ };
+ }
+
+ const handleSubmit = async () => {
+ await stateKategori.create.create();
+ resetForm();
+ router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa")
+ }
+
+ return (
+
+
+
+ router.back()} p="xs" radius="md">
+
+
+
+
+ Tambah Kategori Prestasi
+
+
+
+
+
+ (stateKategori.create.form.name = val.target.value)}
+ required
+ />
+
+
+
+ Simpan
+
+
+
+
+
+ );
+}
+
+export default CreateKategoriPrestasi;
diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/page.tsx
new file mode 100644
index 00000000..7fe4cb43
--- /dev/null
+++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/kategori-prestasi-desa/page.tsx
@@ -0,0 +1,168 @@
+'use client'
+import colors from '@/con/colors';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import prestasiState from '../../../_state/landing-page/prestasi-desa';
+
+
+function KategoriPrestasiDesa() {
+ const [search, setSearch] = useState("")
+ return (
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+ );
+}
+
+function ListKategoriPrestasi({ search }: { search: string }) {
+ const stateKategori = useProxy(prestasiState.kategoriPrestasi)
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
+ const router = useRouter()
+
+ const handleHapus = () => {
+ if (selectedId) {
+ stateKategori.delete.byId(selectedId)
+ setModalHapus(false)
+ setSelectedId(null)
+ }
+ }
+
+ const {
+ data,
+ page,
+ totalPages,
+ loading,
+ load,
+ } = stateKategori.findMany
+
+ useShallowEffect(() => {
+ load(page, 10, search)
+ }, [page, search])
+
+ const filteredData = data || []
+
+ if (loading || !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ List Kategori Prestasi
+
+ } color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
+ Tambah Baru
+
+
+
+
+
+
+
+
+ Nama Kategori
+ Edit
+ Delete
+
+
+
+ {filteredData.length === 0 ? (
+
+
+