Compare commits
31 Commits
nico/28-ju
...
nico/14-au
| Author | SHA1 | Date | |
|---|---|---|---|
| d7a592c635 | |||
| 5e137ba658 | |||
| c99416c7f8 | |||
| 212e2db1fb | |||
| b8a45bc451 | |||
| 0777b00a7d | |||
| a035039b2c | |||
| a6832cad40 | |||
| a1d55e2b0a | |||
| c1583c21b1 | |||
| 2fe8b8ce1a | |||
| 5cbf7810bc | |||
| b3bf6b0327 | |||
| a65529cb23 | |||
| afc7bced44 | |||
| 0ac9fa1f53 | |||
| d4af56b508 | |||
| b62c4be30a | |||
| ab887c30e6 | |||
| 8e76a83d14 | |||
| a2b68ec78b | |||
| 0e55462adc | |||
| 73ae198158 | |||
| 9d14bb0c56 | |||
| 1cdff53c56 | |||
| 54312e9486 | |||
| 024d5517fa | |||
| 4e61695649 | |||
| c11cc421a4 | |||
| 0109886e00 | |||
| 50e8999205 |
13
package.json
13
package.json
@@ -14,8 +14,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cubejs-client/core": "^0.31.0",
|
"@cubejs-client/core": "^0.31.0",
|
||||||
|
"@elysiajs/cookie": "^0.8.0",
|
||||||
"@elysiajs/cors": "^1.2.0",
|
"@elysiajs/cors": "^1.2.0",
|
||||||
"@elysiajs/eden": "^1.3.2",
|
"@elysiajs/eden": "^1.3.2",
|
||||||
|
"@elysiajs/jwt": "^1.3.2",
|
||||||
"@elysiajs/static": "^1.3.0",
|
"@elysiajs/static": "^1.3.0",
|
||||||
"@elysiajs/stream": "^1.1.0",
|
"@elysiajs/stream": "^1.1.0",
|
||||||
"@elysiajs/swagger": "^1.2.0",
|
"@elysiajs/swagger": "^1.2.0",
|
||||||
@@ -44,8 +46,10 @@
|
|||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel-autoplay": "^8.5.2",
|
"embla-carousel-autoplay": "^8.5.2",
|
||||||
@@ -54,6 +58,7 @@
|
|||||||
"framer-motion": "^12.23.5",
|
"framer-motion": "^12.23.5",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
"jotai": "^2.12.3",
|
"jotai": "^2.12.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"list": "^2.0.19",
|
"list": "^2.0.19",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -63,12 +68,15 @@
|
|||||||
"next-view-transitions": "^0.3.4",
|
"next-view-transitions": "^0.3.4",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primereact": "^10.9.6",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "^6.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-simple-toasts": "^6.1.0",
|
"react-simple-toasts": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
@@ -78,15 +86,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.6",
|
"eslint-config-next": "15.1.6",
|
||||||
|
"parcel": "^2.6.2",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5",
|
"typescript": "^5"
|
||||||
"parcel": "^2.6.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
prisma/data/desa/layanan/pelayananSuratKeterangan.json
Normal file
57
prisma/data/desa/layanan/pelayananSuratKeterangan.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id" : "cmdxyb9zi0010vniiaeyi55ui",
|
||||||
|
"name" : "Surat Keterangan Beda Biodata Diri",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau Kartu Keluarga</p></li><li><p>Fotocopy dokumen bersangkutan yang terdapat perbedaan biodata diri misal : Sertifikat Tanah/Ijazah/Polis Asuransi dan lainnya.</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxycqz40014vniidftrixvf",
|
||||||
|
"name" : "Surat Keterangan Yatim Piatu",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau KIA atau Kartu Keluarga</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdwx3wph0003vnr74us2t7h7",
|
||||||
|
"name" : "Surat Keterangan Domisili Organisasi",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen:</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy Surat Keterangan Terdaftar (SKT) organisasi atau Pengukuhan Kelompok</p></li><li><p>Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap dengan Kop Organisasi</p></li><li><p>Tanggal berdiri/Tahun berdiri/Sejak kapan berdirinya organisasi</p></li></ul><p>Alur Pelayanan:</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxxv3i80004vniidg1mrucc",
|
||||||
|
"name" : "Surat Keterangan Penghasilan",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP orang tua atau Fotocopy Kartu keluarga</p></li><li><p>Membuat Surat Pernyataan Penghasilan bermaterai (disertai jumlah penghasilan)</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxxwp070008vnii9jbdcto7",
|
||||||
|
"name" : "Surat Keterangan Tidak Mampu",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP/KIA atau Kartu Keluarga</p></li><li><p>Fotocopy Kartu Indonesia Pintar/Kartu Perlindungan Sosial/Terdaftar dalam DTKS</p></li><li><p>Jika tidak memiliki Kartu tersebut diatas diwajibkan membuat Surat Pernyataan Tidak Mampu</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxxyfkl000cvnii1bxinnfi",
|
||||||
|
"name" : "Surat Keterangan Kelahiran",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy Surat lahir dari dokter/bidan (jika ada)</p></li><li><p>Fotocopy Kartu Keluarga</p></li><li><p>Fotocopy KTP 2 orang saksi</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxy23pl000gvniihsg38aq4",
|
||||||
|
"name" : "Surat Keterangan Usaha",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau Kartu Keluarga</p></li><li><p>Foto Lokasi dan Kegiatan Usaha di cetak dalam selembar kertas (diparaf dan stempel oleh Kelian Banjar Dinas)</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxy4mgt000kvniib1nemjem",
|
||||||
|
"name" : "Surat Keterangan Kematian",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau Kartu Keluarga</p></li><li><p>Surat Kematian dari rumah sakit atau dokter (jika ada)</p></li><li><p>tanggal kematian</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxy61a1000ovniif4ytb9hs",
|
||||||
|
"name" : "Surat Keterangan Tempat Usaha",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau Kartu Keluarga</p></li><li><p>Foto Lokasi dan Kegiatan Usaha di cetak dalam selembar kertas (diparaf dan stempel oleh Kelian Banjar Dinas)</p></li><li><p>Surat Perjanjian Sewa/Kontrak atau Kwintansi Pembayaran Sewa 3 bulan terakhir bagi yang mengontrak tempat usaha, apabila tempat usaha milik sendiri lampiri dengan dokumen kepemilikan tempat usaha (dapat berupa fotocopy sppt atau Fotocopy Sertipikat Hak Milik)</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxy754q000svniiiz8oqyo0",
|
||||||
|
"name" : "Surat Keterangan Belum Kawin",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau Kartu Keluarga</p></li><li><p>Khusus bagi yang berstatus duda atau janda melampirkan fotocopy akta cerai atau dokumen pendukung lainnya</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdxy8pi2000wvnii48fc1sxd",
|
||||||
|
"name" : "Surat Keterangan Kelakuan Baik",
|
||||||
|
"deskripsi" : "<p>Persyaratan Dokumen :</p><ul><li><p>Pengantar Kelian Banjar Dinas di Wilayah Masing - masing</p></li><li><p>Fotocopy KTP atau Kartu Keluarga</p></li></ul><p>Alur Pelayanan :</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
20
prisma/data/desa/layanan/pelayananTelunjukSaktiDesa.json
Normal file
20
prisma/data/desa/layanan/pelayananTelunjukSaktiDesa.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
74
prisma/data/desa/potensi/potensi-desa.json
Normal file
74
prisma/data/desa/potensi/potensi-desa.json
Normal file
@@ -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": "<p>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.</p><p>Potensi Desa melalui TPS3R Pudak Mesari:</p><ol><li><p><strong>Peningkatan Kesehatan Lingkungan:</strong></p><p>Dengan pengelolaan sampah yang efektif, desa dapat menjaga kebersihan lingkungan, mengurangi risiko penyakit, dan menciptakan suasana yang lebih nyaman bagi warga.</p></li><li><p><strong>Pemberdayaan Ekonomi Masyarakat:</strong></p><p>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.</p></li><li><p><strong>Edukasi dan Kesadaran Lingkungan:</strong></p><p>Fasilitas ini dapat menjadi pusat edukasi bagi masyarakat tentang pentingnya pengelolaan sampah, mendorong partisipasi aktif dalam menjaga kelestarian lingkungan.</p></li><li><p><strong>Pengembangan Pariwisata Berkelanjutan:</strong></p><p>Dengan lingkungan yang bersih dan asri, Desa Darmasaba memiliki potensi untuk menarik wisatawan yang tertarik pada ekowisata dan budaya lokal, sehingga meningkatkan perekonomian desa.</p></li></ol>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran BUMDes Pudak Mesari:</p><ol><li><p><strong>Pengembangan Usaha Mikro dan Kecil:</strong></p><p>BUMDes Pudak Mesari menyediakan layanan bagi pelaku usaha mikro dan kecil di desa, seperti penyediaan konsumsi dan snack kotak untuk berbagai acara.</p></li><li><p><strong>Pengelolaan Sampah Berbasis Masyarakat:</strong></p><p>Melalui kolaborasi dengan komunitas pemuda peduli lingkungan, BUMDes Pudak Mesari aktif dalam pengelolaan sampah berbasis masyarakat.</p></li><li><p><strong>Peningkatan Kapasitas dan Transparansi:</strong></p><p>Untuk memastikan pengelolaan yang akuntabel, BUMDes Pudak Mesari rutin mengadakan rapat koordinasi dan pendampingan penyusunan laporan pertanggungjawaban.</p></li><li><p><strong>Kolaborasi Internasional:</strong></p><p>Desa Darmasaba, melalui BUMDes Pudak Mesari, menerima kunjungan dari tim Osaki Jepang untuk memperkuat pengelolaan sampah dan lingkungan.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi Desa melalui Taman Beji Cengana:</p><ol><li><p><strong>Pengembangan Pariwisata Spiritual:</strong></p><p>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.</p></li><li><p><strong>Pelestarian Budaya dan Tradisi:</strong></p><p>Dengan mempromosikan Taman Beji Cengana sebagai pusat kegiatan budaya dan ritual tradisional, desa dapat memastikan bahwa warisan budaya dan tradisi lokal tetap lestari.</p></li><li><p><strong>Pendidikan dan Penelitian:</strong></p><p>Taman Beji Cengana dapat dijadikan sebagai pusat pendidikan dan penelitian bagi akademisi, peneliti, dan pelajar yang tertarik mempelajari budaya, agama, dan sejarah Bali.</p></li><li><p><strong>Pengembangan Ekonomi Kreatif:</strong></p><p>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.</p></li><li><p><strong>Konservasi Lingkungan:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi Desa melalui Gumuh Sari Waterpark:</p><ol><li><p><strong>Pengembangan Pariwisata Lokal:</strong></p><p>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.</p></li><li><p><strong>Peningkatan Ekonomi Masyarakat:</strong></p><p>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.</p></li><li><p><strong>Pengembangan Fasilitas Olahraga dan Kesehatan:</strong></p><p>Dengan adanya pusat futsal dan gym, Gumuh Sari Waterpark mendorong masyarakat untuk berpartisipasi dalam kegiatan olahraga, yang dapat meningkatkan kesehatan dan kesejahteraan warga.</p></li><li><p><strong>Pemberdayaan Komunitas Melalui Event dan Acara:</strong></p><p>Waterpark ini sering menjadi tuan rumah berbagai acara komunitas, seperti pesta busa dan bola, yang dapat mempererat hubungan antarwarga dan menciptakan lingkungan yang harmonis.</p></li><li><p><strong>Peningkatan Infrastruktur dan Aksesibilitas:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi Desa melalui Pertanian:</p><ol><li><p><strong>Potensi dan Komoditas Unggulan</strong></p><p>Pertanian di Desa Darmasaba mengandalkan berbagai jenis tanaman yang memiliki nilai ekonomi tinggi, di antaranya:</p><p>- Padi : Sebagai salah satu desa yang masih mempertahankan sistem subak, Darmasaba menjadi bagian dari lumbung pangan di Bali.</p><p>- Sayur-mayur : Beberapa jenis sayuran seperti kangkung, bayam, cabai, dan tomat banyak dibudidayakan oleh petani lokal.</p><p>- Buah-buahan tropis : Termasuk pisang, mangga, dan kelapa, yang menjadi sumber pendapatan tambahan bagi petani.</p><p>- Tanaman obat dan rempah : Seperti jahe, kunyit, dan lengkuas, yang memiliki permintaan tinggi baik untuk kebutuhan rumah tangga maupun industri herbal.</p></li><li><p><strong>Sistem Irigasi Tradisional Subak:</strong></p><p>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.</p></li><li><p><strong>Pengembangan Pertanian Organik:</strong></p><p>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.</p></li><li><p><strong>Agrowisata sebagai Sumber Pendapatan Baru:</strong></p><p>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.</p></li><li><p><strong>Pemberdayaan Petani dan UMKM Berbasis Pertanian:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi Desa melalui Kawasan Kuliner:</p><ol><li><p><strong>Ragam Kuliner Khas Bali</strong></p><p>Darmasaba memiliki banyak warung dan rumah makan yang menyajikan hidangan khas Bali yang otentik, seperti:</p><p>- Babi Guling : Salah satu kuliner favorit di Bali yang banyak ditemukan di Darmasaba.</p><p>- Ayam Betutu : Hidangan ayam berbumbu khas yang dimasak dengan teknik khas Bali.</p><p>- Lawar : Campuran daging dan sayuran berbumbu khas Bali.</p><p>- Sate Lilit : Sate khas Bali yang terbuat dari daging cincang yang dibalut pada batang serai.</p></li><li><p><strong>Wisata Kuliner Modern & Cafe Kekinian:</strong></p><p>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.</p></li><li><p><strong>Pasar Kuliner Malam:</strong></p><p>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.</p></li><li><p><strong>Potensi Ekonomi & UMKM Kuliner:</strong></p><p>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.</p></li><li><p><strong>Kawasan Kuliner Berbasis Pariwisata:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran IKM Berbasis Pengolahan Pangan:</p><ol><li><p><strong>Produk Unggulan Pengolahan Pangan</strong></p><p>Beberapa produk olahan pangan yang potensial dikembangkan di Darmasaba meliputi:</p><p>- Keripik dan Snack Tradisional : Seperti keripik pisang, keripik singkong, dan rengginang.</p><p>- Sambal Khas Bali : Seperti sambal matah dan sambal embe yang banyak diminati pasar lokal dan nasional.</p><p>- Minuman Herbal dan Jamu : Berbasis rempah seperti kunyit asam, beras kencur, dan wedang jahe.</p><p>- Olahan Makanan Berbasis Kelapa : Seperti virgin coconut oil (VCO), serundeng, dan gula aren.</p><p>- Kue Tradisional Bali : Seperti jaje laklak, jaje uli, dan klepon yang dapat dikemas secara modern.</p></li><li><p><strong>Peluang Ekonomi dan Pemberdayaan UMKM:</strong></p><p>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.</p></li><li><p><strong>Digitalisasi dan Pemasaran Online:</strong></p><p>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.</p></li><li><p><strong>Pengembangan Kawasan Sentra IKM:</strong></p><p>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.</p></li><li><p><strong>Sinergi dengan Pariwisata dan Agrowisata:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran UMKM Genteng:</p><ol><li><p><strong>Genteng Tradisional Berkualitas Tinggi</strong></p><p>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:</p><p>- Genteng Tanah Liat : Kuat, tahan lama, dan memiliki estetika khas tradisional.</p><p>- Genteng Beton : Cocok untuk bangunan modern dengan ketahanan lebih tinggi.</p><p>- Genteng Keramik : Memberikan tampilan elegan dan daya serap air yang lebih rendah.</p></li><li><p><strong>Peluang Ekonomi dan Pemberdayaan Masyarakat:</strong></p><p>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.</p></li><li><p><strong>Inovasi dan Pengembangan Teknologi</strong></p><p>Beberapa pengrajin genteng di Darmasaba telah mulai mengadopsi teknologi modern dalam proses produksi, seperti:</p><p>- Penggunaan cetakan dan oven pembakaran efisien untuk meningkatkan kualitas dan kapasitas produksi.</p><p>- Teknik pelapisan anti bocor dan anti lumut untuk membuat genteng lebih tahan lama.</p><p>- Desain genteng inovatif yang lebih ringan dan mudah dipasang.</p></li><li><p><strong>Pemasaran dan Ekspansi Pasar</strong></p><p>Dengan meningkatnya pembangunan perumahan dan proyek konstruksi di Bali, permintaan akan genteng berkualitas terus bertambah. UMKM genteng Darmasaba dapat memperluas pasarnya dengan:</p><p>- Menjalin kerja sama dengan kontraktor dan pengembang properti.</p><p>- Mempromosikan produk melalui media sosial dan marketplace online.</p><p>- Menyediakan layanan custom sesuai kebutuhan pelanggan.</p></li><li><p><strong>Keberlanjutan dan Ramah Lingkungan:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran Peternakan Ikan Lele:</p><ol><li><p><strong>Kondisi Lingkungan yang Mendukung</strong></p><p>Darmasaba memiliki sumber air yang cukup serta iklim yang cocok untuk budidaya ikan lele. Kolam-kolam budidaya dapat dibuat dengan berbagai sistem, seperti:</p><p>- Kolam Terpal : Mudah dibuat dan lebih efisien dalam perawatan.</p><p>- Kolam Beton : Lebih tahan lama dan cocok untuk produksi skala besar.</p><p>- Sistem Bioflok : Teknologi modern yang dapat meningkatkan kepadatan ikan dan mengurangi limbah.</p></li><li><p><strong>Permintaan Pasar yang Tinggi:</strong></p><p>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.</p></li><li><p><strong>Inovasi dan Pengembangan Teknologi</strong></p><p>Beberapa pengrajin genteng di Darmasaba telah mulai mengadopsi teknologi modern dalam proses produksi, seperti:</p><p>- Penggunaan cetakan dan oven pembakaran efisien untuk meningkatkan kualitas dan kapasitas produksi.</p><p>- Teknik pelapisan anti bocor dan anti lumut untuk membuat genteng lebih tahan lama.</p><p>- Desain genteng inovatif yang lebih ringan dan mudah dipasang.</p></li><li><p><strong>Pemasaran dan Ekspansi Pasar</strong></p><p>Dengan meningkatnya pembangunan perumahan dan proyek konstruksi di Bali, permintaan akan genteng berkualitas terus bertambah. UMKM genteng Darmasaba dapat memperluas pasarnya dengan:</p><p>- Menjalin kerja sama dengan kontraktor dan pengembang properti.</p><p>- Mempromosikan produk melalui media sosial dan marketplace online.</p><p>- Menyediakan layanan custom sesuai kebutuhan pelanggan.</p></li><li><p><strong>Keberlanjutan dan Ramah Lingkungan:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa:</p><ol><li><p><strong>Keindahan Alam dan Udara Segar:</strong></p><p>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.</p></li><li><p><strong>Fasilitas Olahraga dan Rekreasi</strong></p><p>Selain untuk jogging, jalur ini juga cocok digunakan untuk:</p><p>- Bersepeda santai : Jalur yang nyaman untuk pecinta sepeda.</p><p>- Trekking ringan : Cocok bagi wisatawan yang ingin menikmati suasana pedesaan.</p><p>- Meditasi dan Yoga : Area yang tenang dan alami, ideal untuk relaksasi.</p></li><li><p><strong>Destinasi Wisata Sehat dan Edukasi:</strong></p><p>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.</p></li><li><p><strong>Potensi Ekonomi bagi Masyarakat</strong></p><p>Dengan meningkatnya jumlah pengunjung, masyarakat sekitar dapat memanfaatkan peluang usaha seperti:</p><p>- Warung sehat dan kuliner lokal : Menyediakan makanan dan minuman sehat bagi para pengunjung.</p><p>- Jasa penyewaan sepeda : Menarik bagi wisatawan yang ingin berkeliling lebih jauh.</p><p>- Pemandu wisata lokal : Memberikan pengalaman lebih bagi wisatawan yang ingin mengenal sejarah dan budaya desa.</p></li><li><p><strong>Pengembangan Berkelanjutan:</strong></p><p>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.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran Dam Tanah Putih:</p><ol><li><p><strong>Fungsi Utama Sebagai Sumber Pengairan</strong></p><p>Dam Tanah Putih memiliki peran penting dalam sistem irigasi yang menopang sektor pertanian di Darmasaba. Air dari dam ini digunakan untuk:</p><p>- Mengairi sawah dan ladang : Menjamin ketersediaan air bagi petani sepanjang tahun.</p><p>- Menjaga keseimbangan ekosistem : Menjadi habitat bagi ikan air tawar dan berbagai biota air.</p><p>- Menampung air hujan : Membantu mengurangi risiko banjir dan kekeringan.</p></li><li><p><strong>Potensi Wisata Alam dan Rekreasi</strong></p><p>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:</p><p>- Trekking dan jogging di sekitar dam : Menikmati udara segar dan pemandangan indah.</p><p>- Berkemah dan piknik : Cocok untuk keluarga dan komunitas yang ingin menikmati alam.</p><p>- Wisata air : Seperti pemancingan atau wisata perahu kecil yang dapat menarik wisatawan.</p><p>- Spot fotografi alam : Keindahan dam dan sekitarnya menjadi latar yang menarik bagi para fotografer.</p></li><li><p><strong>Potensi Ekonomi dan UMKM Lokal</strong></p><p>Dengan pengembangan dam sebagai destinasi wisata, masyarakat sekitar dapat memanfaatkan peluang usaha seperti:</p><p>- Warung makan dan jajanan tradisional : Menyediakan makanan khas Bali bagi wisatawan.</p><p>- Jasa penyewaan alat rekreasi : Seperti pancing atau perahu kecil.</p><p>- Produk kerajinan tangan dan suvenir : Oleh-oleh khas Darmasaba yang menarik bagi pengunjung.</p></li><li><p><strong>Pengembangan Konservasi dan Edukasi Lingkungan</strong></p><p>Dam Tanah Putih juga bisa menjadi tempat edukasi lingkungan dengan konsep konservasi, di mana pengunjung bisa belajar tentang:</p><p>- Pengelolaan sumber daya air yang berkelanjutan.</p><p>- Keanekaragaman hayati di sekitar dam.</p><p>- Pentingnya ekosistem perairan bagi pertanian dan kehidupan masyarakat.</p></li></ol><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>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.</p><p>Potensi dan Peran UMKM:</p><ol><li><p><strong>Kerajinan Tangan dan Produk Lokal</strong></p><p>Darmasaba memiliki banyak pengrajin yang menghasilkan produk unik dengan nilai seni tinggi, seperti:</p><p>- Genteng dan bahan bangunan tradisional : Genteng khas Darmasaba yang berkualitas tinggi.</p><p>- Kerajinan anyaman dan ukiran : Produk berbasis rotan dan kayu yang banyak diminati pasar lokal dan internasional.</p><p>- Pakaian adat dan kain tradisional : Mendukung pelestarian budaya Bali.</p></li><li><p><strong>Industri Kuliner Khas Darmasaba</strong></p><p>Kuliner khas desa ini memiliki potensi besar untuk dikembangkan sebagai bisnis UMKM, seperti:</p><p>- Babi Guling : Salah satu kuliner favorit yang banyak diminati wisatawan.</p><p>- Jajanan tradisional Bali : Seperti laklak, jaja uli, dan klepon yang masih dibuat dengan cara tradisional.</p><p>- Olahan ikan lele : Seperti abon lele, lele asap, dan pecel lele yang memiliki pasar luas.</p></li><li><p><strong>UMKM Berbasis Pengolahan Pangan</strong></p><p>Beberapa UMKM di Darmasaba mengolah hasil pertanian dan peternakan menjadi produk bernilai tambah, seperti:</p><p>- Keripik singkong dan pisang : Camilan sehat berbasis bahan lokal.</p><p>- Olahan kelapa : Seperti minyak kelapa murni dan gula aren.</p><p>- Produk herbal dan jamu : Menggunakan bahan-bahan alami dari tanaman lokal.</p></li><li><p><strong>Dukungan dan Pengembangan UMKM</strong></p><p>Agar UMKM di Darmasaba semakin berkembang, perlu adanya:</p><p>- Pelatihan dan pendampingan usaha : Untuk meningkatkan kualitas produk dan manajemen usaha.</p><p>- Pemasaran digital : Menggunakan media sosial dan e-commerce untuk menjangkau pasar lebih luas.</p><p>- Kerja sama dengan BUMDes Pudak Mesari : Untuk membantu akses modal dan pengelolaan bisnis yang lebih profesional.</p></li></ol><p>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.</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
22
prisma/data/landing-page/apbdes/apbdes.json
Normal file
22
prisma/data/landing-page/apbdes/apbdes.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
128
prisma/data/landing-page/desa-anti-korupsi/desaantiKorpusi.json
Normal file
128
prisma/data/landing-page/desa-anti-korupsi/desaantiKorpusi.json
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cmds9h9ko000pvnberdjnx64b",
|
||||||
|
"name": "1.1 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA",
|
||||||
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA</p>",
|
||||||
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds9sjmz000svnbesv2133of",
|
||||||
|
"name": "1.2 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA",
|
||||||
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA</p>",
|
||||||
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds9tcpi000vvnbev3ebtlnt",
|
||||||
|
"name": "1.3 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
|
||||||
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN</p>",
|
||||||
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA</p>",
|
||||||
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds9ugap0011vnbe118yv871",
|
||||||
|
"name": "1.5 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA",
|
||||||
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA</p>",
|
||||||
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsa35310014vnbe6qy6l1rz",
|
||||||
|
"name": "2.1 ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA",
|
||||||
|
"deskripsi": "<p>ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA</p>",
|
||||||
|
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsa46590017vnbepp3noso1",
|
||||||
|
"name": "2.2 ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH",
|
||||||
|
"deskripsi": "<p>ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH</p>",
|
||||||
|
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsa5m7z001avnbe4cvfrcz0",
|
||||||
|
"name": "2.3 TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI",
|
||||||
|
"deskripsi": "<p>TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI</p>",
|
||||||
|
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsa8q5q001dvnbemch8j24x",
|
||||||
|
"name": "3.1 ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT",
|
||||||
|
"deskripsi": "<p>ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT</p>",
|
||||||
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsa9lbi001gvnbequn2ba7m",
|
||||||
|
"name": "3.2 ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA",
|
||||||
|
"deskripsi": "<p>ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA</p>",
|
||||||
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>ADANYA KETERBUKAAN AKSES MASYARAKAT TERHADAP INFORMASI LAYANAN PEMERINTAH DESA (KESEHATAN, PENDIDIKAN, SOSIAL, LINGKUNGAN, TRANTIBUMLINMAS, PEKERJAAN UMUM) PEMBANGUNAN, KEPENDUDUKAN, KEUANGAN, DAN PELAYANAN LAINNYA</p>",
|
||||||
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsaaw8d001mvnbek3tfefrk",
|
||||||
|
"name": "3.4 ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT",
|
||||||
|
"deskripsi": "<p>ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT</p>",
|
||||||
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsabhif001pvnbepm06hry6",
|
||||||
|
"name": "3.5 ADANYA MAKLUMAT PELAYANAN",
|
||||||
|
"deskripsi": "<p>ADANYA MAKLUMAT PELAYANAN</p>",
|
||||||
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsag40b001svnbe7krq9khc",
|
||||||
|
"name": "4.1 ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA",
|
||||||
|
"deskripsi": "<p>ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA</p>",
|
||||||
|
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsagkaf001vvnbejo26w8sa",
|
||||||
|
"name": "4.2 ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
|
||||||
|
"deskripsi": "<p>ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN</p>",
|
||||||
|
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsah4qe001yvnbeiy3mwrvb",
|
||||||
|
"name": "4.3 ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA",
|
||||||
|
"deskripsi": "<p>ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA</p>",
|
||||||
|
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdsak5vn0021vnbemg86aab4",
|
||||||
|
"name": "5.1 ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
|
||||||
|
"deskripsi": "<p>ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI</p>",
|
||||||
|
"kategoriId": "cmds9govb000mvnbesq8b4y99",
|
||||||
|
"fileId": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "<p>ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI</p>",
|
||||||
|
"kategoriId": "cmds9govb000mvnbesq8b4y99",
|
||||||
|
"fileId": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
26
prisma/data/landing-page/penghargaan/penghargaan.json
Normal file
26
prisma/data/landing-page/penghargaan/penghargaan.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id" : "cmdzdewrs0003vnp0yh1klh0m",
|
||||||
|
"name" : "Penghargaan Bhawana Sewaka Nugraha kepada Perbekel Darmasaba",
|
||||||
|
"juara" : "Penghargaan Bhawana Sewaka Nugraha kepada Perbekel Darmasaba",
|
||||||
|
"deskripsi" : "<p>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.<br><br>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.<br><br>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.<br><br>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!</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdzdlwqe0006vnp0lsrp5ybn",
|
||||||
|
"name" : "DESA DARMASABA KEMBALI MERAIH PERINGKAT V DALAM AJANG MANGUPURA AWARD",
|
||||||
|
"juara" : "5",
|
||||||
|
"deskripsi" : "<p>Bangga, Darmasaba Berprestasi!</p><p><br>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.</p><p><br>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:<br>- Presentasi Desa pada tanggal 19 Juli 2024<br>- Verifikasi Faktual Lapangan oleh Tim Juri Mangupura Award pada tanggal 5 September 2024</p><p><br>✨ Penghargaan ini bukan hanya sebuah pengakuan, tetapi juga menjadi motivasi bagi Desa Darmasaba untuk terus meningkatkan inovasi, pelayanan, pembangunan serta transformasi digital.<br>Mari bersama kita lanjutkan semangat kolaborasi dan inovasi untuk menjadikan Darmasaba sebagai desa yang semakin unggul dan inspiratif!</p><p><br>#DesaDarmasaba<br>#MangupuraAward2024<br>#BanggaDarmasaba<br>#InovasiDesa<br>#HUTKotaMangupura15<br>#KemajuanDesa<br>#PemdesDarmasaba<br>#PerbekelDarmasaba<br>#DarmasabaBisa<br>#DarmasabaJuara<br> <br>@surya_prabhawa <br>@kecamatanabiansemal <br>@dpmdbadungkab <br>@pemkabbadung<br>@prokompimbadung <br>@seputar_darmasaba</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdzdorcb000avnp0ldlsx73f",
|
||||||
|
"name" : "JEGEG DARMASABA MERAIH JUARA 2 DUTA INVESTASI BADUNG 2024",
|
||||||
|
"juara" : "2",
|
||||||
|
"deskripsi" : "<p>JUARA 2</p><p>DUTA INVESTASI KAB. BADUNG</p><p>TAHUN 2024</p><p>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.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id" : "cmdzdq8ns000dvnp0v917pevj",
|
||||||
|
"name" : "DARMASABA BORONG JUARA BADUNG INVESTMENT AWARD 2024",
|
||||||
|
"juara" : "DARMASABA BORONG JUARA BADUNG INVESTMENT AWARD 2024",
|
||||||
|
"deskripsi" : "<p>DARMASABA MEMBORONG JUARA</p><p>Darmasaba bisa, Darmasaba juara,</p><p>Pemdes Darmasaba tak henti-henti menorehkan prestasi nie Semeton Darmasaba!</p><p>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:</p><p>1. Juara 2 Lomba Video Pendek Potensi dan Peluang Investasi di Desa Kabupaten Badung 2024.</p><p>2. Juara 2 Duta Invenstasi Kabupaten Badung 2024.</p><p>3. Juara Favorit Lomba Video Pendek Potensi dan Peluang Investasi di Desa Kabupaten Badung 2024.</p><p>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.</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
20
prisma/data/landing-page/prestasi-desa/prestasi-desa.json
Normal file
20
prisma/data/landing-page/prestasi-desa/prestasi-desa.json
Normal file
@@ -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": "<p>Tim Bola Voli Putri Dharma Temaja meraih juara 3 dalam Turnamen Bola Voli Mangupura Cup 2024 kategori Putri Se-Bali</p>",
|
||||||
|
"kategoriId": "cmdwrolsl0000vnd3e24q5440"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdwrzs740008vnd329ysez5x",
|
||||||
|
"name": "Prestasi Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung Tahun 2024",
|
||||||
|
"deskripsi": "<p>Prestasi Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung Tahun 2024</p>",
|
||||||
|
"kategoriId": "cmdwrot900001vnd30b5kj96g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdws0sgq000bvnd32o7m94im",
|
||||||
|
"name": "Peringkat 5 Dalam Ajang Bergengsi Mangupura Award",
|
||||||
|
"deskripsi": "<p>Peringkat 5 Dalam Ajang Bergengsi Mangupura Award</p>",
|
||||||
|
"kategoriId": "cmdwrp0pr0002vnd35w6nkjh0"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "edit",
|
|
||||||
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
|
|
||||||
"position": "Perbekel Darmasaba periode 2021-2027"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
32
prisma/data/landing-page/profile/mediaSosial.json
Normal file
32
prisma/data/landing-page/profile/mediaSosial.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cmds8w2q60002vnbe6i8qhkuo",
|
||||||
|
"name": "Telephone Desa Darmasaba",
|
||||||
|
"iconUrl": "081239580000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds8z7u20005vnbegyyvnbk0",
|
||||||
|
"name": "Email Desa Darmasaba",
|
||||||
|
"iconUrl": "desadarmasaba@badungkab.go.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds9023u0008vnbe3oxmhwyf",
|
||||||
|
"name": "Desa Darmasaba",
|
||||||
|
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds90oul000bvnbe2bqkptoi",
|
||||||
|
"name": "Pemerintah Desa Darmasaba",
|
||||||
|
"iconUrl": "https://www.facebook.com/DarmasabaDesaku"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds91i4e000evnbe8gtf1gub",
|
||||||
|
"name": "ddarmasaba",
|
||||||
|
"iconUrl": "https://www.instagram.com/ddarmasaba/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmds92de5000hvnbemlu6sq5x",
|
||||||
|
"name": "desa.darmasaba",
|
||||||
|
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc"
|
||||||
|
}
|
||||||
|
]
|
||||||
7
prisma/data/landing-page/profile/profile.json
Normal file
7
prisma/data/landing-page/profile/profile.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "edit",
|
||||||
|
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
|
||||||
|
"position": "Perbekel Darmasaba periode 2021-2027"
|
||||||
|
}
|
||||||
|
]
|
||||||
50
prisma/data/landing-page/profile/programInovasi.json
Normal file
50
prisma/data/landing-page/profile/programInovasi.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cmdr7039z0002vn5rttctt9hn",
|
||||||
|
"name": "Davest",
|
||||||
|
"description": "Darmasaba Village Festval",
|
||||||
|
"link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdr76nqk0008vn5rdddvcxnr",
|
||||||
|
"name": "Bicara Darmasaba",
|
||||||
|
"description": "Bicara Darmasaba",
|
||||||
|
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdr77vbw000bvn5rvpmoq31s",
|
||||||
|
"name": "Bares",
|
||||||
|
"description": "Darmasaba Recycling Stock/Exchange",
|
||||||
|
"link": "http://darmasaba.desa.id/berita/56722-bares"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdr7bxtp000evn5rmy85wihx",
|
||||||
|
"name": "Sajjana Dharma Raksaka",
|
||||||
|
"description": "Sajjana Dharma Raksaka",
|
||||||
|
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdr7dlnk000hvn5r9lur3z35",
|
||||||
|
"name": "PDKT",
|
||||||
|
"description": "Perangkat Desa Kuat Teknologi",
|
||||||
|
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdr7ftob000mvn5rfhgdtg8v",
|
||||||
|
"name": "GM",
|
||||||
|
"description": "Galah Melah",
|
||||||
|
"link": "https://darmasaba.desa.id/berita/52880-galah-melah"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmdr7glue000pvn5r6onzslju",
|
||||||
|
"name": "Inovasi Desa Darmasaba",
|
||||||
|
"description": "Inovasi Desa Darmasaba",
|
||||||
|
"link": "https://darmasaba.desa.id/produk-lokal-desa"
|
||||||
|
}
|
||||||
|
]
|
||||||
114
prisma/data/landing-page/sdgs-desa/sdgs-desa.json
Normal file
114
prisma/data/landing-page/sdgs-desa/sdgs-desa.json
Normal file
@@ -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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
prisma/data/ppid/ikm/jenis-kelamin/jenis-kelamin.json
Normal file
10
prisma/data/ppid/ikm/jenis-kelamin/jenis-kelamin.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cme8bt5o5000007lb9xp11unb",
|
||||||
|
"name": "Laki-laki"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cme8btctl000107lbh2hocgg8",
|
||||||
|
"name": "Perempuan"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cme8buup6000207lb54q9b0az",
|
||||||
|
"name": "Sangat Baik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cme8bv15o000307lbft9b0vzy",
|
||||||
|
"name": "Baik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cme8bvjvu000507lbgfsveog6",
|
||||||
|
"name": "Kurang Baik"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cme8bvvm6000607lbh6rn2ubm",
|
||||||
|
"name": "Sangat Kurang Baik"
|
||||||
|
}
|
||||||
|
]
|
||||||
14
prisma/data/ppid/ikm/umur-responden/umur-responden.json
Normal file
14
prisma/data/ppid/ikm/umur-responden/umur-responden.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cme8bwgwu000707lbawc6fz3a",
|
||||||
|
"name": "Muda"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cme8hnx09000b07jl3ipifb1k",
|
||||||
|
"name": "Dewasa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cme8ho7dv000c07jlc7lr4b4w",
|
||||||
|
"name": "Lansia"
|
||||||
|
}
|
||||||
|
]
|
||||||
91
prisma/data/ppid/struktur-ppid/pegawai-PPID.json
Normal file
91
prisma/data/ppid/struktur-ppid/pegawai-PPID.json
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440006",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440011",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440012",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440017",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440018",
|
||||||
|
"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": "550e8400-e29b-41d4-a716-446655440021",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
158
prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json
Normal file
158
prisma/data/ppid/struktur-ppid/posisi-organisasi-PPID.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id" : "1",
|
|
||||||
"name" : "Struktur PPID"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
6
prisma/data/user/role.json
Normal file
6
prisma/data/user/role.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cmdpm429r0000vnndkcwslt0h",
|
||||||
|
"name": "warga"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -50,52 +50,56 @@ model AppMenuChild {
|
|||||||
// ========================================= FILE STORAGE ========================================= //
|
// ========================================= FILE STORAGE ========================================= //
|
||||||
|
|
||||||
model FileStorage {
|
model FileStorage {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
realName String
|
realName String
|
||||||
path String
|
path String
|
||||||
mimeType String
|
mimeType String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
link String
|
link String
|
||||||
category String // "image" / "document" / "other"
|
category String // "image" / "document" / "other"
|
||||||
Berita Berita[]
|
Berita Berita[]
|
||||||
PotensiDesa PotensiDesa[]
|
PotensiDesa PotensiDesa[]
|
||||||
Posyandu Posyandu[]
|
Posyandu Posyandu[]
|
||||||
StrukturPPID StrukturPPID[]
|
StrukturPPID StrukturPPID[]
|
||||||
GalleryFoto GalleryFoto[]
|
GalleryFoto GalleryFoto[]
|
||||||
PelayananSuratKeterangan PelayananSuratKeterangan[]
|
Pelapor Pelapor[]
|
||||||
Penghargaan Penghargaan[]
|
Penghargaan Penghargaan[]
|
||||||
ProfileDesaImage ProfileDesaImage[]
|
ProfileDesaImage ProfileDesaImage[]
|
||||||
ProfilePPID ProfilePPID[]
|
ProfilePPID ProfilePPID[]
|
||||||
ProfilPerbekel ProfilPerbekel[]
|
ProfilPerbekel ProfilPerbekel[]
|
||||||
Puskesmas Puskesmas[]
|
Puskesmas Puskesmas[]
|
||||||
ProgramKesehatan ProgramKesehatan[]
|
ProgramKesehatan ProgramKesehatan[]
|
||||||
PenangananDarurat PenangananDarurat[]
|
PenangananDarurat PenangananDarurat[]
|
||||||
KontakDarurat KontakDarurat[]
|
KontakDarurat KontakDarurat[]
|
||||||
InfoWabahPenyakit InfoWabahPenyakit[]
|
InfoWabahPenyakit InfoWabahPenyakit[]
|
||||||
KeamananLingkungan KeamananLingkungan[]
|
KeamananLingkungan KeamananLingkungan[]
|
||||||
MenuTipsKeamanan MenuTipsKeamanan[]
|
MenuTipsKeamanan MenuTipsKeamanan[]
|
||||||
Pelapor Pelapor[]
|
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
|
||||||
PasarDesa PasarDesa[]
|
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
|
||||||
KontakDaruratKeamanan KontakDaruratKeamanan[]
|
PasarDesa PasarDesa[]
|
||||||
KontakItem KontakItem[]
|
KontakDaruratKeamanan KontakDaruratKeamanan[]
|
||||||
Pegawai Pegawai[]
|
KontakItem KontakItem[]
|
||||||
DesaDigital DesaDigital[]
|
Pegawai Pegawai[]
|
||||||
KolaborasiInovasi KolaborasiInovasi[]
|
DesaDigital DesaDigital[]
|
||||||
InfoTekno InfoTekno[]
|
KolaborasiInovasi KolaborasiInovasi[]
|
||||||
PengaduanMasyarakat PengaduanMasyarakat[]
|
InfoTekno InfoTekno[]
|
||||||
KegiatanDesa KegiatanDesa[]
|
PengaduanMasyarakat PengaduanMasyarakat[]
|
||||||
ProgramInovasi ProgramInovasi[]
|
KegiatanDesa KegiatanDesa[]
|
||||||
PejabatDesa PejabatDesa[]
|
ProgramInovasi ProgramInovasi[]
|
||||||
MediaSosial MediaSosial[]
|
PejabatDesa PejabatDesa[]
|
||||||
DesaAntiKorupsi DesaAntiKorupsi[]
|
MediaSosial MediaSosial[]
|
||||||
SDGSDesa SDGSDesa[]
|
DesaAntiKorupsi DesaAntiKorupsi[]
|
||||||
APBDesImage APBDes[] @relation("APBDesImage")
|
SDGSDesa SDGSDesa[]
|
||||||
APBDesFile APBDes[] @relation("APBDesFile")
|
APBDesImage APBDes[] @relation("APBDesImage")
|
||||||
PrestasiDesa PrestasiDesa[]
|
APBDesFile APBDes[] @relation("APBDesFile")
|
||||||
|
PrestasiDesa PrestasiDesa[]
|
||||||
|
DataPerpustakaan DataPerpustakaan[]
|
||||||
|
PegawaiPPID PegawaiPPID[]
|
||||||
|
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -126,15 +130,15 @@ model ProgramInovasi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model MediaSosial {
|
model MediaSosial {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
iconUrl String? @db.VarChar(255)
|
iconUrl String? @db.VarChar(255)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= PROFILE ========================================= //
|
//========================================= PROFILE ========================================= //
|
||||||
@@ -144,8 +148,8 @@ model DesaAntiKorupsi {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
|
kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String
|
kategoriId String
|
||||||
file FileStorage @relation(fields: [fileId], references: [id])
|
file FileStorage? @relation(fields: [fileId], references: [id])
|
||||||
fileId String
|
fileId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -164,30 +168,30 @@ model KategoriDesaAntiKorupsi {
|
|||||||
|
|
||||||
//========================================= SDGS Desa ========================================= //
|
//========================================= SDGS Desa ========================================= //
|
||||||
model SDGSDesa {
|
model SDGSDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
jumlah String
|
jumlah String
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= APBDes ========================================= //
|
//========================================= APBDes ========================================= //
|
||||||
model APBDes {
|
model APBDes {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
jumlah String
|
jumlah String
|
||||||
image FileStorage @relation("APBDesImage", fields: [imageId], references: [id])
|
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
file FileStorage @relation("APBDesFile", fields: [fileId], references: [id])
|
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
||||||
fileId String
|
fileId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= PRESTASI DESA ========================================= //
|
//========================================= PRESTASI DESA ========================================= //
|
||||||
@@ -215,93 +219,112 @@ model KategoriPrestasiDesa {
|
|||||||
PrestasiDesa PrestasiDesa[]
|
PrestasiDesa PrestasiDesa[]
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= INDEKS KEPUASAN MASYARAKAT ========================================= //
|
//========================================= INDEKS KEPUASAAN MASYARAKAT ========================================= //
|
||||||
// Entitas Survey
|
model Responden {
|
||||||
model Survey {
|
id String @id @default(cuid())
|
||||||
id String @id @default(cuid())
|
name String @unique
|
||||||
title String // Judul survei
|
tanggal DateTime // misal: 2025-05-01
|
||||||
totalRespondents Int // Total jumlah responden
|
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
|
||||||
averageScore Float // Rata-rata skor
|
jenisKelaminId String
|
||||||
monthlyStats MonthlyStat[]
|
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
ratingId String
|
||||||
updatedAt DateTime @updatedAt
|
kelompokUmur UmurResponden @relation(fields: [kelompokUmurId], references: [id])
|
||||||
deletedAt DateTime @default(now())
|
kelompokUmurId String
|
||||||
isActive Boolean @default(true)
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entitas Statistik Bulanan
|
model JenisKelaminResponden {
|
||||||
model MonthlyStat {
|
id String @id @default(cuid())
|
||||||
id String @id @default(cuid())
|
name String @unique
|
||||||
month String // Nama bulan (e.g., "Januari", "Februari")
|
createdAt DateTime @default(now())
|
||||||
respondentsCount Int // Jumlah responden per bulan
|
updatedAt DateTime @updatedAt
|
||||||
surveyId String @unique(map: "monthly_stat_survey_id_month_key")
|
deletedAt DateTime @default(now())
|
||||||
survey Survey @relation(fields: [surveyId], references: [id])
|
isActive Boolean @default(true)
|
||||||
AgeStat AgeStat[]
|
Responden Responden[]
|
||||||
ResponseStat ResponseStat[]
|
|
||||||
genderStat genderStat[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entitas Gender
|
model PilihanRatingResponden {
|
||||||
|
id String @id @default(cuid())
|
||||||
model genderStat {
|
name String @unique
|
||||||
id String @id @default(cuid())
|
createdAt DateTime @default(now())
|
||||||
laki Int
|
updatedAt DateTime @updatedAt
|
||||||
perempuan Int
|
deletedAt DateTime @default(now())
|
||||||
percentLaki Float
|
isActive Boolean @default(true)
|
||||||
percentPerempuan Float
|
Responden Responden[]
|
||||||
total Int
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
|
|
||||||
monthlyStatId String?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entitas Age
|
model UmurResponden {
|
||||||
|
id String @id @default(cuid())
|
||||||
model AgeStat {
|
name String @unique
|
||||||
id String @id @default(cuid())
|
createdAt DateTime @default(now())
|
||||||
group String // "18-44", "45+" dll
|
updatedAt DateTime @updatedAt
|
||||||
count Int
|
deletedAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
isActive Boolean @default(true)
|
||||||
updatedAt DateTime @updatedAt
|
Responden Responden[]
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
|
|
||||||
monthlyStatId String?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entitas Response
|
|
||||||
|
|
||||||
model ResponseStat {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
label String // BAIK / BURUK
|
|
||||||
count Int
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
deletedAt DateTime @default(now())
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
|
|
||||||
monthlyStatId String?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU PPID ========================================= //
|
//========================================= MENU PPID ========================================= //
|
||||||
|
|
||||||
//========================================= STRUKTUR PPID ========================================= //
|
//========================================= STRUKTUR PPID ========================================= //
|
||||||
model StrukturPPID {
|
model StrukturPPID {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @db.Text
|
name String @db.Text
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
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?
|
||||||
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
model StrukturOrganisasiPPID {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
posisiOrganisasiId String @db.VarChar(50)
|
||||||
|
pegawaiId String @db.Uuid
|
||||||
|
hubunganOrganisasiId String @db.Uuid
|
||||||
|
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
|
||||||
|
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= VISI MISI PPID ========================================= //
|
// ========================================= VISI MISI PPID ========================================= //
|
||||||
@@ -530,6 +553,19 @@ model ProfilPerbekel {
|
|||||||
isActive Boolean @default(true)
|
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 ========================================= //
|
// ========================================= BERITA ========================================= //
|
||||||
model Berita {
|
model Berita {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -558,17 +594,28 @@ model KategoriBerita {
|
|||||||
|
|
||||||
// ========================================= POTENSI DESA ========================================= //
|
// ========================================= POTENSI DESA ========================================= //
|
||||||
model PotensiDesa {
|
model PotensiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
deskripsi String
|
deskripsi String
|
||||||
kategori String
|
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
kategoriId String?
|
||||||
imageId String
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
content String @db.Text
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
content String @db.Text
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
deletedAt DateTime @default(now())
|
updatedAt DateTime @updatedAt
|
||||||
isActive Boolean @default(true)
|
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 ========================================= //
|
// ========================================= PENGUMUMAN ========================================= //
|
||||||
@@ -621,15 +668,17 @@ model GalleryVideo {
|
|||||||
|
|
||||||
// ========================================= LAYANAN DESA ========================================= //
|
// ========================================= LAYANAN DESA ========================================= //
|
||||||
model PelayananSuratKeterangan {
|
model PelayananSuratKeterangan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
|
||||||
updatedAt DateTime @updatedAt
|
image2Id String?
|
||||||
deletedAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model PelayananTelunjukSaktiDesa {
|
model PelayananTelunjukSaktiDesa {
|
||||||
@@ -666,16 +715,16 @@ model PelayananPendudukNonPermanen {
|
|||||||
|
|
||||||
// ========================================= PENGHARGAAN ========================================= //
|
// ========================================= PENGHARGAAN ========================================= //
|
||||||
model Penghargaan {
|
model Penghargaan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
juara String
|
juara String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= MENU KESEHATAN ========================================= //
|
// ========================================= MENU KESEHATAN ========================================= //
|
||||||
@@ -862,15 +911,42 @@ model PendaftaranJadwalKegiatan {
|
|||||||
|
|
||||||
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
|
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
|
||||||
model DataKematian_Kelahiran {
|
model DataKematian_Kelahiran {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
tahun String
|
kematian Kematian @relation(fields: [kematianId], references: [id])
|
||||||
kematianKasar String
|
kematianId String
|
||||||
kematianBayi String
|
kelahiran Kelahiran @relation(fields: [kelahiranId], references: [id])
|
||||||
kelahiranKasar String
|
kelahiranId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
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 ========================================= //
|
// ========================================= GRAFIK KEPUASAN ========================================= //
|
||||||
@@ -978,16 +1054,17 @@ model DoctorSign {
|
|||||||
|
|
||||||
// ========================================= POSYANDU ========================================= //
|
// ========================================= POSYANDU ========================================= //
|
||||||
model Posyandu {
|
model Posyandu {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
nomor String
|
nomor String
|
||||||
deskripsi String
|
deskripsi String
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
jadwalPelayanan String
|
||||||
imageId String
|
image FileStorage @relation(fields: [imageId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
imageId String
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
deletedAt DateTime @default(now())
|
updatedAt DateTime @updatedAt
|
||||||
isActive Boolean @default(true)
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= PUSKESMAS ========================================= //
|
// ========================================= PUSKESMAS ========================================= //
|
||||||
@@ -1314,8 +1391,9 @@ model PosisiOrganisasi {
|
|||||||
deskripsi String? @db.Text
|
deskripsi String? @db.Text
|
||||||
hierarki Int
|
hierarki Int
|
||||||
|
|
||||||
pegawai Pegawai[]
|
pegawai Pegawai[]
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
||||||
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
|
|
||||||
@@map("posisi_organisasi")
|
@@map("posisi_organisasi")
|
||||||
}
|
}
|
||||||
@@ -1340,7 +1418,8 @@ model Pegawai {
|
|||||||
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
|
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
|
||||||
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
|
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
|
||||||
|
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
||||||
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
|
|
||||||
@@map("pegawai")
|
@@map("pegawai")
|
||||||
}
|
}
|
||||||
@@ -1839,10 +1918,71 @@ model Pengajar {
|
|||||||
isActive Boolean @default(true)
|
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 ========================================= //
|
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
|
||||||
model TujuanProgram {
|
model TujuanProgram {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1852,7 +1992,7 @@ model TujuanProgram {
|
|||||||
|
|
||||||
model ProgramUnggulan {
|
model ProgramUnggulan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1863,7 +2003,7 @@ model ProgramUnggulan {
|
|||||||
// ========================================= BIMBINGAN BELAJAR DESA ========================================= //
|
// ========================================= BIMBINGAN BELAJAR DESA ========================================= //
|
||||||
model TujuanBimbinganBelajarDesa {
|
model TujuanBimbinganBelajarDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1873,7 +2013,7 @@ model TujuanBimbinganBelajarDesa {
|
|||||||
|
|
||||||
model LokasiJadwalBimbinganBelajarDesa {
|
model LokasiJadwalBimbinganBelajarDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1883,7 +2023,7 @@ model LokasiJadwalBimbinganBelajarDesa {
|
|||||||
|
|
||||||
model FasilitasBimbinganBelajarDesa {
|
model FasilitasBimbinganBelajarDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1894,7 +2034,7 @@ model FasilitasBimbinganBelajarDesa {
|
|||||||
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
|
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
|
||||||
model TujuanPendidikanNonFormal {
|
model TujuanPendidikanNonFormal {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1904,7 +2044,7 @@ model TujuanPendidikanNonFormal {
|
|||||||
|
|
||||||
model TempatKegiatan {
|
model TempatKegiatan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -1914,10 +2054,68 @@ model TempatKegiatan {
|
|||||||
|
|
||||||
model JenisProgramYangDiselenggarakan {
|
model JenisProgramYangDiselenggarakan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
nama String
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
|
roleId String
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Role {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
User User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================= 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)
|
||||||
|
}
|
||||||
|
|||||||
345
prisma/seed.ts
345
prisma/seed.ts
@@ -1,4 +1,11 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import profilePejabatDesa from "./data/landing-page/profile/profile.json";
|
||||||
|
import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
|
||||||
|
import programInovasi from "./data/landing-page/profile/programInovasi.json";
|
||||||
|
import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
|
||||||
|
import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json";
|
||||||
|
import apbdes from "./data/landing-page/apbdes/apbdes.json";
|
||||||
|
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
|
||||||
import categoryPengumuman from "./data/category-pengumuman.json";
|
import categoryPengumuman from "./data/category-pengumuman.json";
|
||||||
import kategoriBerita from "./data/kategori-berita.json";
|
import kategoriBerita from "./data/kategori-berita.json";
|
||||||
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
||||||
@@ -9,7 +16,9 @@ import potensi from "./data/list-potensi.json";
|
|||||||
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
|
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
|
||||||
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
|
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
|
||||||
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
|
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
|
||||||
import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json";
|
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.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 pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
|
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
|
||||||
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
|
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
|
||||||
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
|
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
|
||||||
@@ -21,25 +30,137 @@ import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
|
|||||||
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
|
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
|
||||||
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
|
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
|
||||||
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
|
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
|
||||||
import detailDataPengangguran from './data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json';
|
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
|
||||||
import tujuanEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json';
|
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||||
import materiEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json';
|
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||||
import contohEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json';
|
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
|
||||||
import nilaiKonservasiAdat from './data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json';
|
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||||
import bentukKonservasiBerdasarkanAdat from './data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json';
|
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
||||||
import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json';
|
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||||
import profilePejabatDesa from './data/landing-page/profile.json';
|
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||||
import tujuanProgram from './data/pendidikan/program-pendidikan-anak/tujuan-program.json';
|
import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
||||||
import tujuanProgram2 from './data/pendidikan/pendidikan-non-formal/tujuan-program2.json';
|
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||||
import programUnggulan from './data/pendidikan/program-pendidikan-anak/program-unggulan.json';
|
import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
|
||||||
import tujuanBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json';
|
import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
|
||||||
import lokasiJadwalBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json';
|
import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
|
||||||
import fasilitasBimbinganBelajarDesa from './data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json';
|
import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
|
||||||
import tempatKegiatan from './data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json';
|
import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
|
||||||
import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json';
|
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
|
||||||
|
import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
|
||||||
|
import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// =========== LANDING PAGE ===========
|
||||||
|
// =========== PROFILE ===========
|
||||||
|
for (const p of profilePejabatDesa) {
|
||||||
|
await prisma.pejabatDesa.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
position: p.position,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
position: p.position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ profilePejabatDesa seeded without imageId (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========== PROGRAM INOVASI ===========
|
||||||
|
for (const p of programInovasi) {
|
||||||
|
await prisma.programInovasi.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
description: p.description,
|
||||||
|
link: p.link,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
description: p.description,
|
||||||
|
link: p.link,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
iconUrl: p.iconUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("media sosial 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 ...");
|
||||||
|
|
||||||
|
// =========== LAYANAN ===========
|
||||||
for (const l of layanan) {
|
for (const l of layanan) {
|
||||||
await prisma.layanan.upsert({
|
await prisma.layanan.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -56,6 +177,46 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
|
|
||||||
console.log("layanan success ...");
|
console.log("layanan success ...");
|
||||||
|
|
||||||
|
// =========== SDGSDesa ===========
|
||||||
|
for (const l of sdgsDesa) {
|
||||||
|
await prisma.sDGSDesa.upsert({
|
||||||
|
where: {
|
||||||
|
name: l.name,
|
||||||
|
jumlah: l.jumlah,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: l.name,
|
||||||
|
jumlah: l.jumlah,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: l.name,
|
||||||
|
jumlah: l.jumlah,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("sdgs desa success ...");
|
||||||
|
|
||||||
|
// =========== APBDes ===========
|
||||||
|
for (const l of apbdes) {
|
||||||
|
await prisma.aPBDes.upsert({
|
||||||
|
where: {
|
||||||
|
name: l.name,
|
||||||
|
jumlah: l.jumlah,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: l.name,
|
||||||
|
jumlah: l.jumlah,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: l.name,
|
||||||
|
jumlah: l.jumlah,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("sdgs desa success ...");
|
||||||
|
|
||||||
for (const l of sejarahDesa) {
|
for (const l of sejarahDesa) {
|
||||||
await prisma.sejarahDesa.upsert({
|
await prisma.sejarahDesa.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -133,7 +294,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ profilePerbekel seeded without imageId (editable later via UI)");
|
console.log(
|
||||||
|
"✅ profilePerbekel seeded without imageId (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
for (const l of visiMisiDesa) {
|
for (const l of visiMisiDesa) {
|
||||||
await prisma.visiMisiDesa.upsert({
|
await prisma.visiMisiDesa.upsert({
|
||||||
@@ -154,6 +317,43 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
|
|
||||||
console.log("visi misi desa success ...");
|
console.log("visi misi desa success ...");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.posisiOrganisasiPPID.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: p,
|
||||||
|
create: p,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Posisi organisasi berhasil");
|
||||||
|
|
||||||
|
// 2. Seed Pegawai
|
||||||
|
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");
|
||||||
|
|
||||||
for (const l of pelayananPerizinanBerusaha) {
|
for (const l of pelayananPerizinanBerusaha) {
|
||||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
await prisma.pelayananPerizinanBerusaha.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -193,22 +393,6 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
}
|
}
|
||||||
console.log("pelayanan penduduk non permanen success ...");
|
console.log("pelayanan penduduk non permanen success ...");
|
||||||
|
|
||||||
for (const s of strukturPPID) {
|
|
||||||
await prisma.strukturPPID.upsert({
|
|
||||||
where: {
|
|
||||||
id: s.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: s.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: s.id,
|
|
||||||
name: s.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("struktur ppid success ...");
|
|
||||||
|
|
||||||
for (const p of potensi) {
|
for (const p of potensi) {
|
||||||
await prisma.potensi.upsert({
|
await prisma.potensi.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -344,6 +528,54 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
}
|
}
|
||||||
console.log("visi misi PPID success ...");
|
console.log("visi misi PPID 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 v of dasarHukumPPID) {
|
for (const v of dasarHukumPPID) {
|
||||||
await prisma.dasarHukumPPID.upsert({
|
await prisma.dasarHukumPPID.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -378,7 +610,6 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
}
|
}
|
||||||
console.log("kategori produk success ...");
|
console.log("kategori produk success ...");
|
||||||
|
|
||||||
|
|
||||||
for (const p of posisiOrganisasi) {
|
for (const p of posisiOrganisasi) {
|
||||||
await prisma.posisiOrganisasi.upsert({
|
await prisma.posisiOrganisasi.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -567,7 +798,6 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
|
|
||||||
console.log("bentuk konservasi berdasarkan adat success ...");
|
console.log("bentuk konservasi berdasarkan adat success ...");
|
||||||
|
|
||||||
|
|
||||||
for (const n of nilaiKonservasiAdat) {
|
for (const n of nilaiKonservasiAdat) {
|
||||||
await prisma.nilaiKonservasiAdat.upsert({
|
await prisma.nilaiKonservasiAdat.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -587,22 +817,6 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
|
|
||||||
console.log("nilai konservasi adat success ...");
|
console.log("nilai konservasi adat success ...");
|
||||||
|
|
||||||
for (const p of profilePejabatDesa) {
|
|
||||||
await prisma.pejabatDesa.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
position: p.position,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
position: p.position,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ profilePejabatDesa seeded without imageId (editable later via UI)");
|
|
||||||
|
|
||||||
for (const t of tujuanProgram) {
|
for (const t of tujuanProgram) {
|
||||||
await prisma.tujuanProgram.upsert({
|
await prisma.tujuanProgram.upsert({
|
||||||
where: { id: t.id },
|
where: { id: t.id },
|
||||||
@@ -649,7 +863,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ tujuan bimbingan belajar desa seeded (editable later via UI)");
|
console.log(
|
||||||
|
"✅ tujuan bimbingan belajar desa seeded (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
for (const t of lokasiJadwalBimbinganBelajarDesa) {
|
for (const t of lokasiJadwalBimbinganBelajarDesa) {
|
||||||
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
|
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
|
||||||
@@ -665,7 +881,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)");
|
console.log(
|
||||||
|
"✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
for (const t of fasilitasBimbinganBelajarDesa) {
|
for (const t of fasilitasBimbinganBelajarDesa) {
|
||||||
await prisma.fasilitasBimbinganBelajarDesa.upsert({
|
await prisma.fasilitasBimbinganBelajarDesa.upsert({
|
||||||
@@ -681,7 +899,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
for (const t of tujuanProgram2) {
|
for (const t of tujuanProgram2) {
|
||||||
await prisma.tujuanPendidikanNonFormal.upsert({
|
await prisma.tujuanPendidikanNonFormal.upsert({
|
||||||
@@ -697,7 +917,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
for (const t of tempatKegiatan) {
|
for (const t of tempatKegiatan) {
|
||||||
await prisma.tempatKegiatan.upsert({
|
await prisma.tempatKegiatan.upsert({
|
||||||
@@ -713,7 +935,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
|
||||||
|
);
|
||||||
|
|
||||||
for (const t of jenisProgramYangDiselenggarakan) {
|
for (const t of jenisProgramYangDiselenggarakan) {
|
||||||
await prisma.jenisProgramYangDiselenggarakan.upsert({
|
await prisma.jenisProgramYangDiselenggarakan.upsert({
|
||||||
@@ -729,8 +953,9 @@ import jenisProgramYangDiselenggarakan from './data/pendidikan/pendidikan-non-fo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("✅ fasilitas bimbingan belajar desa seeded (editable later via UI)");
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
|
||||||
|
);
|
||||||
})()
|
})()
|
||||||
.then(() => prisma.$disconnect())
|
.then(() => prisma.$disconnect())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -22,19 +23,6 @@ const defaultForm = {
|
|||||||
kategoriBeritaId: "",
|
kategoriBeritaId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Kategori proxy
|
|
||||||
const category = proxy({
|
|
||||||
findMany: {
|
|
||||||
data: [] as Prisma.KategoriBeritaGetPayload<{ omit: { isActive: true } }>[],
|
|
||||||
async load() {
|
|
||||||
const res = await ApiFetch.api.desa.berita.category["find-many"].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
category.findMany.data = res.data?.data ?? [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Berita proxy
|
// 4. Berita proxy
|
||||||
const berita = proxy({
|
const berita = proxy({
|
||||||
create: {
|
create: {
|
||||||
@@ -71,6 +59,8 @@ const berita = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// State untuk berita utama (hanya 1)
|
||||||
|
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.BeritaGetPayload<{
|
| Prisma.BeritaGetPayload<{
|
||||||
@@ -83,38 +73,43 @@ const berita = proxy({
|
|||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
search: "",
|
||||||
async load(page = 1, limit = 10) {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
berita.findMany.loading = true;
|
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
berita.findMany.page = page;
|
berita.findMany.page = page;
|
||||||
try {
|
berita.findMany.search = search;
|
||||||
const res = await ApiFetch.api.desa.berita["find-many"].get({
|
|
||||||
query: {
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
berita.findMany.data = res.data.data ?? [];
|
berita.findMany.data = res.data.data ?? [];
|
||||||
berita.findMany.totalPages = res.data.totalPages ?? 1;
|
berita.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
berita.findMany.data = [];
|
||||||
|
berita.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Gagal fetch berita paginated:", err);
|
console.error("Gagal fetch berita paginated:", err);
|
||||||
|
berita.findMany.data = [];
|
||||||
|
berita.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
berita.findMany.loading = false;
|
berita.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as Prisma.BeritaGetPayload<{
|
||||||
| Prisma.BeritaGetPayload<{
|
include: {
|
||||||
include: {
|
image: true;
|
||||||
image: true;
|
kategoriBerita: true;
|
||||||
kategoriBerita: true;
|
};
|
||||||
};
|
}> | null,
|
||||||
}> | null,
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/desa/berita/${id}`);
|
const res = await fetch(`/api/desa/berita/${id}`);
|
||||||
@@ -122,11 +117,11 @@ const berita = proxy({
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
berita.findUnique.data = data.data ?? null;
|
berita.findUnique.data = data.data ?? null;
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to fetch berita:', res.statusText);
|
console.error("Failed to fetch berita:", res.statusText);
|
||||||
berita.findUnique.data = null;
|
berita.findUnique.data = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching berita:', error);
|
console.error("Error fetching berita:", error);
|
||||||
berita.findUnique.data = null;
|
berita.findUnique.data = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -140,14 +135,14 @@ const berita = proxy({
|
|||||||
berita.delete.loading = true;
|
berita.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/desa/berita/delete/${id}`, {
|
const response = await fetch(`/api/desa/berita/delete/${id}`, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
if (response.ok && result?.success) {
|
||||||
toast.success(result.message || "Berita berhasil dihapus");
|
toast.success(result.message || "Berita berhasil dihapus");
|
||||||
await berita.findMany.load(); // refresh list
|
await berita.findMany.load(); // refresh list
|
||||||
@@ -172,21 +167,21 @@ const berita = proxy({
|
|||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/desa/berita/${id}`, {
|
const response = await fetch(`/api/desa/berita/${id}`, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
@@ -203,7 +198,9 @@ const berita = proxy({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading berita:", error);
|
console.error("Error loading berita:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -217,14 +214,14 @@ const berita = proxy({
|
|||||||
toast.error(err);
|
toast.error(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
berita.edit.loading = true;
|
berita.edit.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/desa/berita/${this.id}`, {
|
const response = await fetch(`/api/desa/berita/${this.id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
judul: this.form.judul,
|
judul: this.form.judul,
|
||||||
@@ -234,14 +231,16 @@ const berita = proxy({
|
|||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success("Berhasil update berita");
|
toast.success("Berhasil update berita");
|
||||||
await berita.findMany.load(); // refresh list
|
await berita.findMany.load(); // refresh list
|
||||||
@@ -251,7 +250,11 @@ const berita = proxy({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating berita:", error);
|
console.error("Error updating berita:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update berita");
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update berita"
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
berita.edit.loading = false;
|
berita.edit.loading = false;
|
||||||
@@ -271,21 +274,22 @@ const berita = proxy({
|
|||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
// findFirst.load()
|
||||||
|
async load(kategori?: string) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.desa.berita["find-first"].get();
|
const res = await ApiFetch.api.desa.berita["find-first"].get({
|
||||||
|
query: kategori ? { kategori } : {},
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
// Add type assertion to ensure type safety
|
this.data = res.data.data || null;
|
||||||
berita.findFirst.data = res.data.data as Prisma.BeritaGetPayload<{
|
} else {
|
||||||
include: {
|
this.data = null;
|
||||||
image: true;
|
|
||||||
kategoriBerita: true;
|
|
||||||
};
|
|
||||||
}> | null;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Gagal fetch berita terbaru:", err);
|
console.error("Gagal fetch berita terbaru:", err);
|
||||||
|
this.data = null;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
@@ -299,7 +303,7 @@ const berita = proxy({
|
|||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -313,15 +317,227 @@ const berita = proxy({
|
|||||||
this.loading = false;
|
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;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
kategoriBerita.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
|
// 5. State global
|
||||||
const stateDashboardBerita = proxy({
|
const stateDashboardBerita = proxy({
|
||||||
category,
|
kategoriBerita,
|
||||||
berita,
|
berita,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -68,10 +69,34 @@ const foto = proxy({
|
|||||||
};
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.desa.gallery.foto["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
foto.findMany.data = res.data?.data ?? [];
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -215,6 +240,28 @@ const foto = proxy({
|
|||||||
foto.update.form = { ...defaultFormFoto };
|
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({
|
const video = proxy({
|
||||||
@@ -257,10 +304,34 @@ const video = proxy({
|
|||||||
};
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.desa.gallery.video["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
video.findMany.data = res.data?.data ?? [];
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -8,12 +9,14 @@ const templateSuratKeteranganForm = z.object({
|
|||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
imageId: z.string().nonempty(),
|
imageId: z.string().nonempty(),
|
||||||
|
image2Id: z.string().nonempty(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const suratKeteranganForm = {
|
const suratKeteranganForm = {
|
||||||
name: "",
|
name: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
|
image2Id: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const telunjukSaktiDesaForm = {
|
const telunjukSaktiDesaForm = {
|
||||||
@@ -105,15 +108,38 @@ const suratKeterangan = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.PelayananSuratKeteranganGetPayload<{
|
data: null as any[] | null,
|
||||||
include: { image: true };
|
page: 1,
|
||||||
}>[],
|
totalPages: 1,
|
||||||
async load() {
|
total: 0,
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
loading: false,
|
||||||
"find-many"
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
].get();
|
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
||||||
if (res.status === 200) {
|
suratKeterangan.findMany.page = page;
|
||||||
suratKeterangan.findMany.data = res.data?.data ?? [];
|
try {
|
||||||
|
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -121,6 +147,7 @@ const suratKeterangan = proxy({
|
|||||||
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
|
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
image: true;
|
image: true;
|
||||||
|
image2: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
@@ -202,6 +229,7 @@ const suratKeterangan = proxy({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi,
|
||||||
imageId: data.imageId || "",
|
imageId: data.imageId || "",
|
||||||
|
image2Id: data.image2Id || "",
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -238,6 +266,7 @@ const suratKeterangan = proxy({
|
|||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
deskripsi: this.form.deskripsi,
|
deskripsi: this.form.deskripsi,
|
||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
|
image2Id: this.form.image2Id,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -307,15 +336,38 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.PelayananTelunjukSaktiDesaGetPayload<{
|
data: null as any[] | null,
|
||||||
omit: { isActive: true };
|
page: 1,
|
||||||
}>[],
|
totalPages: 1,
|
||||||
async load() {
|
total: 0,
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
loading: false,
|
||||||
"find-many"
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
].get();
|
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
if (res.status === 200) {
|
pelayananTelunjukSaktiDesa.findMany.page = page;
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = res.data?.data ?? [];
|
try {
|
||||||
|
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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 telunjuk sakti desa:", res.data?.message);
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading telunjuk sakti desa:", error);
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -5,10 +6,10 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(1).max(50),
|
name: z.string().min(1).max(5000),
|
||||||
juara: z.string().min(1).max(50),
|
juara: z.string().min(1).max(5000),
|
||||||
deskripsi: z.string().min(1).max(5000),
|
deskripsi: z.string().min(1).max(5000),
|
||||||
imageId: z.string().min(1).max(50),
|
imageId: z.string().min(1).max(5000),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
@@ -50,17 +51,38 @@ const penghargaanState = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as any[] | null,
|
||||||
| Prisma.PenghargaanGetPayload<{
|
page: 1,
|
||||||
include: {
|
totalPages: 1,
|
||||||
image: true;
|
total: 0,
|
||||||
};
|
loading: false,
|
||||||
}>[]
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
| null,
|
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
||||||
async load() {
|
penghargaanState.findMany.page = page;
|
||||||
const res = await ApiFetch.api.desa.penghargaan["find-many"].get();
|
try {
|
||||||
if (res.status === 200) {
|
const res = await ApiFetch.api.desa.penghargaan[
|
||||||
penghargaanState.findMany.data = res.data?.data ?? [];
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,228 @@ import { toast } from "react-toastify";
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
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;
|
||||||
|
};
|
||||||
|
})[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
category.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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({
|
const templateFormPengumuman = z.object({
|
||||||
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
@@ -12,33 +234,15 @@ const templateFormPengumuman = z.object({
|
|||||||
categoryPengumumanId: z.string().nonempty(),
|
categoryPengumumanId: z.string().nonempty(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const category = proxy({
|
const defaultForm = {
|
||||||
findMany: {
|
judul: "",
|
||||||
data: null as
|
deskripsi: "",
|
||||||
| null
|
content: "",
|
||||||
| Prisma.CategoryPengumumanGetPayload<{ omit: { isActive: true } }>[],
|
categoryPengumumanId: "",
|
||||||
async load() {
|
};
|
||||||
const res = await ApiFetch.api.desa.pengumuman.category[
|
|
||||||
"find-many"
|
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
category.findMany.data = (res.data?.data as any) ?? [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type PengumumanForm = Prisma.PengumumanGetPayload<{
|
|
||||||
select: {
|
|
||||||
judul: true;
|
|
||||||
deskripsi: true;
|
|
||||||
content: true;
|
|
||||||
categoryPengumumanId: true;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
const pengumuman = proxy({
|
const pengumuman = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {} as PengumumanForm,
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templateFormPengumuman.safeParse(pengumuman.create.form);
|
const cek = templateFormPengumuman.safeParse(pengumuman.create.form);
|
||||||
@@ -74,11 +278,35 @@ const pengumuman = proxy({
|
|||||||
};
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.desa.pengumuman["find-many"].get();
|
totalPages: 1,
|
||||||
console.log(res);
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
pengumuman.findMany.data = res.data?.data ?? [];
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -112,7 +340,7 @@ const pengumuman = proxy({
|
|||||||
try {
|
try {
|
||||||
pengumuman.delete.loading = true;
|
pengumuman.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/desa/pengumuman/delete/${id}`, {
|
const response = await fetch(`/api/desa/pengumuman/del/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -135,9 +363,9 @@ const pengumuman = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
edit: {
|
||||||
id: "",
|
id: "",
|
||||||
form: {} as PengumumanForm,
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
@@ -153,6 +381,7 @@ const pengumuman = proxy({
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -168,20 +397,21 @@ const pengumuman = proxy({
|
|||||||
content: data.content,
|
content: data.content,
|
||||||
categoryPengumumanId: data.categoryPengumumanId || "",
|
categoryPengumumanId: data.categoryPengumumanId || "",
|
||||||
};
|
};
|
||||||
return data;
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.message || "Gagal mengambil data pengumuman");
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
console.error("Error loading pengumuman:", error);
|
||||||
toast.error("Terjadi kesalahan saat mengambil data pengumuman");
|
toast.error(
|
||||||
} finally {
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
pengumuman.update.loading = false;
|
);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
const cek = templateFormPengumuman.safeParse(pengumuman.update.form);
|
const cek = templateFormPengumuman.safeParse(pengumuman.edit.form);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
const err = `[${cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}`)
|
.map((v) => `${v.path.join(".")}`)
|
||||||
@@ -191,7 +421,7 @@ const pengumuman = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pengumuman.update.loading = true;
|
pengumuman.edit.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/desa/pengumuman/${this.id}`, {
|
const response = await fetch(`/api/desa/pengumuman/${this.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -202,7 +432,7 @@ const pengumuman = proxy({
|
|||||||
judul: this.form.judul,
|
judul: this.form.judul,
|
||||||
deskripsi: this.form.deskripsi,
|
deskripsi: this.form.deskripsi,
|
||||||
content: this.form.content,
|
content: this.form.content,
|
||||||
categoryPengumumanId: this.form.categoryPengumumanId,
|
categoryPengumumanId: this.form.categoryPengumumanId || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,9 +461,14 @@ const pengumuman = proxy({
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
pengumuman.update.loading = false;
|
pengumuman.edit.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
pengumuman.edit.id = "";
|
||||||
|
pengumuman.edit.form = { ...defaultForm };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
findFirst: {
|
findFirst: {
|
||||||
data: null as Prisma.PengumumanGetPayload<{
|
data: null as Prisma.PengumumanGetPayload<{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -5,219 +6,470 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(1).max(50),
|
name: z.string().min(1).max(5000),
|
||||||
deskripsi: z.string().min(1).max(5000),
|
deskripsi: z.string().min(1).max(5000),
|
||||||
kategori: z.string().min(1).max(50),
|
kategoriId: z.string().min(1).max(50),
|
||||||
imageId: z.string().min(1).max(50),
|
imageId: z.string().min(1).max(50),
|
||||||
content: z.string().min(1).max(5000),
|
content: z.string().min(1).max(5000),
|
||||||
})
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
name: "",
|
name: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
kategori: "",
|
kategoriId: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
content: "",
|
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,
|
||||||
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
|
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
|
potensiDesa.findMany.page = page;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
kategoriPotensi.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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({
|
const potensiDesaState = proxy({
|
||||||
create: {
|
potensiDesa,
|
||||||
form: { ...defaultForm },
|
kategoriPotensi,
|
||||||
loading: false,
|
});
|
||||||
async create() {
|
|
||||||
const cek = templateForm.safeParse(potensiDesaState.create.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
potensiDesaState.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.desa.potensi["create"].post(potensiDesaState.create.form);
|
|
||||||
if (res.status === 200) {
|
|
||||||
potensiDesaState.findMany.load();
|
|
||||||
return toast.success("Potensi berhasil disimpan!");
|
|
||||||
}
|
|
||||||
return toast.error("Gagal menyimpan potensi");
|
|
||||||
} catch (error) {
|
|
||||||
console.log((error as Error).message);
|
|
||||||
} finally {
|
|
||||||
potensiDesaState.create.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findMany: {
|
|
||||||
data: null as
|
|
||||||
| Prisma.PotensiDesaGetPayload<{
|
|
||||||
include: {
|
|
||||||
image: true;
|
|
||||||
}
|
|
||||||
}>[]
|
|
||||||
| null,
|
|
||||||
async load() {
|
|
||||||
const res = await ApiFetch.api.desa.potensi["find-many"].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
potensiDesaState.findMany.data = res.data?.data ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findUnique: {
|
|
||||||
data: null as
|
|
||||||
| Prisma.PotensiDesaGetPayload<{
|
|
||||||
include: {
|
|
||||||
image: true;
|
|
||||||
}
|
|
||||||
}> | null,
|
|
||||||
async load(id: string) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/desa/potensi/${id}`);
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
potensiDesaState.findUnique.data = data.data ?? null;
|
|
||||||
} else {
|
|
||||||
console.error('Failed to fetch potensi:', res.statusText);
|
|
||||||
potensiDesaState.findUnique.data = null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching potensi:', error);
|
|
||||||
potensiDesaState.findUnique.data = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
loading: false,
|
|
||||||
async byId(id: string) {
|
|
||||||
if (!id) return toast.warn("ID tidak valid");
|
|
||||||
|
|
||||||
try {
|
|
||||||
potensiDesaState.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 potensiDesaState.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 {
|
|
||||||
potensiDesaState.delete.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
id: "",
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
async load(id: string) {
|
export default potensiDesaState;
|
||||||
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,
|
|
||||||
kategori: data.kategori,
|
|
||||||
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(potensiDesaState.edit.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
|
||||||
toast.error(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
potensiDesaState.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,
|
|
||||||
kategori: this.form.kategori,
|
|
||||||
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 potensiDesaState.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 {
|
|
||||||
potensiDesaState.edit.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
potensiDesaState.edit.id = "";
|
|
||||||
potensiDesaState.edit.form = { ...defaultForm };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default potensiDesaState
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
|
||||||
// ========================================= SEJARAH DESA ========================================= //
|
// ========================================= SEJARAH DESA ========================================= //
|
||||||
const sejarahDesaForm = z.object({
|
const sejarahDesaForm = z.object({
|
||||||
@@ -106,16 +108,13 @@ const sejarahDesa = proxy({
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/desa/profile/sejarah/${this.id}`, {
|
||||||
`/api/desa/profile/sejarah/${this.id}`,
|
method: "PUT",
|
||||||
{
|
headers: {
|
||||||
method: "PUT",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify(this.form),
|
||||||
},
|
});
|
||||||
body: JSON.stringify(this.form),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
@@ -409,16 +408,13 @@ const lambangDesa = proxy({
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/desa/profile/lambang/${this.id}`, {
|
||||||
`/api/desa/profile/lambang/${this.id}`,
|
method: "PUT",
|
||||||
{
|
headers: {
|
||||||
method: "PUT",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify(this.form),
|
||||||
},
|
});
|
||||||
body: JSON.stringify(this.form),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
@@ -588,14 +584,11 @@ const maskotDesa = proxy({
|
|||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/desa/profile/maskot/${this.id}`, {
|
||||||
`/api/desa/profile/maskot/${this.id}`,
|
method: "PUT",
|
||||||
{
|
headers: { "Content-Type": "application/json" },
|
||||||
method: "PUT",
|
body: JSON.stringify(this.form),
|
||||||
headers: { "Content-Type": "application/json" },
|
});
|
||||||
body: JSON.stringify(this.form),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -818,12 +811,247 @@ const profilPerbekel = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//========================================= 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({
|
const stateProfileDesa = proxy({
|
||||||
lambangDesa,
|
lambangDesa,
|
||||||
maskotDesa,
|
maskotDesa,
|
||||||
profilPerbekel,
|
profilPerbekel,
|
||||||
visiMisiDesa,
|
visiMisiDesa,
|
||||||
sejarahDesa,
|
sejarahDesa,
|
||||||
|
mantanPerbekel,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default stateProfileDesa;
|
export default stateProfileDesa;
|
||||||
|
|||||||
@@ -5,176 +5,190 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateJumlahPendudukMiskin = z.object({
|
const templateJumlahPendudukMiskin = z.object({
|
||||||
year: z.number().min(1, "Data tahun harus diisi"),
|
year: z.number().min(1, "Data tahun harus diisi"),
|
||||||
totalPoorPopulation: z.number().min(1, "Data total penduduk miskin harus diisi"),
|
totalPoorPopulation: z
|
||||||
|
.number()
|
||||||
|
.min(1, "Data total penduduk miskin harus diisi"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{
|
type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
id: true;
|
id: true;
|
||||||
year: true;
|
year: true;
|
||||||
totalPoorPopulation: true;
|
totalPoorPopulation: true;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const defaultForm: Omit<JumlahPendudukMiskin, 'id'> & { id?: string } = {
|
const defaultForm: Omit<JumlahPendudukMiskin, "id"> & { id?: string } = {
|
||||||
year: 0,
|
year: 0,
|
||||||
totalPoorPopulation: 0,
|
totalPoorPopulation: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const jumlahPendudukMiskin = proxy({
|
const jumlahPendudukMiskin = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: defaultForm,
|
form: defaultForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templateJumlahPendudukMiskin.safeParse(
|
const cek = templateJumlahPendudukMiskin.safeParse(
|
||||||
jumlahPendudukMiskin.create.form
|
jumlahPendudukMiskin.create.form
|
||||||
);
|
);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
const err = `[${cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}`)
|
.map((v) => `${v.path.join(".")}`)
|
||||||
.join("\n")}] required`;
|
.join("\n")}] required`;
|
||||||
return toast.error(err);
|
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,
|
|
||||||
loading: false,
|
|
||||||
async load() {
|
|
||||||
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
|
|
||||||
"find-many"
|
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
try {
|
||||||
export default jumlahPendudukMiskin
|
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,
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templatePersentase = z.object({
|
//persentase kelahiran kematian
|
||||||
|
|
||||||
|
const templatePersentaseKelahiran = z.object({
|
||||||
tahun: z.string().min(4, "Tahun harus diisi"),
|
tahun: z.string().min(4, "Tahun harus diisi"),
|
||||||
kematianKasar: z.string().min(1, "Kematian kasar harus diisi"),
|
kematianKasar: z.string().min(1, "Kematian kasar harus diisi"),
|
||||||
kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"),
|
kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"),
|
||||||
@@ -13,18 +16,14 @@ const templatePersentase = z.object({
|
|||||||
|
|
||||||
type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
|
type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
tahun: true;
|
kematianId: true;
|
||||||
kematianKasar: true;
|
kelahiranId: true;
|
||||||
kelahiranKasar: true;
|
|
||||||
kematianBayi: true;
|
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const defaultForm: Persentase = {
|
const defaultForm: Persentase = {
|
||||||
tahun: "",
|
kematianId: "",
|
||||||
kematianKasar: "",
|
kelahiranId: "",
|
||||||
kelahiranKasar: "",
|
|
||||||
kematianBayi: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const persentasekelahiran = proxy({
|
const persentasekelahiran = proxy({
|
||||||
@@ -32,7 +31,9 @@ const persentasekelahiran = proxy({
|
|||||||
form: defaultForm,
|
form: defaultForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templatePersentase.safeParse(persentasekelahiran.create.form);
|
const cek = templatePersentaseKelahiran.safeParse(
|
||||||
|
persentasekelahiran.create.form
|
||||||
|
);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
const err = `[${cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}`)
|
.map((v) => `${v.path.join(".")}`)
|
||||||
@@ -47,7 +48,7 @@ const persentasekelahiran = proxy({
|
|||||||
].post(persentasekelahiran.create.form);
|
].post(persentasekelahiran.create.form);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.data;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Success create");
|
||||||
persentasekelahiran.create.form = { ...defaultForm };
|
persentasekelahiran.create.form = { ...defaultForm };
|
||||||
@@ -69,21 +70,51 @@ const persentasekelahiran = proxy({
|
|||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.DataKematian_KelahiranGetPayload<{
|
| Prisma.DataKematian_KelahiranGetPayload<{
|
||||||
omit: { isActive: true };
|
include: {
|
||||||
|
kematian: true;
|
||||||
|
kelahiran: true;
|
||||||
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
|
totalPages: 1,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
persentasekelahiran.findMany.data = res.data?.data ?? [];
|
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: {
|
findUnique: {
|
||||||
data: null as Prisma.DataKematian_KelahiranGetPayload<{
|
data: null as Prisma.DataKematian_KelahiranGetPayload<{
|
||||||
omit: { isActive: true };
|
include: {
|
||||||
|
kematian: true;
|
||||||
|
kelahiran: true;
|
||||||
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
@@ -114,13 +145,11 @@ const persentasekelahiran = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
tahun: this.form.tahun,
|
kematianId: this.form.kematianId,
|
||||||
kematianKasar: this.form.kematianKasar,
|
kelahiranId: this.form.kelahiranId,
|
||||||
kelahiranKasar: this.form.kelahiranKasar,
|
|
||||||
kematianBayi: this.form.kematianBayi,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cek = templatePersentase.safeParse(formData);
|
const cek = templatePersentaseKelahiran.safeParse(formData);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
const err = `[${cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}`)
|
.map((v) => `${v.path.join(".")}`)
|
||||||
@@ -197,4 +226,521 @@ const persentasekelahiran = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default persentasekelahiran;
|
// 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;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -9,6 +10,7 @@ const templateForm = z.object({
|
|||||||
nomor: z.string().min(1, { message: "Nomor is required" }),
|
nomor: z.string().min(1, { message: "Nomor is required" }),
|
||||||
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
|
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
|
||||||
imageId: z.string().nonempty(),
|
imageId: z.string().nonempty(),
|
||||||
|
jadwalPelayanan: z.string().min(1, { message: "Jadwal Pelayanan is required" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
@@ -16,6 +18,7 @@ const defaultForm = {
|
|||||||
nomor: "",
|
nomor: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
|
jadwalPelayanan: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const posyandustate = proxy({
|
const posyandustate = proxy({
|
||||||
@@ -50,19 +53,43 @@ const posyandustate = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.PosyanduGetPayload<{
|
| Prisma.PosyanduGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
image: true;
|
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) {
|
||||||
| null,
|
console.error("Gagal fetch posyandu paginated:", err);
|
||||||
async load() {
|
posyandustate.findMany.data = [];
|
||||||
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get();
|
posyandustate.findMany.totalPages = 1;
|
||||||
if (res.status === 200) {
|
} finally {
|
||||||
posyandustate.findMany.data = res.data?.data ?? [];
|
posyandustate.findMany.loading = false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
@@ -148,6 +175,7 @@ const posyandustate = proxy({
|
|||||||
nomor: data.nomor,
|
nomor: data.nomor,
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi,
|
||||||
imageId: data.imageId || "",
|
imageId: data.imageId || "",
|
||||||
|
jadwalPelayanan: data.jadwalPelayanan || "",
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -181,6 +209,7 @@ const posyandustate = proxy({
|
|||||||
nomor: this.form.nomor,
|
nomor: this.form.nomor,
|
||||||
deskripsi: this.form.deskripsi,
|
deskripsi: this.form.deskripsi,
|
||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
|
jadwalPelayanan: this.form.jadwalPelayanan,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -54,20 +55,38 @@ const desaAntikorupsi = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Array<
|
data: null as any[] | null,
|
||||||
Prisma.DesaAntiKorupsiGetPayload<{
|
page: 1,
|
||||||
include: {
|
totalPages: 1,
|
||||||
file: true;
|
total: 0,
|
||||||
kategori: true;
|
loading: false,
|
||||||
};
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
}>
|
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
|
||||||
> | null,
|
desaAntikorupsi.findMany.page = page;
|
||||||
async load() {
|
try {
|
||||||
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
||||||
"find-many"
|
"findMany"
|
||||||
].get();
|
].get({
|
||||||
if (res.status === 200) {
|
query: { page, limit },
|
||||||
desaAntikorupsi.findMany.data = res.data?.data ?? [];
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -281,14 +300,38 @@ const kategoriDesaAntiKorupsi = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Array<{
|
data: null as any[] | null,
|
||||||
id: string;
|
page: 1,
|
||||||
name: string;
|
totalPages: 1,
|
||||||
}> | null,
|
total: 0,
|
||||||
async load() {
|
loading: false,
|
||||||
const res = await ApiFetch.api.landingpage.kategoridak["find-many"].get();
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
if (res.status === 200) {
|
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
|
||||||
kategoriDesaAntiKorupsi.findMany.data = res.data?.data ?? [];
|
kategoriDesaAntiKorupsi.findMany.page = page;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.landingpage.kategoridak[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
828
src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts
Normal file
828
src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
/* 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(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 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
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -5,10 +6,10 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateProgramInovasi = z.object({
|
const templateProgramInovasi = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(1, "Nama minimal 1 karakter"),
|
||||||
description: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
description: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
link: z.string().min(3, "Link minimal 3 karakter"),
|
link: z.string().min(1, "Link minimal 1 karakter"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
|
type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
|
||||||
@@ -59,15 +60,38 @@ const programInovasi = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as any[] | null,
|
||||||
| Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>[]
|
page: 1,
|
||||||
| null,
|
totalPages: 1,
|
||||||
async load() {
|
total: 0,
|
||||||
const res = await ApiFetch.api.landingpage.programinovasi[
|
loading: false,
|
||||||
"find-many"
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
].get();
|
programInovasi.findMany.loading = true; // Use the full path to access the property
|
||||||
if (res.status === 200) {
|
programInovasi.findMany.page = page;
|
||||||
programInovasi.findMany.data = res.data?.data ?? [];
|
try {
|
||||||
|
const res = await ApiFetch.api.landingpage.programinovasi[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -453,13 +477,38 @@ const mediaSosial = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as any[] | null,
|
||||||
| Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
|
page: 1,
|
||||||
| null,
|
totalPages: 1,
|
||||||
async load() {
|
total: 0,
|
||||||
const res = await ApiFetch.api.landingpage.mediasosial["find-many"].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
mediaSosial.findMany.data = res.data?.data ?? [];
|
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
||||||
|
mediaSosial.findMany.page = page;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.landingpage.mediasosial[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -52,19 +53,38 @@ const sdgsDesa = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Array<
|
data: null as any[] | null,
|
||||||
Prisma.SDGSDesaGetPayload<{
|
page: 1,
|
||||||
include: {
|
totalPages: 1,
|
||||||
image: true;
|
total: 0,
|
||||||
};
|
loading: false,
|
||||||
}>
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
> | null,
|
sdgsDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
async load() {
|
sdgsDesa.findMany.page = page;
|
||||||
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
try {
|
||||||
"find-many"
|
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
||||||
].get();
|
"findMany"
|
||||||
if (res.status === 200) {
|
].get({
|
||||||
sdgsDesa.findMany.data = res.data?.data ?? [];
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
282
src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
Normal file
282
src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
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 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;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
|
||||||
|
"findMany"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const beasiswaDesaState = proxy({
|
||||||
|
beasiswaPendaftar,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default beasiswaDesaState;
|
||||||
178
src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
Normal file
178
src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
Normal file
@@ -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<DataPendidikan, "id"> & { 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;
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
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: [] as Prisma.DataPerpustakaanGetPayload<{
|
||||||
|
include: {
|
||||||
|
kategori: true;
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||||
|
"findMany"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
dataPerpustakaan.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
|
||||||
|
"findMany"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
kategoriBuku.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 perpustakaanDigitalState = proxy({
|
||||||
|
dataPerpustakaan,
|
||||||
|
kategoriBuku,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default perpustakaanDigitalState;
|
||||||
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|||||||
const templateGrafikJenisKelamin = z.object({
|
const templateGrafikJenisKelamin = z.object({
|
||||||
laki: z.string().min(1, "Data laki-laki harus diisi"),
|
laki: z.string().min(1, "Data laki-laki harus diisi"),
|
||||||
perempuan: z.string().min(1, "Data perempuan harus diisi"),
|
perempuan: z.string().min(1, "Data perempuan harus diisi"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
laki: "",
|
laki: "",
|
||||||
@@ -17,10 +17,12 @@ const defaultForm = {
|
|||||||
|
|
||||||
const grafikBerdasarkanJenisKelamin = proxy({
|
const grafikBerdasarkanJenisKelamin = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {...defaultForm},
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async create(){
|
async create() {
|
||||||
const cek = templateGrafikJenisKelamin.safeParse(grafikBerdasarkanJenisKelamin.create.form);
|
const cek = templateGrafikJenisKelamin.safeParse(
|
||||||
|
grafikBerdasarkanJenisKelamin.create.form
|
||||||
|
);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = cek.error.issues.map((i) => i.message).join("\n");
|
const err = cek.error.issues.map((i) => i.message).join("\n");
|
||||||
toast.error(err);
|
toast.error(err);
|
||||||
@@ -33,14 +35,20 @@ const grafikBerdasarkanJenisKelamin = proxy({
|
|||||||
"create"
|
"create"
|
||||||
].post(grafikBerdasarkanJenisKelamin.create.form);
|
].post(grafikBerdasarkanJenisKelamin.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Grafik berdasarkan jenis kelamin berhasil ditambahkan");
|
toast.success(
|
||||||
|
"Grafik berdasarkan jenis kelamin berhasil ditambahkan"
|
||||||
|
);
|
||||||
await grafikBerdasarkanJenisKelamin.findMany.load();
|
await grafikBerdasarkanJenisKelamin.findMany.load();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin");
|
toast.error(
|
||||||
|
res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal create:", error);
|
console.error("Gagal create:", error);
|
||||||
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin");
|
toast.error(
|
||||||
|
"Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
grafikBerdasarkanJenisKelamin.create.loading = false;
|
grafikBerdasarkanJenisKelamin.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -52,8 +60,9 @@ const grafikBerdasarkanJenisKelamin = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
load: async (page = 1, limit = 10) => {
|
||||||
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
|
// Change to arrow function
|
||||||
|
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
|
||||||
grafikBerdasarkanJenisKelamin.findMany.page = page;
|
grafikBerdasarkanJenisKelamin.findMany.page = page;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
|
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
|
||||||
@@ -61,13 +70,17 @@ const grafikBerdasarkanJenisKelamin = proxy({
|
|||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query: { page, limit },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
|
grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
|
||||||
grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
|
grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
|
||||||
grafikBerdasarkanJenisKelamin.findMany.totalPages = res.data.totalPages || 1;
|
grafikBerdasarkanJenisKelamin.findMany.totalPages =
|
||||||
|
res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load grafik berdasarkan jenis kelamin:", res.data?.message);
|
console.error(
|
||||||
|
"Failed to load grafik berdasarkan jenis kelamin:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
grafikBerdasarkanJenisKelamin.findMany.data = [];
|
grafikBerdasarkanJenisKelamin.findMany.data = [];
|
||||||
grafikBerdasarkanJenisKelamin.findMany.total = 0;
|
grafikBerdasarkanJenisKelamin.findMany.total = 0;
|
||||||
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
|
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
|
||||||
@@ -106,7 +119,7 @@ const grafikBerdasarkanJenisKelamin = proxy({
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
id: "",
|
id: "",
|
||||||
form: {...defaultForm},
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId() {
|
async byId() {
|
||||||
// Method implementation if needed
|
// Method implementation if needed
|
||||||
@@ -119,20 +132,24 @@ const grafikBerdasarkanJenisKelamin = proxy({
|
|||||||
}
|
}
|
||||||
const cek = templateGrafikJenisKelamin.safeParse(this.form);
|
const cek = templateGrafikJenisKelamin.safeParse(this.form);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
toast.error(err);
|
toast.error(err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`, {
|
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`,
|
||||||
method: "PUT",
|
{
|
||||||
headers: {
|
method: "PUT",
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
},
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify(this.form),
|
},
|
||||||
});
|
body: JSON.stringify(this.form),
|
||||||
|
}
|
||||||
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!response.ok || !result?.success) {
|
if (!response.ok || !result?.success) {
|
||||||
throw new Error(result?.message || "Gagal update data");
|
throw new Error(result?.message || "Gagal update data");
|
||||||
@@ -156,29 +173,40 @@ const grafikBerdasarkanJenisKelamin = proxy({
|
|||||||
try {
|
try {
|
||||||
grafikBerdasarkanJenisKelamin.delete.loading = true;
|
grafikBerdasarkanJenisKelamin.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`, {
|
const response = await fetch(
|
||||||
method: "DELETE",
|
`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "DELETE",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
if (response.ok && result?.success) {
|
||||||
toast.success(result.message || "Grafik berdasarkan jenis kelamin berhasil dihapus");
|
toast.success(
|
||||||
|
result.message ||
|
||||||
|
"Grafik berdasarkan jenis kelamin berhasil dihapus"
|
||||||
|
);
|
||||||
await grafikBerdasarkanJenisKelamin.findMany.load(); // refresh list
|
await grafikBerdasarkanJenisKelamin.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus grafik berdasarkan jenis kelamin");
|
toast.error(
|
||||||
|
result?.message ||
|
||||||
|
"Gagal menghapus grafik berdasarkan jenis kelamin"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
console.error("Gagal delete:", error);
|
||||||
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin");
|
toast.error(
|
||||||
|
"Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
grafikBerdasarkanJenisKelamin.delete.loading = false;
|
grafikBerdasarkanJenisKelamin.delete.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default grafikBerdasarkanJenisKelamin;
|
export default grafikBerdasarkanJenisKelamin;
|
||||||
|
|||||||
@@ -1,169 +1,683 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
})
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
name: "",
|
name: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
|
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
id: true;
|
id: true;
|
||||||
name: true;
|
name: true;
|
||||||
imageId: true;
|
imageId: true;
|
||||||
image?: {
|
image?: {
|
||||||
select: {
|
select: {
|
||||||
link: true;
|
link: true;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const stateStrukturPPID = proxy({
|
const stateStruktur = proxy({
|
||||||
struktur: {
|
struktur: {
|
||||||
data: null as StrukturPPIDForm | null,
|
data: null as StrukturPPIDForm | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null as string | null,
|
error: null as string | null,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if(!id) {
|
if (!id) {
|
||||||
toast.warn("ID tidak valid")
|
toast.warn("ID tidak valid");
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ppid/strukturppid/${id}`);
|
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 (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
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: {
|
const result = await response.json();
|
||||||
id: "",
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
error: null as string | null,
|
|
||||||
isReadOnly: false,
|
|
||||||
|
|
||||||
initialize(strukturData: StrukturPPIDForm) {
|
if (result.success) {
|
||||||
this.id = strukturData.id;
|
this.data = result.data;
|
||||||
this.isReadOnly = false;
|
return result.data;
|
||||||
this.form = {
|
} else {
|
||||||
name: strukturData.name || "",
|
throw new Error(result.message || "Gagal mengambil data struktur");
|
||||||
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 stateStrukturPPID.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;
|
|
||||||
}
|
}
|
||||||
},
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
async loadForEdit(id: string) {
|
this.error = errorMessage;
|
||||||
const strukturData = await this.struktur.load(id);
|
console.error("Load struktur error:", errorMessage);
|
||||||
if (strukturData) {
|
toast.error("Terjadi kesalahan saat mengambil data struktur");
|
||||||
this.editStruktur.initialize(strukturData);
|
return null;
|
||||||
}
|
} finally {
|
||||||
return strukturData;
|
this.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.struktur.reset();
|
this.data = null;
|
||||||
this.editStruktur.reset();
|
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;
|
||||||
|
}>,
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
||||||
|
"find-many"
|
||||||
|
].get();
|
||||||
|
if (res.status === 200) {
|
||||||
|
// The API now returns the id field, so we can use it directly
|
||||||
|
this.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Find many error:", error);
|
||||||
|
this.data = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
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 any[] | null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 0,
|
||||||
|
loading: false,
|
||||||
|
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||||
|
pegawai.findMany.loading = true; // Use the full path to access the property
|
||||||
|
pegawai.findMany.page = page;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
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.PegawaiGetPayload<{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
export default stateStrukturPPID;
|
||||||
|
|
||||||
63
src/app/admin/(dashboard)/_state/state-file-storage.ts
Normal file
63
src/app/admin/(dashboard)/_state/state-file-storage.ts
Normal file
@@ -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<void>;
|
||||||
|
del: (params: { id: string }) => Promise<void>;
|
||||||
|
}>({
|
||||||
|
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;
|
||||||
456
src/app/admin/(dashboard)/_state/user/user-state.ts
Normal file
456
src/app/admin/(dashboard)/_state/user/user-state.ts
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
import { proxy } from 'valtio'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import ApiFetch from '@/lib/api-fetch'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// 1. Validasi Zod
|
||||||
|
const userSchema = z.object({
|
||||||
|
nama: z.string().min(1, 'Nama harus diisi'),
|
||||||
|
email: z.string().email('Email tidak valid'),
|
||||||
|
password: z.string().min(6, 'Password minimal 6 karakter'),
|
||||||
|
roleId: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
|
||||||
|
|
||||||
|
// 2. State Valtio
|
||||||
|
const userState = proxy({
|
||||||
|
// Register
|
||||||
|
register: {
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
|
||||||
|
if (!valid.success) {
|
||||||
|
const err = valid.error.issues.map(i => i.message).join(', ')
|
||||||
|
return toast.error(err)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
userState.register.loading = true
|
||||||
|
const res = await ApiFetch.api.user.register.post(userState.register.form)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('Registrasi berhasil, silakan login')
|
||||||
|
userState.register.form = { ...defaultForm } // reset
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || 'Gagal registrasi')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Terjadi kesalahan saat registrasi')
|
||||||
|
} finally {
|
||||||
|
userState.register.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Login
|
||||||
|
login: {
|
||||||
|
form: { email: '', password: '' },
|
||||||
|
loading: false,
|
||||||
|
async submit() {
|
||||||
|
try {
|
||||||
|
userState.login.loading = true
|
||||||
|
const res = await ApiFetch.api.user.login.post(userState.login.form)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('Login berhasil')
|
||||||
|
const token = res.data?.data?.token
|
||||||
|
if (typeof token === 'string') {
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
// Optional: simpan user role untuk otorisasi
|
||||||
|
const user = res.data?.data?.user
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || 'Login gagal')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Terjadi kesalahan saat login')
|
||||||
|
} finally {
|
||||||
|
userState.login.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// CRUD User (untuk admin)
|
||||||
|
create: {
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
async create(isAdmin = false) {
|
||||||
|
const valid = userSchema.safeParse(userState.create.form)
|
||||||
|
if (!valid.success) {
|
||||||
|
const err = valid.error.issues.map(i => i.message).join(', ')
|
||||||
|
return toast.error(err)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
userState.create.loading = true
|
||||||
|
const endpoint = isAdmin ? 'create' : 'register'
|
||||||
|
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('User berhasil dibuat')
|
||||||
|
userState.findMany.load() // refresh list
|
||||||
|
userState.create.form = { ...defaultForm } // reset form
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || 'Gagal membuat user')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal membuat user')
|
||||||
|
} finally {
|
||||||
|
userState.create.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find Many
|
||||||
|
findMany: {
|
||||||
|
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
||||||
|
loading: false,
|
||||||
|
async load() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.user.findMany.get()
|
||||||
|
if (res.status === 200) {
|
||||||
|
this.data = res.data?.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal muat data user')
|
||||||
|
} finally {
|
||||||
|
this.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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update
|
||||||
|
update: {
|
||||||
|
id: '',
|
||||||
|
form: { ...defaultForm },
|
||||||
|
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) {
|
||||||
|
const user = data.data
|
||||||
|
this.id = user.id
|
||||||
|
this.form = {
|
||||||
|
nama: user.nama,
|
||||||
|
email: user.email,
|
||||||
|
password: '', // jangan kirim password lama
|
||||||
|
roleId: user.roleId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal muat data')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/user/update/${this.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('Update berhasil')
|
||||||
|
userState.findMany.load()
|
||||||
|
} else {
|
||||||
|
toast.error(data.message || 'Gagal update')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Gagal update 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const templateRole = z.object({
|
||||||
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultRole = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
@@ -5,25 +5,21 @@ import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'
|
|||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "List Survey",
|
label: "List Berita",
|
||||||
value: "listSurvey",
|
value: "list_berita",
|
||||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey"
|
href: "/admin/desa/berita/list-berita"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "List Bulanan",
|
label: "Kategori Berita",
|
||||||
value: "listBulanan",
|
value: "kategori_berita",
|
||||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan"
|
href: "/admin/desa/berita/kategori-berita"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "List Gender Stat",
|
|
||||||
value: "listGenderStat",
|
|
||||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat"
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||||
@@ -45,7 +41,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title order={3}>Indeks Kepuasan Masyarakat</Title>
|
<Title order={3}>Gallery</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((e, i) => (
|
||||||
@@ -64,4 +60,4 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabs;
|
export default LayoutTabsBerita;
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
import { Box, Button, Center, FileInput, Image, Paper, Select, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { IconArrowBack, IconImageInPicture } 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 stateDashboardBerita from '../../../_state/desa/berita';
|
|
||||||
|
|
||||||
export default function CreateBerita() {
|
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
// Reset state di valtio
|
|
||||||
beritaState.berita.create.form = {
|
|
||||||
judul: "",
|
|
||||||
deskripsi: "",
|
|
||||||
kategoriBeritaId: "",
|
|
||||||
imageId: "",
|
|
||||||
content: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset state lokal
|
|
||||||
setPreviewImage(null);
|
|
||||||
setFile(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!file) {
|
|
||||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload gambar dulu
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simpan ID gambar ke form
|
|
||||||
beritaState.berita.create.form.imageId = uploaded.id;
|
|
||||||
|
|
||||||
// Submit data berita
|
|
||||||
await beritaState.berita.create.create();
|
|
||||||
|
|
||||||
// Reset form setelah submit
|
|
||||||
resetForm();
|
|
||||||
router.push("/admin/desa/berita")
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Title order={3}>Create Berita</Title>
|
|
||||||
<TextInput
|
|
||||||
value={beritaState.berita.create.form.judul}
|
|
||||||
onChange={(val) => {
|
|
||||||
beritaState.berita.create.form.judul = val.target.value;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
|
||||||
<SelectCategory
|
|
||||||
value={beritaState.berita.create.form.kategoriBeritaId}
|
|
||||||
onChange={(val) => {
|
|
||||||
beritaState.berita.create.form.kategoriBeritaId = val?.id || "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
value={beritaState.berita.create.form.deskripsi}
|
|
||||||
onChange={(val) => {
|
|
||||||
beritaState.berita.create.form.deskripsi = val.target.value;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
|
||||||
placeholder="masukkan deskripsi"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FileInput
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
|
|
||||||
value={file}
|
|
||||||
onChange={async (e) => {
|
|
||||||
if (!e) return;
|
|
||||||
setFile(e);
|
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
|
||||||
);
|
|
||||||
setPreviewImage(base64);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{previewImage ? (
|
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
|
||||||
) : (
|
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
|
||||||
<IconImageInPicture />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<Box>
|
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
|
||||||
<CreateEditor
|
|
||||||
value={beritaState.berita.create.form.content}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
beritaState.berita.create.form.content = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface SelectCategoryProps {
|
|
||||||
onChange: (value: Prisma.KategoriBeritaGetPayload<{
|
|
||||||
select: {
|
|
||||||
name: true;
|
|
||||||
id: true;
|
|
||||||
};
|
|
||||||
}> | null) => void;
|
|
||||||
value?: string | null;
|
|
||||||
defaultValue?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectCategory({
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
defaultValue,
|
|
||||||
}: SelectCategoryProps) {
|
|
||||||
const categoryState = useProxy(stateDashboardBerita.category);
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
categoryState.findMany.load().then(() => {
|
|
||||||
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!categoryState.findMany.data) {
|
|
||||||
return <Skeleton height={38} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const selectedValue = value || defaultValue;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
|
||||||
placeholder="Pilih kategori"
|
|
||||||
data={categoryState.findMany.data.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
value={selectedValue || null}
|
|
||||||
onChange={(val: string | null) => {
|
|
||||||
if (val) {
|
|
||||||
const selected = categoryState.findMany.data?.find((item) => item.id === val);
|
|
||||||
if (selected) {
|
|
||||||
onChange(selected);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onChange(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/* 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, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function EditKategoriBerita() {
|
||||||
|
const editState = useProxy(stateDashboardBerita.kategoriBerita)
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: editState.update.form.name || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadKategori = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
||||||
|
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 handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
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 (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3}>Edit Kategori Berita</Title>
|
||||||
|
<TextInput
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>}
|
||||||
|
placeholder="masukkan nama kategori Berita"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleSubmit}>Simpan</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditKategoriBerita;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
'use client'
|
||||||
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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 (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={4}>Create Kategori Berita</Title>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>}
|
||||||
|
placeholder='Masukkan nama kategori Berita'
|
||||||
|
value={createState.create.form.name}
|
||||||
|
onChange={(val) => {
|
||||||
|
createState.create.form.name = val.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateKategoriBerita;
|
||||||
128
src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
Normal file
128
src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { IconEdit, 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 JudulList from '../../../_com/judulList';
|
||||||
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function KategoriBerita() {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Kategori Berita'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListKategoriBerita search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListKategoriBerita({ search }: { search: string }) {
|
||||||
|
const listDataState = useProxy(stateDashboardBerita.kategoriBerita)
|
||||||
|
const router = useRouter();
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listDataState.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
listDataState.delete.delete(selectedId)
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
|
||||||
|
listDataState.findMany.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.name.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!listDataState.findMany.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p="md">
|
||||||
|
<Stack>
|
||||||
|
<JudulList
|
||||||
|
title='List Kategori Berita'
|
||||||
|
href='/admin/desa/berita/kategori-berita/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>No</TableTh>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Edit</TableTh>
|
||||||
|
<TableTh>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={100}>
|
||||||
|
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color='green' onClick={() => router.push(`/admin/pendidikan/perpustakaan-digital/kategori-Berita/${item.id}`)}>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
color='red'
|
||||||
|
disabled={listDataState.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setModalHapus(true)
|
||||||
|
}}>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
text='Apakah anda yakin ingin menghapus kategori Berita ini?'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KategoriBerita;
|
||||||
13
src/app/admin/(dashboard)/desa/berita/layout.tsx
Normal file
13
src/app/admin/(dashboard)/desa/berita/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react';
|
||||||
|
import LayoutTabsBerita from './_com/layoutTabs';
|
||||||
|
|
||||||
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<LayoutTabsBerita>
|
||||||
|
{children}
|
||||||
|
</LayoutTabsBerita>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -1,30 +1,29 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
"use client";
|
"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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
Skeleton,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
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 { FileInput } from "@mantine/core";
|
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import stateDashboardBerita from "../../../../_state/desa/berita";
|
|
||||||
|
|
||||||
function EditBerita() {
|
function EditBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
@@ -43,6 +42,7 @@ function EditBerita() {
|
|||||||
|
|
||||||
// Load berita by id saat pertama kali
|
// Load berita by id saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
beritaState.kategoriBerita.findMany.load()
|
||||||
const loadBerita = async () => {
|
const loadBerita = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -99,7 +99,7 @@ function EditBerita() {
|
|||||||
|
|
||||||
await beritaState.berita.edit.update();
|
await beritaState.berita.edit.update();
|
||||||
toast.success("Berita berhasil diperbarui!");
|
toast.success("Berita berhasil diperbarui!");
|
||||||
router.push("/admin/desa/berita");
|
router.push("/admin/desa/berita/list-berita");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating berita:", error);
|
console.error("Error updating berita:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui berita");
|
toast.error("Terjadi kesalahan saat memperbarui berita");
|
||||||
@@ -130,46 +130,88 @@ function EditBerita() {
|
|||||||
placeholder="masukkan deskripsi"
|
placeholder="masukkan deskripsi"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
value={file}
|
<Box>
|
||||||
onChange={async (e) => {
|
<Dropzone
|
||||||
if (!e) return;
|
onDrop={(files) => {
|
||||||
setFile(e);
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
if (selectedFile) {
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
setFile(selectedFile);
|
||||||
);
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
setPreviewImage(base64);
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
{previewImage ? (
|
<div>
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
<Text size="xl" inline>
|
||||||
) : (
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
</Text>
|
||||||
<IconImageInPicture />
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
</Center>
|
Maksimal 5MB dan harus format gambar
|
||||||
)}
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
||||||
beritaState.berita.edit.form.content = htmlContent;
|
beritaState.berita.edit.form.content = htmlContent;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<SelectCategory
|
<Select
|
||||||
value={formData.kategoriBeritaId}
|
value={formData.kategoriBeritaId}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })}
|
||||||
setFormData({
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
...formData,
|
placeholder='Pilih kategori'
|
||||||
kategoriBeritaId: val?.id || ''
|
data={
|
||||||
});
|
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
||||||
}}
|
value: v.id,
|
||||||
|
label: v.name
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
required
|
||||||
|
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit Berita</Button>
|
<Button onClick={handleSubmit}>Edit Berita</Button>
|
||||||
@@ -179,61 +221,4 @@ function EditBerita() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectCategoryProps {
|
|
||||||
onChange: (value: Prisma.KategoriBeritaGetPayload<{
|
|
||||||
select: {
|
|
||||||
name: true;
|
|
||||||
id: true;
|
|
||||||
};
|
|
||||||
}> | null) => void;
|
|
||||||
value?: string | null;
|
|
||||||
defaultValue?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectCategory({
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
defaultValue,
|
|
||||||
}: SelectCategoryProps) {
|
|
||||||
const categoryState = useProxy(stateDashboardBerita.category);
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
categoryState.findMany.load().then(() => {
|
|
||||||
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!categoryState.findMany.data) {
|
|
||||||
return <Skeleton height={38} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const selectedValue = value || defaultValue;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
|
||||||
placeholder="Pilih kategori"
|
|
||||||
data={categoryState.findMany.data.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
value={selectedValue || null}
|
|
||||||
onChange={(val: string | null) => {
|
|
||||||
if (val) {
|
|
||||||
const selected = categoryState.findMany.data?.find((item) => item.id === val);
|
|
||||||
if (selected) {
|
|
||||||
onChange(selected);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onChange(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditBerita;
|
export default EditBerita;
|
||||||
@@ -8,8 +8,8 @@ import { useParams, useRouter } from 'next/navigation';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
|
||||||
function DetailBerita() {
|
function DetailBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita)
|
||||||
@@ -28,7 +28,7 @@ function DetailBerita() {
|
|||||||
beritaState.berita.delete.byId(selectedId)
|
beritaState.berita.delete.byId(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
router.push("/admin/desa/berita")
|
router.push("/admin/desa/berita/list-berita")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ function DetailBerita() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (beritaState.berita.findUnique.data) {
|
if (beritaState.berita.findUnique.data) {
|
||||||
router.push(`/admin/desa/berita/${beritaState.berita.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!beritaState.berita.findUnique.data}
|
disabled={!beritaState.berita.findUnique.data}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
'use client'
|
||||||
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
export default function CreateBerita() {
|
||||||
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
beritaState.kategoriBerita.findMany.load()
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
// Reset state di valtio
|
||||||
|
beritaState.berita.create.form = {
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
kategoriBeritaId: "",
|
||||||
|
imageId: "",
|
||||||
|
content: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset state lokal
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn("Pilih file gambar terlebih dahulu");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload gambar dulu
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error("Gagal upload gambar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan ID gambar ke form
|
||||||
|
beritaState.berita.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
|
// Submit data berita
|
||||||
|
await beritaState.berita.create.create();
|
||||||
|
|
||||||
|
// Reset form setelah submit
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/desa/berita/list-berita")
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3}>Create Berita</Title>
|
||||||
|
<TextInput
|
||||||
|
value={beritaState.berita.create.form.judul}
|
||||||
|
onChange={(val) => {
|
||||||
|
beritaState.berita.create.form.judul = val.target.value;
|
||||||
|
}}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||||
|
placeholder="masukkan judul"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
||||||
|
placeholder="Pilih kategori"
|
||||||
|
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}))}
|
||||||
|
value={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"
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={beritaState.berita.create.form.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
beritaState.berita.create.form.deskripsi = val.target.value;
|
||||||
|
}}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
||||||
|
placeholder="masukkan deskripsi"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
|
<Box>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={beritaState.berita.create.form.content}
|
||||||
|
onChange={(htmlContent) => {
|
||||||
|
beritaState.berita.create.form.content = htmlContent;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,8 +6,9 @@ import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/ico
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import stateDashboardBerita from '../../_state/desa/berita';
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Berita() {
|
function Berita() {
|
||||||
@@ -38,23 +39,17 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
} = beritaState.berita.findMany;
|
} = beritaState.berita.findMany;
|
||||||
|
|
||||||
|
|
||||||
// Fetch pertama kali
|
// Fetch data when page or search changes
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10); // awal page = 1
|
load(page, 10, search);
|
||||||
}, [page]);
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (data || []).filter((item) => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.judul.toLowerCase().includes(keyword) ||
|
|
||||||
item.kategoriBerita?.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return <Skeleton h={500} />;
|
return <Skeleton h={500} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredData = data || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors["white-1"]} p={"md"}>
|
<Paper bg={colors["white-1"]} p={"md"}>
|
||||||
@@ -67,7 +62,7 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push("/admin/desa/berita/create")}
|
onClick={() => router.push("/admin/desa/berita/list-berita/create")}
|
||||||
bg={colors["blue-button"]}
|
bg={colors["blue-button"]}
|
||||||
>
|
>
|
||||||
<IconCircleDashedPlus size={25} />
|
<IconCircleDashedPlus size={25} />
|
||||||
@@ -107,7 +102,7 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
<Button
|
<Button
|
||||||
bg={"green"}
|
bg={"green"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(`/admin/desa/berita/${item.id}`)
|
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconDeviceImacCog size={25} />
|
<IconDeviceImacCog size={25} />
|
||||||
@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -18,6 +19,11 @@ function EditFoto() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: fotoState.update.form.name || '',
|
||||||
|
deskripsi: fotoState.update.form.deskripsi || '',
|
||||||
|
imagesId: fotoState.update.form.imagesId || ''
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadFoto = async () => {
|
const loadFoto = async () => {
|
||||||
@@ -26,6 +32,11 @@ function EditFoto() {
|
|||||||
try {
|
try {
|
||||||
const data = await fotoState.update.load(id);
|
const data = await fotoState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
name: data.name || '',
|
||||||
|
deskripsi: data.deskripsi || '',
|
||||||
|
imagesId: data.imageGalleryFoto?.id || ''
|
||||||
|
});
|
||||||
if (data?.imageGalleryFoto?.link) {
|
if (data?.imageGalleryFoto?.link) {
|
||||||
setPreviewImage(data.imageGalleryFoto.link);
|
setPreviewImage(data.imageGalleryFoto.link);
|
||||||
}
|
}
|
||||||
@@ -40,6 +51,12 @@ function EditFoto() {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
fotoState.update.form = {
|
||||||
|
...fotoState.update.form,
|
||||||
|
name: formData.name,
|
||||||
|
deskripsi: formData.deskripsi,
|
||||||
|
imagesId: formData.imagesId
|
||||||
|
};
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
@@ -74,30 +91,55 @@ function EditFoto() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
|
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
|
||||||
placeholder='Masukkan judul foto'
|
placeholder='Masukkan judul foto'
|
||||||
value={fotoState.update.form.name}
|
value={formData.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
(fotoState.update.form.name = e.target.value)
|
(formData.name = e.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
|
<Text>Upload Foto</Text>
|
||||||
value={file}
|
<Dropzone
|
||||||
onChange={async (e) => {
|
onDrop={(files) => {
|
||||||
if (!e) return;
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
setFile(e);
|
if (selectedFile) {
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
setFile(selectedFile);
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
);
|
}
|
||||||
setPreviewImage(base64);
|
}}
|
||||||
}}
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
/>
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
{previewImage ? (
|
accept={{ 'image/*': [] }}
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
>
|
||||||
) : (
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
<Dropzone.Accept>
|
||||||
<IconImageInPicture />
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Center>
|
</Dropzone.Accept>
|
||||||
)}
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage ? (
|
||||||
|
<Image alt="" src={previewImage} w={200} h={200} />
|
||||||
|
) : (
|
||||||
|
<Center w={200} h={200} bg={"gray"}>
|
||||||
|
<IconImageInPicture />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -69,25 +70,62 @@ function CreateFoto() {
|
|||||||
fotoState.create.form.name = val.target.value;
|
fotoState.create.form.name = val.target.value;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
value={file}
|
<Box>
|
||||||
onChange={async (e) => {
|
<Dropzone
|
||||||
if (!e) return;
|
onDrop={(files) => {
|
||||||
setFile(e);
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
if (selectedFile) {
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
setFile(selectedFile);
|
||||||
);
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
setPreviewImage(base64);
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
{previewImage ? (
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
accept={{ 'image/*': [] }}
|
||||||
) : (
|
>
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
<IconImageInPicture />
|
<Dropzone.Accept>
|
||||||
</Center>
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
)}
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
|
|||||||
@@ -1,93 +1,124 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import colors from '@/con/colors';
|
import colors from "@/con/colors";
|
||||||
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import stateFileStorage from "@/state/state-list-image";
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import {
|
||||||
import { useRouter } from 'next/navigation';
|
ActionIcon,
|
||||||
import JudulListTab from '../../../_com/judulListTab';
|
Box,
|
||||||
import { useProxy } from 'valtio/utils';
|
Flex,
|
||||||
import stateGallery from '../../../_state/desa/gallery';
|
Group,
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
Image,
|
||||||
import HeaderSearch from '../../../_com/header';
|
Pagination,
|
||||||
import { useState } from 'react';
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} 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";
|
||||||
|
|
||||||
function Foto() {
|
export default function ListImage() {
|
||||||
const [search, setSearch] = useState("");
|
const { list, total } = useSnapshot(stateFileStorage);
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<HeaderSearch
|
|
||||||
title='Posisi Organisasi'
|
|
||||||
placeholder='pencarian'
|
|
||||||
searchIcon={<IconSearch size={20} />}
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<ListFoto search={search} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListFoto({ search }: { search: string }) {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
fotoState.findMany.load()
|
stateFileStorage.load();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const filteredData = (fotoState.findMany.data || []).filter(item => {
|
let timeOut: NodeJS.Timer;
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fotoState.findMany.data) {
|
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Skeleton h={500} />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Stack p={"lg"}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Flex justify="space-between">
|
||||||
<JudulListTab
|
<Title order={3}>List Foto</Title>
|
||||||
title='List Foto'
|
<TextInput
|
||||||
href='/admin/desa/gallery/foto/create'
|
radius={"lg"}
|
||||||
placeholder='pencarian'
|
leftSection={<IconSearch />}
|
||||||
searchIcon={<IconSearch size={16} />}
|
rightSection={
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() => {
|
||||||
|
stateFileStorage.load();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
placeholder="Pencarian"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (timeOut) clearTimeout(timeOut);
|
||||||
|
timeOut = setTimeout(() => {
|
||||||
|
stateFileStorage.load({ search: e.target.value });
|
||||||
|
}, 200);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
</Flex>
|
||||||
<TableThead>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<TableTr>
|
<SimpleGrid
|
||||||
<TableTh>Judul Foto</TableTh>
|
cols={{
|
||||||
<TableTh>Tanggal Foto</TableTh>
|
base: 3,
|
||||||
<TableTh>Deskripsi Foto</TableTh>
|
md: 5,
|
||||||
<TableTh>Detail</TableTh>
|
lg: 10,
|
||||||
</TableTr>
|
}}
|
||||||
</TableThead>
|
>
|
||||||
<TableTbody>
|
{list &&
|
||||||
{filteredData.map((item) => (
|
list.map((v, k) => {
|
||||||
<TableTr key={item.id}>
|
return (
|
||||||
<TableTd>{item.name}</TableTd>
|
<Paper key={k} shadow="sm">
|
||||||
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
|
<Stack pos={"relative"} gap={0} justify="space-between">
|
||||||
<TableTd>
|
<motion.div
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
onClick={() => {
|
||||||
</TableTd>
|
// copy to clipboard
|
||||||
<TableTd>
|
navigator.clipboard.writeText(v.url);
|
||||||
<Button onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}>
|
toast("Berhasil disalin");
|
||||||
<IconDeviceImac size={20} />
|
}}
|
||||||
</Button>
|
whileHover={{ scale: 1.05 }}
|
||||||
</TableTd>
|
whileTap={{ scale: 0.8 }}
|
||||||
</TableTr>
|
>
|
||||||
))}
|
<Image
|
||||||
</TableTbody>
|
h={100}
|
||||||
</Table>
|
src={v.url + "?size=100"}
|
||||||
|
alt={v.name}
|
||||||
|
fit="cover"
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
<Box p={"md"} h={54}>
|
||||||
|
<Text lineClamp={2} fz={"xs"}>
|
||||||
|
{v.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Group justify="end">
|
||||||
|
<IconTrash
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
stateFileStorage.del({ name: v.name }).finally(() => {
|
||||||
|
toast("Berhasil dihapus");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SimpleGrid>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
{total && (
|
||||||
|
<Pagination
|
||||||
|
total={total}
|
||||||
|
onChange={(e) => {
|
||||||
|
stateFileStorage.page = e;
|
||||||
|
stateFileStorage.load();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Foto;
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import LayoutTabsGallery from "../../ppid/_com/layoutTabsGallery"
|
import LayoutTabsGallery from "./lib/layoutTabs"
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,30 +5,20 @@ import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'
|
|||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Grafik Hasil Kepuasan Masyarakat",
|
label: "Foto",
|
||||||
value: "grafikhasilkepuasamanmasyarakat",
|
value: "foto",
|
||||||
href: "/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat"
|
href: "/admin/desa/gallery/foto"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Grafik Berdasarkan Jenis Kelamin Responden",
|
label: "Video",
|
||||||
value: "grafikberdasarkanjeniskelaminresponden",
|
value: "video",
|
||||||
href: "/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden"
|
href: "/admin/desa/gallery/video"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Grafik Berdasarkan Pilihan Responden",
|
|
||||||
value: "grafikberdasarkanpilihanresponden",
|
|
||||||
href: "/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Grafik Berdasarkan Umur Responden",
|
|
||||||
value: "grafikberdasarkanumurresponden",
|
|
||||||
href: "/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur"
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||||
@@ -50,7 +40,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title order={3}>Indeks Kepuasan Masyarakat (IKM) Desa Darmasaba</Title>
|
<Title order={3}>Gallery</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((e, i) => (
|
||||||
@@ -69,4 +59,4 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabs;
|
export default LayoutTabsGallery;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import JudulListTab from '../../../_com/judulListTab';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import stateGallery from '../../../_state/desa/gallery';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import HeaderSearch from '../../../_com/header';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import stateGallery from '../../../_state/desa/gallery';
|
||||||
|
|
||||||
function Video() {
|
function Video() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -29,35 +29,34 @@ function Video() {
|
|||||||
function ListVideo({ search }: { search: string }) {
|
function ListVideo({ search }: { search: string }) {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = videoState.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
videoState.findMany.load()
|
load(page, 10, search)
|
||||||
}, [])
|
}, [page, search])
|
||||||
|
|
||||||
const filteredData = (videoState.findMany.data || []).filter(item => {
|
const filteredData = (data || [])
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!videoState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<JudulListTab
|
<JudulList
|
||||||
title='List Video'
|
title='List Video'
|
||||||
href='/admin/desa/gallery/video/create'
|
href='/admin/desa/gallery/video/create'
|
||||||
placeholder='pencarian'
|
|
||||||
searchIcon={<IconSearch size={16} />}
|
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
@@ -71,10 +70,25 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Box w={200}>
|
||||||
|
<Text lineClamp={1}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
|
||||||
|
<TableTd>
|
||||||
|
<Box w={200}>
|
||||||
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
|
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
|
||||||
@@ -86,6 +100,15 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)} // ini penting!
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -16,11 +17,14 @@ function EditSuratKeterangan() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [file2, setFile2] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: stateSurat.edit.form.name,
|
name: stateSurat.edit.form.name,
|
||||||
deskripsi: stateSurat.edit.form.deskripsi,
|
deskripsi: stateSurat.edit.form.deskripsi,
|
||||||
imageId: stateSurat.edit.form.imageId,
|
imageId: stateSurat.edit.form.imageId,
|
||||||
|
image2Id: stateSurat.edit.form.image2Id,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,12 +35,22 @@ function EditSuratKeterangan() {
|
|||||||
const data = await stateSurat.edit.load(id);
|
const data = await stateSurat.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name,
|
name: data.name || "",
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi || "",
|
||||||
imageId: data.imageId,
|
imageId: data.imageId || "",
|
||||||
|
image2Id: data.image2Id || "",
|
||||||
});
|
});
|
||||||
if (data?.image?.link) {
|
|
||||||
|
if (data.image?.link) {
|
||||||
setPreviewImage(data.image.link);
|
setPreviewImage(data.image.link);
|
||||||
|
} else {
|
||||||
|
setPreviewImage(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.image2?.link) {
|
||||||
|
setPreviewImage2(data.image2.link);
|
||||||
|
} else {
|
||||||
|
setPreviewImage2(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -54,6 +68,7 @@ function EditSuratKeterangan() {
|
|||||||
name: formData.name,
|
name: formData.name,
|
||||||
deskripsi: formData.deskripsi,
|
deskripsi: formData.deskripsi,
|
||||||
imageId: formData.imageId,
|
imageId: formData.imageId,
|
||||||
|
image2Id: formData.image2Id,
|
||||||
}
|
}
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||||
@@ -66,6 +81,17 @@ function EditSuratKeterangan() {
|
|||||||
stateSurat.edit.form.imageId = uploaded.id;
|
stateSurat.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file2) {
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error("Gagal upload gambar");
|
||||||
|
}
|
||||||
|
|
||||||
|
stateSurat.edit.form.image2Id = uploaded.id;
|
||||||
|
}
|
||||||
|
|
||||||
await stateSurat.edit.update()
|
await stateSurat.edit.update()
|
||||||
toast.success("Surat berhasil diperbarui!")
|
toast.success("Surat berhasil diperbarui!")
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
|
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
|
||||||
@@ -103,25 +129,106 @@ function EditSuratKeterangan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
value={file}
|
<Box >
|
||||||
onChange={async (e) => {
|
<Dropzone
|
||||||
if (!e) return;
|
onDrop={(files) => {
|
||||||
setFile(e);
|
const file = files[0]; // Hanya ambil file pertama
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
if (file) {
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
setFile(file);
|
||||||
);
|
setPreviewImage(URL.createObjectURL(file)); // Buat preview
|
||||||
setPreviewImage(base64);
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
maxSize={5 * 1024 ** 2} // 5MB
|
||||||
{previewImage ? (
|
accept={{
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
||||||
) : (
|
}}
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
>
|
||||||
<IconImageInPicture />
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
</Center>
|
<Dropzone.Accept>
|
||||||
)}
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag images here or click to select files
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Attach as many files as you like, each file should not exceed 5mb
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
{previewImage && (
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
fit="cover"
|
||||||
|
radius="sm"
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
|
<Box >
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const file = files[0]; // Hanya ambil file pertama
|
||||||
|
if (file) {
|
||||||
|
setFile2(file);
|
||||||
|
setPreviewImage2(URL.createObjectURL(file)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxSize={5 * 1024 ** 2} // 5MB
|
||||||
|
accept={{
|
||||||
|
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag images here or click to select files
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Attach as many files as you like, each file should not exceed 5mb
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
{previewImage2 && (
|
||||||
|
<Image
|
||||||
|
src={previewImage2}
|
||||||
|
alt="Preview"
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
fit="cover"
|
||||||
|
radius="sm"
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ function DetailSuratKeterangan() {
|
|||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" />
|
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||||
|
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image2?.link} alt="gambar" />
|
||||||
|
</Box>
|
||||||
<Flex gap={"xs"} mt={10}>
|
<Flex gap={"xs"} mt={10}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -12,8 +13,8 @@ import { useProxy } from 'valtio/utils';
|
|||||||
|
|
||||||
function CreateSuratKeterangan() {
|
function CreateSuratKeterangan() {
|
||||||
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -21,33 +22,57 @@ function CreateSuratKeterangan() {
|
|||||||
name: "",
|
name: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
|
image2Id: ""
|
||||||
}
|
}
|
||||||
setPreviewImage(null)
|
setPreviewImage(null)
|
||||||
setFile(null)
|
setPreviewImage2(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!previewImage) {
|
||||||
return toast.error("Silahkan pilih file gambar terlebih dahulu")
|
return toast.warn("Pilih file gambar utama terlebih dahulu");
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
try {
|
||||||
file: file,
|
// Upload gambar utama
|
||||||
name: file.name
|
const res1 = await ApiFetch.api.fileStorage.create.post({
|
||||||
})
|
file: previewImage.file,
|
||||||
|
name: `main_${previewImage.file.name}`,
|
||||||
|
});
|
||||||
|
|
||||||
const uploaded = res.data?.data
|
const uploadedImage1 = res1.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploadedImage1?.id) {
|
||||||
return toast.error("Gagal upload gambar")
|
return toast.error("Gagal upload gambar utama");
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadedImage2 = null;
|
||||||
|
// Upload gambar kedua jika ada
|
||||||
|
if (previewImage2) {
|
||||||
|
const res2 = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file: previewImage2.file,
|
||||||
|
name: `secondary_${previewImage2.file.name}`,
|
||||||
|
});
|
||||||
|
uploadedImage2 = res2.data?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set form data
|
||||||
|
stateSurat.create.form.imageId = uploadedImage1.id;
|
||||||
|
if (uploadedImage2?.id) {
|
||||||
|
stateSurat.create.form.image2Id = uploadedImage2.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the record
|
||||||
|
await stateSurat.create.create();
|
||||||
|
|
||||||
|
// Reset form dan redirect
|
||||||
|
resetForm();
|
||||||
|
toast.success("Data surat keterangan berhasil ditambahkan");
|
||||||
|
router.push("/admin/desa/layanan/pelayanan_surat_keterangan");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating surat keterangan:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menambahkan surat keterangan");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
stateSurat.create.form.imageId = uploaded.id
|
|
||||||
|
|
||||||
await stateSurat.create.create()
|
|
||||||
resetForm()
|
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
|
|
||||||
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
@@ -75,25 +100,105 @@ function CreateSuratKeterangan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
|
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Utama</Text>
|
||||||
value={file}
|
<Dropzone
|
||||||
onChange={async (e) => {
|
onDrop={(files) => {
|
||||||
if (!e) return;
|
const file = files[0];
|
||||||
setFile(e);
|
if (file) {
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
setPreviewImage({
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
file,
|
||||||
);
|
preview: URL.createObjectURL(file)
|
||||||
setPreviewImage(base64);
|
});
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
{previewImage ? (
|
maxSize={5 * 1024 ** 2}
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
accept={{
|
||||||
) : (
|
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
}}
|
||||||
<IconImageInPicture />
|
>
|
||||||
</Center>
|
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
||||||
)}
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<div>
|
||||||
|
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7} display="block">
|
||||||
|
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
{previewImage && (
|
||||||
|
<Image
|
||||||
|
src={previewImage.preview}
|
||||||
|
alt="Preview Gambar Utama"
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
fit="cover"
|
||||||
|
radius="sm"
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box mt="lg">
|
||||||
|
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Tambahan (Opsional)</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const file = files[0];
|
||||||
|
if (file) {
|
||||||
|
setPreviewImage2({
|
||||||
|
file,
|
||||||
|
preview: URL.createObjectURL(file)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{
|
||||||
|
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<div>
|
||||||
|
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7} display="block">
|
||||||
|
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
{previewImage2 ? (
|
||||||
|
<Image
|
||||||
|
src={previewImage2.preview}
|
||||||
|
alt="Preview Gambar Tambahan"
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
fit="cover"
|
||||||
|
radius="sm"
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text size="sm" c="dimmed" mt="sm">
|
||||||
|
Kosongkan jika tidak ada gambar tambahan
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function SuratKeterangan() {
|
function SuratKeterangan() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Pelayanan Surat Keterangan'
|
||||||
placeholder='pencarian'
|
placeholder='pencarian'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -30,54 +30,89 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
|
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useShallowEffect(() => {
|
const {
|
||||||
suratKeteranganState.findMany.load()
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = suratKeteranganState.findMany;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(page, 10)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const filteredData = (suratKeteranganState.findMany.data || []).filter(item => {
|
const filteredData = useMemo(() => {
|
||||||
const keyword = search.toLowerCase();
|
if (!data) return [];
|
||||||
return (
|
return data.filter(item => {
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
const keyword = search.toLowerCase();
|
||||||
item.deskripsi.toLowerCase().includes(keyword)
|
return (
|
||||||
);
|
item.name?.toLowerCase().includes(keyword) ||
|
||||||
});
|
item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
if (!suratKeteranganState.findMany.data) {
|
})
|
||||||
return (
|
}, [data, search]);
|
||||||
<Stack py={10}>
|
|
||||||
<Skeleton h={500} />
|
// Handle loading state
|
||||||
</Stack>
|
if (loading || !data) {
|
||||||
)
|
return (
|
||||||
}
|
<Stack py={10}>
|
||||||
|
<Skeleton height={300} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<JudulList
|
||||||
|
title='List Surat Keterangan'
|
||||||
|
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Deskripsi</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<JudulListTab
|
<JudulList
|
||||||
title='List Surat Keterangan'
|
title='List Surat Keterangan'
|
||||||
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
|
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
|
||||||
placeholder='pencarian'
|
|
||||||
searchIcon={<IconSearch size={16} />}
|
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh>Nama</TableTh>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh>Deskripsi</TableTh>
|
||||||
<TableTh>Image</TableTh>
|
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh>Detail</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Box w={200}>
|
||||||
</TableTd>
|
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||||
<TableTd>
|
</Box>
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={300}>
|
||||||
|
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text>
|
<Text>
|
||||||
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
|
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
|
||||||
@@ -90,6 +125,18 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
@@ -76,6 +75,14 @@ function EditPelayananTelunjukSakti() {
|
|||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
||||||
placeholder="masukkan nama surat keterangan"
|
placeholder="masukkan nama surat keterangan"
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
setFormData({ ...formData, deskripsi: val.target.value });
|
||||||
|
}}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
|
||||||
|
placeholder="masukkan tautan link"
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={formData.link}
|
value={formData.link}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -84,15 +91,6 @@ function EditPelayananTelunjukSakti() {
|
|||||||
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
||||||
placeholder="masukkan link"
|
placeholder="masukkan link"
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={formData.deskripsi}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
setFormData({ ...formData, deskripsi: htmlContent });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -58,11 +58,24 @@ function DetailPelayananTelunjukSakti() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Link</Text>
|
<Text fw={"bold"} fz={"lg"}>Link</Text>
|
||||||
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.link}</Text>
|
<Text
|
||||||
|
component="a"
|
||||||
|
href={telunjukSaktiState.findUnique.data?.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{telunjukSaktiState.findUnique.data?.link}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||||
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: telunjukSaktiState.findUnique.data?.deskripsi }}></Text>
|
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.deskripsi}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex gap={"xs"} mt={10}>
|
<Flex gap={"xs"} mt={10}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
@@ -43,6 +42,14 @@ function CreatePelayananTelunjukDesa() {
|
|||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>}
|
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>}
|
||||||
placeholder="masukkan nama pelayanan telunjuk sakti desa"
|
placeholder="masukkan nama pelayanan telunjuk sakti desa"
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={stateTelunjukDesa.create.form.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
stateTelunjukDesa.create.form.deskripsi = val.target.value;
|
||||||
|
}}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
|
||||||
|
placeholder="masukkan tautan link"
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.link}
|
value={stateTelunjukDesa.create.form.link}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -51,15 +58,6 @@ function CreatePelayananTelunjukDesa() {
|
|||||||
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
||||||
placeholder="masukkan link"
|
placeholder="masukkan link"
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
|
||||||
<CreateEditor
|
|
||||||
value={stateTelunjukDesa.create.form.deskripsi}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
stateTelunjukDesa.create.form.deskripsi = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,89 +1,153 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function PelayananTelunjukSakti() {
|
function PelayananTelunjukSakti() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Posisi Organisasi'
|
||||||
placeholder='pencarian'
|
placeholder='pencarian'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListPelayananTelunjukSakti search={search} />
|
<ListPelayananTelunjukSakti search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||||
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useShallowEffect(() => {
|
const {
|
||||||
telunjukSaktiState.findMany.load()
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = telunjukSaktiState.findMany;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(page, 10)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const filteredData = (telunjukSaktiState.findMany.data || []).filter(item => {
|
const filteredData = useMemo(() => {
|
||||||
const keyword = search.toLowerCase();
|
if (!data) return [];
|
||||||
return (
|
return data.filter(item => {
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
const keyword = search.toLowerCase();
|
||||||
item.deskripsi.toLowerCase().includes(keyword)
|
return (
|
||||||
);
|
item.name?.toLowerCase().includes(keyword) ||
|
||||||
});
|
item.link?.toLowerCase().includes(keyword) ||
|
||||||
|
item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
||||||
|
}, [data, search]);
|
||||||
|
|
||||||
if (!telunjukSaktiState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={300} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<JudulList
|
||||||
|
title='List Pelayanan Telunjuk Sakti Desa'
|
||||||
|
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
||||||
|
/>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Link</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Text fz={"sm"} color="gray.5">
|
||||||
|
Tidak ada data
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<JudulListTab
|
<JudulList
|
||||||
title='List Pelayanan Telunjuk Sakti Desa'
|
title='List Pelayanan Telunjuk Sakti Desa'
|
||||||
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
||||||
placeholder='pencarian'
|
|
||||||
searchIcon={<IconSearch size={16} />}
|
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh>Nama</TableTh>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh>Link</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
<TableTh>Detail</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{item.name}</TableTd>
|
<TableTd>
|
||||||
<TableTd><Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /></TableTd>
|
<Box w={100}>
|
||||||
<TableTd>
|
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
|
||||||
<Text>
|
</Box>
|
||||||
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
|
</TableTd>
|
||||||
<IconDeviceImac size={20} />
|
<TableTd>
|
||||||
</Button>
|
<Box w={100}>
|
||||||
</Text>
|
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||||
</TableTd>
|
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
||||||
</TableTr>
|
</a>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text>
|
||||||
|
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
</Button>
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
|||||||
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
@@ -105,26 +106,62 @@ function EditPenghargaan() {
|
|||||||
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
|
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
|
||||||
placeholder="masukkan juara"
|
placeholder="masukkan juara"
|
||||||
/>
|
/>
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
value={file}
|
<Box>
|
||||||
onChange={async (e) => {
|
<Dropzone
|
||||||
if (!e) return;
|
onDrop={(files) => {
|
||||||
setFile(e);
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
if (selectedFile) {
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
setFile(selectedFile);
|
||||||
);
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
setPreviewImage(base64);
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
{previewImage ? (
|
<div>
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
<Text size="xl" inline>
|
||||||
) : (
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
</Text>
|
||||||
<IconImageInPicture />
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
</Center>
|
Maksimal 5MB dan harus format gambar
|
||||||
)}
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ function DetailPenghargaan() {
|
|||||||
{statePenghargaan.findUnique.data ? (
|
{statePenghargaan.findUnique.data ? (
|
||||||
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||||
|
<Image w={{ base: 400, md: 400, lg: 400 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" />
|
||||||
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||||
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text>
|
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text>
|
||||||
@@ -62,10 +66,6 @@ function DetailPenghargaan() {
|
|||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} />
|
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
<Flex gap={"xs"} mt={10}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import penghargaanState from '../../../_state/desa/penghargaan';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
import CreateEditor from '../../../_com/createEditor';
|
import CreateEditor from '../../../_com/createEditor';
|
||||||
|
import penghargaanState from '../../../_state/desa/penghargaan';
|
||||||
|
|
||||||
|
|
||||||
function CreatePenghargaan() {
|
function CreatePenghargaan() {
|
||||||
@@ -85,25 +86,62 @@ function CreatePenghargaan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<FileInput
|
<Box>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
value={file}
|
<Box>
|
||||||
onChange={async (e) => {
|
<Dropzone
|
||||||
if (!e) return;
|
onDrop={(files) => {
|
||||||
setFile(e);
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
if (selectedFile) {
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
setFile(selectedFile);
|
||||||
);
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
setPreviewImage(base64);
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
{previewImage ? (
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
accept={{ 'image/*': [] }}
|
||||||
) : (
|
>
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
<IconImageInPicture />
|
<Dropzone.Accept>
|
||||||
</Center>
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
)}
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,61 +1,103 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import JudulListTab from '../../_com/judulListTab';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
|
import JudulList from '../../_com/judulList';
|
||||||
|
|
||||||
function Penghargaan() {
|
function Penghargaan() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Penghargaan'
|
||||||
placeholder='pencarian'
|
placeholder='pencarian'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListPenghargaan search={search} />
|
<ListPenghargaan search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListPenghargaan({ search }: { search: string }) {
|
function ListPenghargaan({ search }: { search: string }) {
|
||||||
const state = useProxy(penghargaanState)
|
const state = useProxy(penghargaanState)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useShallowEffect(() => {
|
|
||||||
state.findMany.load()
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = state.findMany;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(page, 10)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const filteredData = (state.findMany.data || []).filter(item => {
|
const filteredData = useMemo(() => {
|
||||||
const keyword = search.toLowerCase();
|
if (!data) return [];
|
||||||
return (
|
return data.filter(item => {
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
const keyword = search.toLowerCase();
|
||||||
item.deskripsi.toLowerCase().includes(keyword)
|
return (
|
||||||
);
|
item.name?.toLowerCase().includes(keyword) ||
|
||||||
});
|
item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}, [data, search]);
|
||||||
|
|
||||||
if (!state.findMany.data) {
|
// Handle loading state
|
||||||
return(
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={300} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<JudulList
|
||||||
|
title='List Penghargaan'
|
||||||
|
href='/admin/desa/penghargaan/create'
|
||||||
|
/>
|
||||||
|
<Table striped withTableBorder withRowBorders>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Deskripsi</TableTh>
|
||||||
|
<TableTh>Image</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Text ta="center">Tidak ada data</Text>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<JudulListTab
|
<JudulList
|
||||||
title='List Penghargaan'
|
title='List Penghargaan'
|
||||||
href='/admin/desa/penghargaan/create'
|
href='/admin/desa/penghargaan/create'
|
||||||
placeholder='pencarian'
|
|
||||||
searchIcon={<IconSearch size={16} />}
|
|
||||||
/>
|
/>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
@@ -69,25 +111,43 @@ function ListPenghargaan({ search }: { search: string }) {
|
|||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Box w={100}>
|
||||||
|
<Text lineClamp={1} truncate="end" fz={"sm"}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={100}>
|
||||||
|
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
<Image w={100} src={item.image?.link} alt="gambar" />
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text>
|
<Text>
|
||||||
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
|
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
|
||||||
<IconDeviceImac size={20} />
|
<IconDeviceImac size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
|
|
||||||
|
|
||||||
function EditPengumuman() {
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25}/>
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Title order={4}>Edit Pengumuman</Title>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
|
||||||
placeholder='Masukkan judul'
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
|
|
||||||
placeholder='Masukkan deskripsi singkat'
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Tanggal</Text>}
|
|
||||||
placeholder='Masukkan tanggal'
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Waktu</Text>}
|
|
||||||
placeholder='Masukkan waktu'
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
|
|
||||||
<KeamananEditor
|
|
||||||
showSubmit={false}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group>
|
|
||||||
<Button bg={colors['blue-button']}>Submit</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditPengumuman;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "List Pengumuman",
|
||||||
|
value: "listpengumuman",
|
||||||
|
href: "/admin/desa/pengumuman/list-pengumuman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori Pengumuman",
|
||||||
|
value: "kategoripengumuman",
|
||||||
|
href: "/admin/desa/pengumuman/kategori-pengumuman"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
|
const handleTabChange = (value: string | null) => {
|
||||||
|
const tab = tabs.find(t => t.value === value)
|
||||||
|
if (tab) {
|
||||||
|
router.push(tab.href)
|
||||||
|
}
|
||||||
|
setActiveTab(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const match = tabs.find(tab => tab.href === pathname)
|
||||||
|
if (match) {
|
||||||
|
setActiveTab(match.value)
|
||||||
|
}
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={3}>Pengumuman</Title>
|
||||||
|
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||||
|
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsPanel key={i} value={e.value}>
|
||||||
|
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||||
|
<></>
|
||||||
|
</TabsPanel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutTabsLayanan;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function EditKategoriPengumuman() {
|
||||||
|
const editState = useProxy(stateDesaPengumuman.category)
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: editState.update.form.name || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadKategori = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
name: data.name || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading kategori Pengumuman:", error);
|
||||||
|
toast.error("Gagal memuat data kategori Pengumuman");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadKategori();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
editState.update.form = {
|
||||||
|
...editState.update.form,
|
||||||
|
name: formData.name,
|
||||||
|
};
|
||||||
|
await editState.update.update();
|
||||||
|
toast.success('Kategori Pengumuman berhasil diperbarui!');
|
||||||
|
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating kategori Pengumuman:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3}>Edit Kategori Pengumuman</Title>
|
||||||
|
<TextInput
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>}
|
||||||
|
placeholder="masukkan nama kategori Pengumuman"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleSubmit}>Simpan</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditKategoriPengumuman;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
'use client'
|
||||||
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function CreateKategoriPengumuman() {
|
||||||
|
const createState = useProxy(stateDesaPengumuman.category)
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
createState.create.form = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await createState.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/desa/pengumuman/kategori-pengumuman")
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={4}>Create Kategori Pengumuman</Title>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Pengumuman</Text>}
|
||||||
|
placeholder='Masukkan nama kategori Pengumuman'
|
||||||
|
value={createState.create.form.name}
|
||||||
|
onChange={(val) => {
|
||||||
|
createState.create.form.name = val.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateKategoriPengumuman;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { IconEdit, 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 JudulList from '../../../_com/judulList';
|
||||||
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function KategoriPengumuman() {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Kategori Pengumuman'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListKategoriPengumuman search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListKategoriPengumuman({ search }: { search: string }) {
|
||||||
|
const listDataState = useProxy(stateDesaPengumuman.category)
|
||||||
|
const router = useRouter();
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listDataState.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
listDataState.delete.delete(selectedId)
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
|
||||||
|
listDataState.findMany.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.name.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!listDataState.findMany.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p="md">
|
||||||
|
<Stack>
|
||||||
|
<JudulList
|
||||||
|
title='List Kategori Pengumuman'
|
||||||
|
href='/admin/desa/pengumuman/kategori-pengumuman/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>No</TableTh>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Edit</TableTh>
|
||||||
|
<TableTh>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={100}>
|
||||||
|
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color='green' onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
color='red'
|
||||||
|
disabled={listDataState.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setModalHapus(true)
|
||||||
|
}}>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
text='Apakah anda yakin ingin menghapus kategori Pengumuman ini?'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KategoriPengumuman;
|
||||||
12
src/app/admin/(dashboard)/desa/pengumuman/layout.tsx
Normal file
12
src/app/admin/(dashboard)/desa/pengumuman/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import LayoutTabs from './_com/layoutTabs';
|
||||||
|
|
||||||
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<LayoutTabs>
|
||||||
|
{children}
|
||||||
|
</LayoutTabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
|
import stateDesaPengumuman from "@/app/admin/(dashboard)/_state/desa/pengumuman";
|
||||||
|
import colors from "@/con/colors";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconArrowBack } from "@tabler/icons-react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
|
function EditPengumuman() {
|
||||||
|
const editState = useProxy(stateDesaPengumuman);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
judul: editState.pengumuman.edit.form.judul || '',
|
||||||
|
deskripsi: editState.pengumuman.edit.form.deskripsi || '',
|
||||||
|
categoryPengumumanId: editState.pengumuman.edit.form.categoryPengumumanId || '',
|
||||||
|
content: editState.pengumuman.edit.form.content || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load pengumuman by id saat pertama kali
|
||||||
|
useEffect(() => {
|
||||||
|
editState.category.findMany.load()
|
||||||
|
const loadpengumuman = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await stateDesaPengumuman.pengumuman.edit.load(id); // akses langsung, bukan dari proxy
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
judul: data.judul || '',
|
||||||
|
deskripsi: data.deskripsi || '',
|
||||||
|
categoryPengumumanId: data.categoryPengumumanId || '',
|
||||||
|
content: data.content || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pengumuman:", error);
|
||||||
|
toast.error("Gagal memuat data pengumuman");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadpengumuman();
|
||||||
|
}, [params?.id]); // ✅ hapus editState dari dependency
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// edit global state with form data
|
||||||
|
editState.pengumuman.edit.form = {
|
||||||
|
...editState.pengumuman.edit.form,
|
||||||
|
judul: formData.judul,
|
||||||
|
deskripsi: formData.deskripsi,
|
||||||
|
content: formData.content,
|
||||||
|
categoryPengumumanId: formData.categoryPengumumanId || ''
|
||||||
|
};
|
||||||
|
await editState.pengumuman.edit.update();
|
||||||
|
toast.success("pengumuman berhasil diperbarui!");
|
||||||
|
router.push("/admin/desa/pengumuman/list-pengumuman");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating pengumuman:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat memperbarui pengumuman");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3}>Edit pengumuman</Title>
|
||||||
|
<TextInput
|
||||||
|
value={formData.judul}
|
||||||
|
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||||
|
placeholder="masukkan judul"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
||||||
|
placeholder="masukkan deskripsi"
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(htmlContent) => {
|
||||||
|
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
||||||
|
editState.pengumuman.edit.form.content = htmlContent;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={formData.categoryPengumumanId}
|
||||||
|
onChange={(val) => setFormData({ ...formData, categoryPengumumanId: val || "" })}
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleSubmit}>Edit pengumuman</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPengumuman;
|
||||||
@@ -1,40 +1,46 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Box, Button, Paper } from '@mantine/core';
|
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
|
|
||||||
function DetailPengumuman() {
|
function DetailPengumuman() {
|
||||||
// const pengumumanState = useProxy(stateDesaPengumuman)
|
const pengumumanState = useProxy(stateDesaPengumuman)
|
||||||
// const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
// const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
// const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
// pengumumanState.pengumuman.findUnique.load(params?.id as string)
|
pengumumanState.pengumuman.findUnique.load(params?.id as string)
|
||||||
// }, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
// const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
// if (selectedId) {
|
if (selectedId) {
|
||||||
// pengumumanState.pengumuman.delete.byId(selectedId)
|
pengumumanState.pengumuman.delete.byId(selectedId)
|
||||||
// setModalHapus(false)
|
setModalHapus(false)
|
||||||
// setSelectedId(null)
|
setSelectedId(null)
|
||||||
// router.push("/admin/desa/pengumuman")
|
router.push("/admin/desa/pengumuman/list-pengumuman")
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (!pengumumanState.pengumuman.findUnique.data) {
|
if (!pengumumanState.pengumuman.findUnique.data) {
|
||||||
// return (
|
return (
|
||||||
// <Stack py={10}>
|
<Stack py={10}>
|
||||||
// <Skeleton h={400} />
|
<Skeleton h={400} />
|
||||||
// </Stack>
|
</Stack>
|
||||||
// )
|
)
|
||||||
// }
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -44,7 +50,7 @@ function DetailPengumuman() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||||
{/* <Stack>
|
<Stack>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Pengumuman</Text>
|
<Text fz={"xl"} fw={"bold"}>Detail Pengumuman</Text>
|
||||||
{pengumumanState.pengumuman.findUnique.data ? (
|
{pengumumanState.pengumuman.findUnique.data ? (
|
||||||
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||||
@@ -79,11 +85,11 @@ function DetailPengumuman() {
|
|||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pengumumanState.pengumuman.findUnique.data) {
|
if (pengumumanState.pengumuman.findUnique.data) {
|
||||||
router.push(`/admin/desa/pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/pengumuman/list-pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!pengumumanState.pengumuman.findUnique.data}
|
disabled={!pengumumanState.pengumuman.findUnique.data}
|
||||||
color={"green"}
|
color={"green"}
|
||||||
>
|
>
|
||||||
@@ -93,16 +99,16 @@ function DetailPengumuman() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack> */}
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
text='Apakah anda yakin ingin menghapus pengumuman ini?'
|
||||||
/> */}
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
'use client'
|
'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 colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import CreateEditor from '../../../_com/createEditor';
|
|
||||||
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
|
||||||
|
|
||||||
function CreatePengumuman() {
|
function CreatePengumuman() {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman)
|
const pengumumanState = useProxy(stateDesaPengumuman)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
pengumumanState.category.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await pengumumanState.pengumuman.create.create()
|
await pengumumanState.pengumuman.create.create()
|
||||||
resetForm()
|
resetForm()
|
||||||
router.push("/admin/desa/pengumuman")
|
router.push("/admin/desa/pengumuman/list-pengumuman")
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -45,10 +49,21 @@ function CreatePengumuman() {
|
|||||||
pengumumanState.pengumuman.create.form.judul = val.target.value
|
pengumumanState.pengumuman.create.form.judul = val.target.value
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SelectCategory
|
<Select
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
|
placeholder='Pilih kategori'
|
||||||
|
data={pengumumanState.category.findMany.data?.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}))}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
pengumumanState.pengumuman.create.form.categoryPengumumanId = val.id;
|
const selected = pengumumanState.category.findMany.data?.find((item) => item.id === val);
|
||||||
|
if (selected) {
|
||||||
|
pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
searchable
|
||||||
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
|
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
|
||||||
@@ -76,35 +91,4 @@ function CreatePengumuman() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectCategory({
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
onChange: (value: Prisma.CategoryPengumumanGetPayload<{ select: { name: true; id: true; } }>) => void;
|
|
||||||
}) {
|
|
||||||
const categoryState = useProxy(stateDesaPengumuman.category);
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
categoryState.findMany.load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
|
||||||
placeholder='Pilih kategori'
|
|
||||||
data={categoryState.findMany.data?.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))}
|
|
||||||
onChange={(val) => {
|
|
||||||
const selected = categoryState.findMany.data?.find((item) => item.id === val);
|
|
||||||
if (selected) {
|
|
||||||
onChange(selected);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
searchable
|
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreatePengumuman;
|
export default CreatePengumuman;
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import HeaderSearch from '../../_com/header';
|
|
||||||
import stateDesaPengumuman from '../../_state/desa/pengumuman';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||||
|
|
||||||
|
|
||||||
function Pengumuman() {
|
function Pengumuman() {
|
||||||
@@ -16,7 +15,7 @@ function Pengumuman() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='List Pengumuman'
|
||||||
placeholder='pencarian'
|
placeholder='pencarian'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -30,31 +29,21 @@ function Pengumuman() {
|
|||||||
function ListPengumuman({ search }: { search: string }) {
|
function ListPengumuman({ search }: { search: string }) {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman)
|
const pengumumanState = useProxy(stateDesaPengumuman)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const {
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = pengumumanState.pengumuman.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pengumumanState.pengumuman.findMany.load()
|
load(page, 10, search)
|
||||||
}, [])
|
}, [page, search])
|
||||||
|
|
||||||
|
const filteredData = (data || [])
|
||||||
|
|
||||||
const handleHapus = () => {
|
if (loading || !data) {
|
||||||
if (selectedId) {
|
|
||||||
pengumumanState.pengumuman.delete.byId(selectedId)
|
|
||||||
setModalHapus(false)
|
|
||||||
setSelectedId(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredData = (pengumumanState.pengumuman.findMany.data || []).filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.judul.toLowerCase().includes(keyword) ||
|
|
||||||
item.CategoryPengumuman?.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pengumumanState.pengumuman.findMany.data) {
|
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
@@ -71,7 +60,7 @@ function ListPengumuman({ search }: { search: string }) {
|
|||||||
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text>
|
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button onClick={() => router.push("/admin/desa/pengumuman/create")} bg={colors['blue-button']}>
|
<Button onClick={() => router.push("/admin/desa/pengumuman/list-pengumuman/create")} bg={colors['blue-button']}>
|
||||||
<IconCircleDashedPlus size={25} />
|
<IconCircleDashedPlus size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
@@ -96,7 +85,7 @@ function ListPengumuman({ search }: { search: string }) {
|
|||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd >{item.CategoryPengumuman?.name}</TableTd>
|
<TableTd >{item.CategoryPengumuman?.name}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/detail`)}>
|
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)}>
|
||||||
<IconDeviceImacCog size={25} />
|
<IconDeviceImacCog size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -107,14 +96,15 @@ function ListPengumuman({ search }: { search: string }) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
<Pagination
|
||||||
<ModalKonfirmasiHapus
|
value={page}
|
||||||
opened={modalHapus}
|
onChange={(newPage) => load(newPage)}
|
||||||
onClose={() => setModalHapus(false)}
|
total={totalPages}
|
||||||
onConfirm={handleHapus}
|
mt="md"
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
mb="md"
|
||||||
/>
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
63
src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
Normal file
63
src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "List Potensi",
|
||||||
|
value: "list_potensi",
|
||||||
|
href: "/admin/desa/potensi/list-potensi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Kategori Potensi",
|
||||||
|
value: "kategori_potensi",
|
||||||
|
href: "/admin/desa/potensi/kategori-potensi"
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
|
const handleTabChange = (value: string | null) => {
|
||||||
|
const tab = tabs.find(t => t.value === value)
|
||||||
|
if (tab) {
|
||||||
|
router.push(tab.href)
|
||||||
|
}
|
||||||
|
setActiveTab(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const match = tabs.find(tab => tab.href === pathname)
|
||||||
|
if (match) {
|
||||||
|
setActiveTab(match.value)
|
||||||
|
}
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={3}>Potensi</Title>
|
||||||
|
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||||
|
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((e, i) => (
|
||||||
|
<TabsPanel key={i} value={e.value}>
|
||||||
|
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||||
|
<></>
|
||||||
|
</TabsPanel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutTabsPotensi;
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
|
||||||
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import CreateEditor from '../../../_com/createEditor';
|
|
||||||
|
|
||||||
|
|
||||||
function CreatePotensi() {
|
|
||||||
const potensiState = useProxy(potensiDesaState);
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
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');
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
potensiState.create.form = {
|
|
||||||
name: '',
|
|
||||||
deskripsi: '',
|
|
||||||
kategori: '',
|
|
||||||
imageId: '',
|
|
||||||
content: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
setPreviewImage(null);
|
|
||||||
setFile(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box mb={10}>
|
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={3}>Create Potensi</Title>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
value={potensiState.create.form.name}
|
|
||||||
onChange={(val) => (potensiState.create.form.name = val.target.value)}
|
|
||||||
label={<Text fz="sm" fw="bold">Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
value={potensiState.create.form.deskripsi}
|
|
||||||
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
|
|
||||||
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
|
||||||
placeholder="masukkan deskripsi"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
value={potensiState.create.form.kategori}
|
|
||||||
onChange={(val) => (potensiState.create.form.kategori = val.target.value)}
|
|
||||||
label={<Text fz="sm" fw="bold">Kategori</Text>}
|
|
||||||
placeholder="masukkan kategori"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FileInput
|
|
||||||
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
|
|
||||||
value={file}
|
|
||||||
onChange={async (e) => {
|
|
||||||
if (!e) return;
|
|
||||||
setFile(e);
|
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
|
||||||
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
|
|
||||||
);
|
|
||||||
setPreviewImage(base64);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{previewImage ? (
|
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
|
||||||
) : (
|
|
||||||
<Center w={200} h={200} bg="gray">
|
|
||||||
<IconImageInPicture />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text fz="sm" fw="bold">Konten</Text>
|
|
||||||
<CreateEditor
|
|
||||||
value={potensiState.create.form.content}
|
|
||||||
onChange={(htmlContent) => {
|
|
||||||
potensiState.create.form.content = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
|
||||||
Simpan Potensi
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreatePotensi;
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function EditKategoriPotensi() {
|
||||||
|
const editState = useProxy(potensiDesaState.kategoriPotensi)
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
nama: editState.update.form.nama || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadKategori = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
nama: data.nama || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading kategori potensi:", error);
|
||||||
|
toast.error("Gagal memuat data kategori potensi");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadKategori();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
editState.update.form = {
|
||||||
|
...editState.update.form,
|
||||||
|
nama: formData.nama,
|
||||||
|
};
|
||||||
|
await editState.update.update();
|
||||||
|
toast.success('Kategori Potensi berhasil diperbarui!');
|
||||||
|
router.push('/admin/desa/potensi/kategori-potensi');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating kategori potensi:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui kategori potensi');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={3}>Edit Kategori Potensi</Title>
|
||||||
|
<TextInput
|
||||||
|
value={formData.nama}
|
||||||
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
|
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Potensi</Text>}
|
||||||
|
placeholder="masukkan nama kategori potensi"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleSubmit}>Simpan</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditKategoriPotensi;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
'use client'
|
||||||
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function CreateKategoriPotensi() {
|
||||||
|
const createState = useProxy(potensiDesaState.kategoriPotensi)
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
createState.create.form = {
|
||||||
|
nama: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await createState.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/desa/potensi/kategori-potensi")
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={4}>Create Kategori Potensi</Title>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Potensi</Text>}
|
||||||
|
placeholder='Masukkan nama kategori Potensi'
|
||||||
|
value={createState.create.form.nama}
|
||||||
|
onChange={(val) => {
|
||||||
|
createState.create.form.nama = val.target.value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateKategoriPotensi;
|
||||||
128
src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
Normal file
128
src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { IconEdit, 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 JudulList from '../../../_com/judulList';
|
||||||
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function KategoriPotensi() {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Kategori Potensi'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListKategoriPotensi search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListKategoriPotensi({ search }: { search: string }) {
|
||||||
|
const listDataState = useProxy(potensiDesaState.kategoriPotensi)
|
||||||
|
const router = useRouter();
|
||||||
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listDataState.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
listDataState.delete.delete(selectedId)
|
||||||
|
setModalHapus(false)
|
||||||
|
setSelectedId(null)
|
||||||
|
|
||||||
|
listDataState.findMany.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.nama.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!listDataState.findMany.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton h={500} />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p="md">
|
||||||
|
<Stack>
|
||||||
|
<JudulList
|
||||||
|
title='List Kategori Potensi'
|
||||||
|
href='/admin/desa/potensi/kategori-potensi/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>No</TableTh>
|
||||||
|
<TableTh>Nama</TableTh>
|
||||||
|
<TableTh>Edit</TableTh>
|
||||||
|
<TableTh>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={100}>
|
||||||
|
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.nama}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
color='red'
|
||||||
|
disabled={listDataState.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setModalHapus(true)
|
||||||
|
}}>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
text='Apakah anda yakin ingin menghapus kategori Potensi ini?'
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KategoriPotensi;
|
||||||
14
src/app/admin/(dashboard)/desa/potensi/layout.tsx
Normal file
14
src/app/admin/(dashboard)/desa/potensi/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react';
|
||||||
|
import LayoutTabsPotensi from './_lib/layoutTabs';
|
||||||
|
|
||||||
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<LayoutTabsPotensi>
|
||||||
|
{children}
|
||||||
|
</LayoutTabsPotensi>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
|
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
|
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from "@mantine/core";
|
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||||
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { useParams } from "next/navigation";
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function EditPotensi() {
|
function EditPotensi() {
|
||||||
const potensiState = useProxy(potensiDesaState)
|
const potensiState = useProxy(potensiDesaState.potensiDesa)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
|
||||||
@@ -24,23 +25,24 @@ function EditPotensi() {
|
|||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
kategori: '',
|
kategoriId: '',
|
||||||
content: '',
|
content: '',
|
||||||
imageId: ''
|
imageId: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
potensiDesaState.kategoriPotensi.findMany.load()
|
||||||
const loadPotensi = async () => {
|
const loadPotensi = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await potensiDesaState.edit.load(id); // ambil data dari API
|
const data = await potensiState.edit.load(id); // ambil data dari API
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || '',
|
||||||
kategori: data.kategori || '',
|
kategoriId: data.kategoriId || '',
|
||||||
content: data.content || '',
|
content: data.content || '',
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || '',
|
||||||
});
|
});
|
||||||
@@ -65,7 +67,7 @@ function EditPotensi() {
|
|||||||
...potensiState.edit.form,
|
...potensiState.edit.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
deskripsi: formData.deskripsi,
|
deskripsi: formData.deskripsi,
|
||||||
kategori: formData.kategori,
|
kategoriId: formData.kategoriId,
|
||||||
content: formData.content,
|
content: formData.content,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ function EditPotensi() {
|
|||||||
|
|
||||||
await potensiState.edit.update();
|
await potensiState.edit.update();
|
||||||
toast.success("Potensi berhasil diperbarui!");
|
toast.success("Potensi berhasil diperbarui!");
|
||||||
router.push("/admin/desa/potensi");
|
router.push("/admin/desa/potensi/list-potensi");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating potensi:", error);
|
console.error("Error updating potensi:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui potensi");
|
toast.error("Terjadi kesalahan saat memperbarui potensi");
|
||||||
@@ -119,40 +121,82 @@ function EditPotensi() {
|
|||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
||||||
placeholder="masukkan deskripsi"
|
placeholder="masukkan deskripsi"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<Select
|
||||||
value={formData.kategori}
|
value={formData.kategoriId}
|
||||||
onChange={(e) => {
|
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
|
||||||
const val = e.target.value;
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
setFormData((prev) => ({ ...prev, kategori: val }));
|
placeholder='Pilih kategori'
|
||||||
potensiState.edit.form.kategori = val;
|
data={
|
||||||
}}
|
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
value: v.id,
|
||||||
placeholder="masukkan kategori"
|
label: v.nama
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
required
|
||||||
|
error={!formData.kategoriId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
|
<Box>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
<FileInput
|
<div>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
|
<Text size="xl" inline>
|
||||||
value={file}
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
onChange={async (e) => {
|
</Text>
|
||||||
if (!e) return;
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
setFile(e);
|
Maksimal 5MB dan harus format gambar
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
</Text>
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
</div>
|
||||||
);
|
</Group>
|
||||||
setPreviewImage(base64);
|
</Dropzone>
|
||||||
}}
|
|
||||||
/>
|
{/* Tampilkan preview kalau ada */}
|
||||||
{previewImage ? (
|
{previewImage && (
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
<Box mt="sm">
|
||||||
) : (
|
<Image
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
src={previewImage}
|
||||||
<IconImageInPicture />
|
alt="Preview"
|
||||||
</Center>
|
style={{
|
||||||
)}
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
||||||
potensiState.edit.form.content = htmlContent;
|
potensiState.edit.form.content = htmlContent;
|
||||||
@@ -5,18 +5,19 @@ import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import potensiDesaState from '../../../../_state/desa/potensi';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
|
|
||||||
|
|
||||||
export default function DetailPotensi() {
|
export default function DetailPotensi() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
const potensiState = useProxy(potensiDesaState)
|
const potensiState = useProxy(potensiDesaState.potensiDesa)
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
potensiState.findUnique.load(params?.id as string)
|
potensiState.findUnique.load(params?.id as string)
|
||||||
@@ -60,7 +61,7 @@ export default function DetailPotensi() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
|
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.kategori}</Text>
|
<Text fz={"lg"}>{potensiState.findUnique.data.kategori?.nama}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
|
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
|
||||||
@@ -91,7 +92,7 @@ export default function DetailPotensi() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (potensiState.findUnique.data) {
|
if (potensiState.findUnique.data) {
|
||||||
router.push(`/admin/desa/potensi/edit/${potensiState.findUnique.data.id}`)
|
router.push(`/admin/desa/potensi/list-potensi/${potensiState.findUnique.data.id}/edit`)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!potensiState.findUnique.data}
|
disabled={!potensiState.findUnique.data}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
'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 } 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<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(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 (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Title order={3}>Create Potensi</Title>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={potensiState.create.form.name}
|
||||||
|
onChange={(val) => (potensiState.create.form.name = val.target.value)}
|
||||||
|
label={<Text fz="sm" fw="bold">Judul</Text>}
|
||||||
|
placeholder="masukkan judul"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={potensiState.create.form.deskripsi}
|
||||||
|
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
|
||||||
|
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
||||||
|
placeholder="masukkan deskripsi"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||||
|
placeholder='Pilih kategori'
|
||||||
|
value={potensiState.create.form.kategoriId || ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
potensiState.create.form.kategoriId = val ?? "";
|
||||||
|
}}
|
||||||
|
data={potensiDesaState.kategoriPotensi.findMany.data?.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.nama,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
|
<Box>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* Tampilkan preview kalau ada */}
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '200px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">Konten</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={potensiState.create.form.content}
|
||||||
|
onChange={(htmlContent) => {
|
||||||
|
potensiState.create.form.content = htmlContent;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
||||||
|
Simpan Potensi
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreatePotensi;
|
||||||
158
src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
Normal file
158
src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
|
import JudulList from '../../../_com/judulList';
|
||||||
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function Potensi() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<HeaderSearch
|
||||||
|
title='Posisi Organisasi'
|
||||||
|
placeholder='pencarian'
|
||||||
|
searchIcon={<IconSearch size={20} />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<ListPotensi search={search} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListPotensi({ search }: { search: string }) {
|
||||||
|
const potensiState = useProxy(potensiDesaState)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = potensiState.potensiDesa.findMany;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
potensiState.kategoriPotensi.findMany.load()
|
||||||
|
load(page, 10)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const filteredData = (potensiState.potensiDesa.findMany.data || []).filter(item => {
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.name.toLowerCase().includes(keyword) ||
|
||||||
|
item.kategori?.nama.toLowerCase().includes(keyword) ||
|
||||||
|
item.deskripsi.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle loading state
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={300} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack>
|
||||||
|
<JudulList
|
||||||
|
title='List Potensi'
|
||||||
|
href='/admin/desa/potensi/list-potensi/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Judul</TableTh>
|
||||||
|
<TableTh>Kategori</TableTh>
|
||||||
|
<TableTh>Deskripsi</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>Tidak Ada Data</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack>
|
||||||
|
<JudulList
|
||||||
|
title='List Potensi'
|
||||||
|
href='/admin/desa/potensi/list-potensi/create'
|
||||||
|
/>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Judul</TableTh>
|
||||||
|
<TableTh>Kategori</TableTh>
|
||||||
|
<TableTh>Deskripsi</TableTh>
|
||||||
|
<TableTh>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={100}>
|
||||||
|
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||||
|
</Box></TableTd>
|
||||||
|
<TableTd>{item.kategori?.nama}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={300}>
|
||||||
|
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}>
|
||||||
|
<IconDeviceImacCog size={25} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Potensi;
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
|
|
||||||
'use client'
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import HeaderSearch from '../../_com/header';
|
|
||||||
import JudulList from '../../_com/judulList';
|
|
||||||
import potensiDesaState from '../../_state/desa/potensi';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
|
|
||||||
function Potensi() {
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<HeaderSearch
|
|
||||||
title='Posisi Organisasi'
|
|
||||||
placeholder='pencarian'
|
|
||||||
searchIcon={<IconSearch size={20} />}
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<ListPotensi search={search} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListPotensi({ search }: { search: string }) {
|
|
||||||
const potensiState = useProxy(potensiDesaState)
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
potensiState.findMany.load()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const filteredData = (potensiState.findMany.data || []).filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.kategori.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!potensiState.findMany.data) {
|
|
||||||
return (
|
|
||||||
<Stack py={10}>
|
|
||||||
<Skeleton h={500} />
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<JudulList
|
|
||||||
title='List Potensi'
|
|
||||||
href='/admin/desa/potensi/create'
|
|
||||||
/>
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh>Judul</TableTh>
|
|
||||||
<TableTh>Kategori</TableTh>
|
|
||||||
<TableTh>Image</TableTh>
|
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
|
||||||
<TableTd>
|
|
||||||
<Box w={100}>
|
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
|
||||||
</Box></TableTd>
|
|
||||||
<TableTd>{item.kategori}</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Image w={100} src={item.image?.link} alt="image" />
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button onClick={() => router.push(`/admin/desa/potensi/detail/${item.id}`)}>
|
|
||||||
<IconDeviceImacCog size={25} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
))}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Potensi;
|
|
||||||
@@ -18,6 +18,11 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
label: "Profile Perbekel",
|
label: "Profile Perbekel",
|
||||||
value: "profileperbekel",
|
value: "profileperbekel",
|
||||||
href: "/admin/desa/profile/profile-perbekel"
|
href: "/admin/desa/profile/profile-perbekel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Profile Perbekel Dari Masa Ke Masa",
|
||||||
|
value: "profile-perbekel-dari-masa-ke-masa",
|
||||||
|
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ function Page() {
|
|||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||||
<Box w={{ base: '100%', md: '50%' }}>
|
<Box w={{ base: '100%', md: '50%' }}>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const newImages = files.map((file) => ({
|
const newImages = files.map((file) => ({
|
||||||
file,
|
file,
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
'use client'
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
function EditPerbekelDariMasaKeMasa() {
|
||||||
|
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
nama: state.update.form.nama || '',
|
||||||
|
daerah: state.update.form.daerah || '',
|
||||||
|
periode: state.update.form.periode || '',
|
||||||
|
imageId: state.update.form.imageId || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadFoto = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
const data = await state.update.load(id);
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
nama: data.nama || '',
|
||||||
|
daerah: data.daerah || '',
|
||||||
|
periode: data.periode || '',
|
||||||
|
imageId: data.imageId || ''
|
||||||
|
});
|
||||||
|
if (data?.imageGalleryFoto?.link) {
|
||||||
|
setPreviewImage(data.imageGalleryFoto.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading foto:', error);
|
||||||
|
toast.error('Gagal memuat data foto');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadFoto();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
state.update.form = {
|
||||||
|
...state.update.form,
|
||||||
|
nama: formData.nama,
|
||||||
|
daerah: formData.daerah,
|
||||||
|
periode: formData.periode,
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
state.update.form.imageId = uploaded.id;
|
||||||
|
}
|
||||||
|
await state.update.update();
|
||||||
|
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
||||||
|
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating perbekel dari masa ke masa:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box mb={10}>
|
||||||
|
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Title order={4}>Edit Perbekel Dari Masa Ke Masa</Title>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>}
|
||||||
|
placeholder='Masukkan nama'
|
||||||
|
value={formData.nama}
|
||||||
|
onChange={(e) =>
|
||||||
|
(formData.nama = e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Text>Upload Foto</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0]; // Ambil file pertama
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid.')}
|
||||||
|
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" inline>
|
||||||
|
Drag gambar ke sini atau klik untuk pilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" inline mt={7}>
|
||||||
|
Maksimal 5MB dan harus format gambar
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage ? (
|
||||||
|
<Image alt="" src={previewImage} w={200} h={200} />
|
||||||
|
) : (
|
||||||
|
<Center w={200} h={200} bg={"gray"}>
|
||||||
|
<IconImageInPicture />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Daerah</Text>}
|
||||||
|
placeholder='Masukkan daerah'
|
||||||
|
value={formData.daerah}
|
||||||
|
onChange={(e) =>
|
||||||
|
(formData.daerah = e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw={"bold"} fz={"sm"}>Periode</Text>}
|
||||||
|
placeholder='Masukkan periode'
|
||||||
|
value={formData.periode}
|
||||||
|
onChange={(e) =>
|
||||||
|
(formData.periode = e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPerbekelDariMasaKeMasa;
|
||||||
@@ -1,40 +1,38 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function DetailPerbekelDariMasa() {
|
||||||
function DetailListBulanan() {
|
const state = useProxy(stateProfileDesa.mantanPerbekel)
|
||||||
const detailState = useProxy(indeksKepuasanState.monthlyStatState)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
detailState.findUnique.load(params?.id as string)
|
state.findUnique.load(params?.id as string)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
detailState.delete.byId(selectedId)
|
state.delete.byId(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
|
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!detailState.findUnique.data) {
|
if (!state.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton h={500} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -48,42 +46,46 @@ function DetailListBulanan() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
|
<Text fz={"xl"} fw={"bold"}>Detail Perbekel Dari Masa Ke Masa</Text>
|
||||||
{detailState.findUnique.data ? (
|
{state.findUnique.data ? (
|
||||||
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Bulan</Text>
|
<Text fw={"bold"} fz={"lg"}>Nama Perbekel</Text>
|
||||||
<Text fz={"lg"}>{detailState.findUnique.data?.month}</Text>
|
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
|
<Text fw={"bold"} fz={"lg"}>Daerah</Text>
|
||||||
<Text fz={"lg"}>{detailState.findUnique.data?.respondentsCount}</Text>
|
<Text fz={"lg"}>{state.findUnique.data?.daerah}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Survey</Text>
|
<Text fw={"bold"} fz={"lg"}>Periode</Text>
|
||||||
<Text fz={"lg"}>{detailState.findUnique.data?.survey.title }</Text>
|
<Text fz={"lg"}>{state.findUnique.data?.periode}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||||
|
<Image w={{ base: 300, md: 350}} src={state.findUnique.data?.image?.link} alt="gambar" />
|
||||||
</Box>
|
</Box>
|
||||||
<Flex gap={"xs"} mt={10}>
|
<Flex gap={"xs"} mt={10}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (detailState.findUnique.data) {
|
if (state.findUnique.data) {
|
||||||
setSelectedId(detailState.findUnique.data.id);
|
setSelectedId(state.findUnique.data.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
disabled={state.delete.loading || !state.findUnique.data}
|
||||||
color={"red"}
|
color={"red"}
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (detailState.findUnique.data) {
|
if (state.findUnique.data) {
|
||||||
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${detailState.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${state.findUnique.data.id}/edit`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!detailState.findUnique.data}
|
disabled={!state.findUnique.data}
|
||||||
color={"green"}
|
color={"green"}
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
@@ -100,10 +102,10 @@ function DetailListBulanan() {
|
|||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus list bulanan ini?'
|
text='Apakah anda yakin ingin menghapus perbekel dari masa ke masa ini?'
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailListBulanan;
|
export default DetailPerbekelDariMasa;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user