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 :

Alur Pelayanan :

" + }, + { + "id" : "cmdxycqz40014vniidftrixvf", + "name" : "Surat Keterangan Yatim Piatu", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdwx3wph0003vnr74us2t7h7", + "name" : "Surat Keterangan Domisili Organisasi", + "deskripsi" : "

Persyaratan Dokumen:

Alur Pelayanan:

" + }, + { + "id" : "cmdxxv3i80004vniidg1mrucc", + "name" : "Surat Keterangan Penghasilan", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdxxwp070008vnii9jbdcto7", + "name" : "Surat Keterangan Tidak Mampu", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdxxyfkl000cvnii1bxinnfi", + "name" : "Surat Keterangan Kelahiran", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdxy23pl000gvniihsg38aq4", + "name" : "Surat Keterangan Usaha", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdxy4mgt000kvniib1nemjem", + "name" : "Surat Keterangan Kematian", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdxy61a1000ovniif4ytb9hs", + "name" : "Surat Keterangan Tempat Usaha", + "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" + }, + { + "id" : "cmdxy754q000svniiiz8oqyo0", + "name" : "Surat Keterangan Belum Kawin", + "deskripsi" : "

Persyaratan Dokumen :

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:

  1. Peningkatan Kesehatan Lingkungan:

    Dengan pengelolaan sampah yang efektif, desa dapat menjaga kebersihan lingkungan, mengurangi risiko penyakit, dan menciptakan suasana yang lebih nyaman bagi warga.

  2. 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.

  3. Edukasi dan Kesadaran Lingkungan:

    Fasilitas ini dapat menjadi pusat edukasi bagi masyarakat tentang pentingnya pengelolaan sampah, mendorong partisipasi aktif dalam menjaga kelestarian lingkungan.

  4. 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:

  1. 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.

  2. Pengelolaan Sampah Berbasis Masyarakat:

    Melalui kolaborasi dengan komunitas pemuda peduli lingkungan, BUMDes Pudak Mesari aktif dalam pengelolaan sampah berbasis masyarakat.

  3. Peningkatan Kapasitas dan Transparansi:

    Untuk memastikan pengelolaan yang akuntabel, BUMDes Pudak Mesari rutin mengadakan rapat koordinasi dan pendampingan penyusunan laporan pertanggungjawaban.

  4. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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" : "" + } +] \ 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": "", + "pengalamanOrganisasi": "", + "programUnggulan": "

Pemberdayaan Ekonomi dan UMKM

" + } +] \ 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" : "" + } +] \ 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": "" + } + ] \ 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": "" + } +] 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": "" + } +] 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": "" + } + ] \ 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": "" + } + ] \ 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": "" + } +] 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": "" + } +] \ 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": "" + } +] \ 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": "" + } +] \ 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

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": "" + } +] \ 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": "" + } +] \ 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": "" + } +] 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": "" + } +] 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:

  1. TPS3R Pudak Mesari

  2. Bumdes Pudak Mesari

  3. Pertanian

  4. Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa

  5. Taman Beji Cengana

  6. Dam Tanah Putih

  7. Gumuh Sari Water Park

  8. UMKM

  9. Kawasan Kuliner

  10. IKM berbasis Pengolahan Pangan

  11. Genteng

  12. Peternakan Ikan Lele

  13. 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:

", + "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" : "" + } +] \ 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": "", + "pengalaman": "", + "unggulan": "

Pemberdayaan Ekonomi dan UMKM

" + } +] 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": "
  1. Meningkatkan pengelolaan dan pelayanan informasi yang berkualitas, benar dan bertanggung jawab.
  2. Membangun dan mengembangkan sistem penyediaan dan layanan informasi.
  3. Meningkatkan dan mengembangkan kompetensi dan kualitas SDM dalam bidang pelayanan informasi.
  4. 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() { darmasaba { + 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} + + + + + + ) +} 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) 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") + ) : ( + "" + )} + + + + + Belum punya akun? + + + +
+
+
+
+
+ ); +} + +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); + }} + /> + + + + + + + + +
+
+
+
+
+ ); +} + +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 + + + + + + + + + + + + ); +} + +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 && ( + + )} + + ); +} \ 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 && ( + + )} + + ); +} + +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 && ( + + )} + + ); +} \ 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 */} + + + + + + Edit Kategori Berita + + + + {/* Form Wrapper */} + + + + + + + + + + + ); +} + +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 */} + + + + + + Tambah Kategori Berita + + + + {/* Form utama */} + + + (createState.create.form.name = e.target.value)} + required + /> + + + + + + + + ); +} + +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 + + + + + + + + + + No + Nama + Edit + Hapus + + + + {filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + + {index + 1} + + + + {item.name} + + + + + + + + + + + + + + )) + ) : ( + + +
+ + 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 */} + + + + + + Edit Berita + + + + {/* Form */} + + + handleChange("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 && ( + + Preview Gambar + + )} + + + + + Konten + + { + beritaState.berita.create.form.content = htmlContent; + }} + /> + + + + + + + + + ); +} 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 + + + + + + + + + + Judul + Kategori + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.judul} + + + + + + {item.kategoriBerita?.name || '-'} + + + + + + + )) + ) : ( + + +
+ + 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} + + + + + {v.name} + + + + + + { + stateFileStorage + .del({ id: v.id }) + .finally(() => toast("Foto berhasil dihapus")); + }} + > + + + + + + + ))} + + ) : ( + + Kosong + + 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 ( + + + + + + + Edit Video + + + + + + handleChange('name', e.currentTarget.value)} + required + /> + + + handleChange('linkVideo', e.currentTarget.value)} + required + /> + {embedLink && ( + + + + )} + + + + + Deskripsi Video + + handleChange('deskripsi', val)} + /> + + + + + + + + + ); +} + +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 */} + + + {/* 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 */} + + + + + + + ); +} + +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 + + + + + + + + + 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', + })} + + + + + + + + + + + + + )) + ) : ( + + +
+ 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 */} + + + + + + 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 + /> + + 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)} + /> + + + + + + + + + ); +} + +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 ( + + + + + + + Detail Pengumuman + + + + + + + Kategori + + + {data?.CategoryPengumuman?.name || '-'} + + + + + + Judul + + + {data?.judul || '-'} + + + + + + Deskripsi + + + {data?.deskripsi || '-'} + + + + + + Konten + + + + + + + + + + + + + + + + + + + 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 */} + + + + + + Tambah Pengumuman + + + + + + {/* Judul */} + (pengumumanState.pengumuman.create.form.judul = val.target.value)} + label="Judul" + placeholder="Masukkan judul pengumuman" + required + /> + + {/* Kategori */} + 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 && ( + + Preview Gambar + + )} + + + + + Konten Lengkap + + + handleChange("content", htmlContent) + } + /> + + + + + + + + + ); +} + +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 ( + + + + + + + Detail Potensi + + + + + + Judul + {data.name || '-'} + + + + Kategori + {data.kategori?.nama || '-'} + + + + Deskripsi + + + + + Gambar + {data.image?.link ? ( + {data.name + ) : ( + Tidak ada gambar + )} + + + + Konten + + + + + + + + + + + + + + + + + + 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 */} + + + + + + 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 */} + 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}%` + : '-'} + + + + + + + + + ); +} + +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 */} + + + {/* 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + Tambah Data Pengangguran + + + + {/* Form Card */} + + + + 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 ( + + + + + + 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 && ( + + Preview Gambar + + )} + + + {/* 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 + setFormData({ ...formData, isActive: val === 'true' })} + clearable + /> + + + {/* Submit Button */} + + + + + + + ); +} 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 ( + + + + + + + + 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 ? ( + {data.namaLengkap + ) : ( + Tidak ada foto profil + )} + + + + + + + + + + + + + + + + + + + {/* 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 ( + + + + + + + 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 + + Preview + + )} +
+ + (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 + + 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)} + /> + + + + + +
+
+
+ ); +} + +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 */} + + + + + + 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 + /> + + + + + + + + ); +} + +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 + + + + + + + + + Judul Laporan Publik + Tanggal + Status + Deskripsi + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.judul} + + + + + {new Date(item.tanggalWaktu).toLocaleDateString('id-ID')} + + + + + {item.status} + + + + + + + + + + + + )) + ) : ( + + +
+ + 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 */} + + + + + + Edit Pencegahan Kriminalitas + + + + {/* Form container */} + + + + + + + Deskripsi + + + setFormData((prev) => ({ ...prev, deskripsiSingkat: val })) + } + /> + + + + + Deskripsi Lengkap + + + setFormData((prev) => ({ ...prev, deskripsi: val })) + } + /> + + + + + {embedLink && ( + + + + )} + + + {/* Action button */} + + + + + + + ); +} + +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 */} + + + {/* 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 */} + + + + + + + ); +} + +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 + + + + + + {/* Tabel */} + + + + + Nama Pencegahan + Deskripsi + Deskripsi Singkat + Aksi + + + + {data.length > 0 ? ( + data.map((item) => ( + + + + + {item.judul} + + + + + + + + + + + + + + + + + + )) + ) : ( + + +
+ + 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)} + /> + + + + + {/* Modal Update */} + setModalUpdateOpen(false)} + title="Update Layanan Polsek" + centered + > + + setNamaLayananUpdate(e.currentTarget.value)} + /> + + + + + {/* Header */} + + + + + + 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" + /> + + Layanan Polsek} + placeholder="Pilih layanan polsek" + data={layananOptions} + value={polsekState.create.form.layananPolsekId} + onChange={(val) => (polsekState.create.form.layananPolsekId = val || "")} + /> + + + {/* Tombol Submit */} + + + + + + + ); +} + +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 + + + + + + + + + + Nama Polsek + Jarak + Alamat + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.nama} + + + + {item.jarakKeDesa} + + + {item.alamat} + + + + + + + )) + ) : ( + + +
+ + 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 */} + + + + + + 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 ? ( + + Preview Gambar + + ) : ( + + + + )} + + + {/* Input Judul */} + + setFormData((prev) => ({ ...prev, judul: e.target.value })) + } + required + /> + + {/* Input Deskripsi */} + + + Deskripsi + + + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } + /> + + + {/* Button 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 ( + + + + + + + Detail Tips Keamanan + + + + + + Nama Tips Keamanan + {data.judul || '-'} + + + + Deskripsi + + + + + Gambar + {data.image?.link ? ( + {data.judul + ) : ( + Tidak ada gambar + )} + + + + + + + + + + + + + + + + + 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 */} + + + + + + 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 && ( + + Preview Gambar + + )} + + + {/* Input Judul */} + (stateKeamanan.create.form.judul = e.target.value)} + required + /> + + {/* Editor Deskripsi */} + + + Deskripsi Tips Keamanan + + { + stateKeamanan.create.form.deskripsi = val; + }} + /> + + + {/* Submit Button */} + + + + + + + ); +} + +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 + + + + + + + + + Judul + Deskripsi + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.judul} + + + + + + + + + + + + + )) + ) : ( + + +
+ 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 && ( + + )} + + ); +} \ 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 && ( + + )} + + ); +} + +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 && ( + + )} + + ); +} \ 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 */} + + + + + + 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 && ( + + Preview Gambar + + )} + + + {/* 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 */} + + + + + + + ); +} + +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 */} + + + {/* Wrapper Detail */} + + + + Detail Artikel Kesehatan + + + + + {/* Judul */} + + Judul + {data.title} + + {/* Gambar */} + + Gambar + {data.image?.link ? ( + {data.title + ) : ( + 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 && ( + + Preview Gambar + + )} + + + { + 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 */} + + + + + + + ); +} + +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 + + + + + + {/* Tabel */} + + + + + Judul + Konten + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.title} + + + + + {item.content} + + + + + + + )) + ) : ( + + +
+ 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 */} + + + + + + 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 */} + + + + + + + ); +} + +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 */} + + + {/* 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 */} + + + + + + + ); +} + +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 ( + + + + + + + + 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; + }} + /> + + + + + + ); +} + +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 ( + + + + + } + 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} + + + + + + + + ))} + +
+
+
+
+
+ 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 */} + + + + + {/* 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 + + + + + + {/* Tabel */} + + + + + Fasilitas Kesehatan + Dokter + Layanan + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + {item.name} + + + + + + {item.dokterdantenagamedis?.name || '-'} + + + + + + {item.tarifdanlayanan?.layanan || '-'} + + + + + + + + )) + ) : ( + + +
+ + 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 ( + + + + + } + 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} + + + + + + + + ))} + +
+
+
+
+
+ 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 */} + + + + + + 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 + /> + ))} + + + + + + + + ); +} + +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 */} + + + {/* 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 + /> + + + + + + + + ); +} + +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 + + + + + + {/* 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} + + + + + + + )) + ) : ( + + +
+ + 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 */} + + + + + + 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 */} + + + + + + + ); +} + +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 */} + + + {/* 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 */} + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 */} + + + + + + + ); +} + +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 */} + + + + + {/* 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 + + + + + + {/* 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} + + + + + + + + )) + ) : ( + + +
+ + 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 */} + + + + + + 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 + /> + + + + + + + + ); +} + +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 */} + + + + {/* 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 */} + + + + + + + + + + + + + + + + + {/* 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 */} + + + + + + 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 + /> + + + + + + + + + ); +} + + +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 */} + + + + + + {/* 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 + + + + + + + {/* 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} + + + + + + + + )) + ) : ( + + +
+ + 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 */} + + + + + + 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)} + /> + + + + + + + + + ); +} + +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 */} + + + + + + + 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 + + + + + + + + + + + + + + + + + + + + + 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 */} + + + + + + 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; + }} + /> + + + + + + + + + + ); +} + + +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 */} + + + + + + {/* 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 + + + + + + + + + + + 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} + + + + + + + + )) + ) : ( + + +
+ + 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: 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 + + + + + + + + + + ) : null} + +
+
+
+
+ + ); +} + +export default Page; diff --git a/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx b/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx new file mode 100644 index 00000000..1ccaa2f0 --- /dev/null +++ b/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx @@ -0,0 +1,102 @@ +'use client' +import polsekTerdekatState from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat'; +import colors from '@/con/colors'; +import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconNavigation, IconSearch } from '@tabler/icons-react'; +import React, { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import BackButton from '../../../desa/layanan/_com/BackButto'; +import { useRouter } from 'next/navigation'; +import { useDebouncedValue } from '@mantine/hooks'; + +function Page() { + const state = useProxy(polsekTerdekatState); + const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay + const router = useRouter() + + const { + data, + page, + totalPages, + loading, + load, + } = state.findMany; + + useShallowEffect(() => { + load(page, 3, debouncedSearch) + }, [page, debouncedSearch]) + + if (loading || !data) { + return ( + + + + ) + } + + return ( + + + + + + + + Semua Polsek Terdekat + + + + setSearch(e.target.value)} + leftSection={} + w={{ base: "50%", md: "100%" }} + /> + + + + + {data.map((v, k) => { + return ( + + + {v.nama} + Alamat: {v.alamat} + Jarak: {v.jarakKeDesa} + Telepon: {v.nomorTelepon} + Jam Operasional: {v.jamOperasional} + + + + + + + + + ) + })} + + +
+ load(newPage)} // ini penting! + total={totalPages} + mt="md" + mb="md" + /> +
+
+ ); +} + +export default Page; diff --git a/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx b/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx new file mode 100644 index 00000000..e4709396 --- /dev/null +++ b/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx @@ -0,0 +1,108 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; +import BackButton from '../../desa/layanan/_com/BackButto'; +import { useProxy } from 'valtio/utils'; +import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { useState } from 'react'; +import { IconSearch } from '@tabler/icons-react'; + + +function Page() { + const state = useProxy(tipsKeamananState) + const [search, setSearch] = useState('') + const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay + const { + data, + page, + totalPages, + loading, + load, + } = state.findMany; + + useShallowEffect(() => { + load(page, 3, debouncedSearch) + }, [page, debouncedSearch]) + + if (loading || !data) { + return ( + + + + ) + } + + return ( + + + + + + + + + Tips Keamanan + + + + setSearch(e.target.value)} + leftSection={} + w={{ base: "50%", md: "100%" }} + /> + + + + Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. + + + + + + {data.map((v, k) => { + return ( + + +
+ +
+ + + + {v.judul} + + + + + + +
+
+ ) + })} +
+
+
+
+ load(newPage)} // ini penting! + total={totalPages} + my="md" + /> +
+
+ ); +} + +export default Page; diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx new file mode 100644 index 00000000..7fe14d54 --- /dev/null +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx @@ -0,0 +1,179 @@ +'use client' +import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; +import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; +import colors from '@/con/colors'; +import { Box, Divider, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react'; +import { useParams } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; + +function Page() { + const state = useProxy(artikelKesehatanState); + const params = useParams(); + + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); + + if (!state.findUnique.data) { + return ( + + + + + + + ); + } + + return ( + + + + + + + + + + + {state.findUnique.data.title || 'Detail Artikel Kesehatan'} + + + + + + {state.findUnique.data.title} + + + + + + + + + {new Date(state.findUnique.data.createdAt).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + + + + + + + Pendahuluan + + + + + + {state.findUnique.data.symptom?.title} + + + + + + {state.findUnique.data.prevention?.title} + + + + + + {state.findUnique.data.firstaid?.title} + + + + + + {state.findUnique.data.mythvsfact?.title} + + + + + + Mitos + Fakta + + + + {state.findUnique.data?.mythvsfact ? ( + + + + + + + + + ) : ( + + Belum ada data mitos dan fakta + + )} + +
+
+
+ + + Kapan Harus ke Dokter? + + + + Segera bawa penderita ke fasilitas kesehatan jika mengalami: + + + + + + Kasus DBD di Wilayah Abiansemal + + + + + + Informasi Lebih Lanjut + + + Hotline DBD: (0361) 123456 + WhatsApp Center: 081234567890 + Email: p2p@dinkes.badungkab.go.id + + + + + + Referensi + + + Kementerian Kesehatan RI. (2024). Pedoman Pencegahan dan Pengendalian DBD. + World Health Organization. (2024). Dengue Guidelines for Diagnosis, Treatment, Prevention and Control. + Dinas Kesehatan Kabupaten Badung. (2025). Laporan Surveilans DBD Triwulan I 2025. + + +
+
+
+
+
+
+
+ ); +} + +export default Page; diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx new file mode 100644 index 00000000..defb85c2 --- /dev/null +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx @@ -0,0 +1,88 @@ +'use client' +import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; +import colors from '@/con/colors'; +import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconCalendar, IconChevronRight } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; + +function ArtikelKesehatanPage() { + const state = useProxy(artikelKesehatanState) + const router = useRouter() + + useShallowEffect(() => { + state.findMany.load() + }, []) + + if (!state.findMany.data) { + return ( + + + Memuat artikel kesehatan... + + ) + } + + return ( + + + + + Artikel Kesehatan + + + {state.findMany.data.length === 0 ? ( + + + Belum ada artikel kesehatan yang tersedia + + + ) : ( + state.findMany.data.map((item) => ( + (e.currentTarget.style.transform = 'translateY(-4px)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} + > + + {item.title} + + + {item.title} + + + + {new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} • Dinas Kesehatan + + + + {item.content} + + + + + + + )) + )} + + + + ); +} + +export default ArtikelKesehatanPage; diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx new file mode 100644 index 00000000..6aa24dfd --- /dev/null +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx @@ -0,0 +1,320 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client' +import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; +import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; +import colors from '@/con/colors'; +import { ActionIcon, AspectRatio, Badge, Box, Button, Card, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconUser } from '@tabler/icons-react'; +import { useParams } from 'next/navigation'; +import { useMemo } from 'react'; +import { useProxy } from 'valtio/utils'; + +interface Kontak { + telepon: string; + whatsapp: string; + email: string; +} + + +interface Lokasi { + mapsEmbed: string; +} + +interface TarifDanLayanan { + layanan: string; + tarif: string | number; + gratisBpjs?: boolean; +} + +function Page() { + const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); + const params = useParams(); + + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); + + const data = state.findUnique.data as any; // Temporary any to fix type issues + + const nama = data?.name || 'Fasilitas Kesehatan'; + const prosedur = data?.prosedurpendaftaran.content || ''; + const alamat = data?.informasiumum?.alamat || '-'; + const jam = data?.informasiumum?.jamOperasional || '-'; + const layananUnggulan = data?.layananunggulan?.content || ''; + const tenaga = data?.dokterdantenagamedis || null; + const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || ''; + const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null; + const kontak = (data?.kontak as Kontak) || { + telepon: '(0361) 123456', + whatsapp: '6289647037426', + email: 'info@fasilitas-kesehatan.id' + }; + const lokasi = (data?.lokasi as Lokasi) || { + mapsEmbed: 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3945.272172359321!2d115.21836257533302!3d-8.569807186941553!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23e9d99b9395f%3A0xb002795fdcb33b30!2sUPTD%20Puskesmas%20Abiansemal%20III!5e0!3m2!1sid!2sid!4v1744792682341!5m2!1sid!2sid' + }; + + const gratisBpjs = (data?.tarifdanlayanan as TarifDanLayanan)?.gratisBpjs ?? true; + + const formatRupiah = useMemo( + () => (v?: number | string) => { + if (v === null || v === undefined || v === '') return '-'; + const n = typeof v === 'string' ? Number(v) : v; + if (Number.isNaN(n as number)) return String(v); + return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(n as number); + }, + [] + ); + + if (state.findUnique.loading || !data) { + return ( + + + + + + + + + + + + + + + + + + + + + + ); + } + + return ( + + + + + + + + }>Pelayanan Aktif + }>Jam: {jam} + + + + + + + + {nama} + + + + {alamat} + + + + {kontak.telepon} + + {({ copied, copy }) => ( + + {copied ? : } + + )} + + + + + {kontak.whatsapp} + + {({ copied, copy }) => ( + + {copied ? : } + + )} + + + + + {kontak.email} + + {({ copied, copy }) => ( + + {copied ? : } + + )} + + + + + + + + + + + + + + Informasi Umum + Terverifikasi + + + + + Nama Fasilitas + {nama} + + + Jam Operasional + {jam} + + + + Layanan Unggulan + + {layananUnggulan ? ( + + ) : ( + + + + Belum ada informasi fasilitas pendukung. + + + )} + + Peta Lokasi + +