Compare commits
34 Commits
nico/13-au
...
nico/4-9-2
| Author | SHA1 | Date | |
|---|---|---|---|
| 8817b937b1 | |||
| 2adf60f9eb | |||
| fa9601e126 | |||
| 7ae83788b4 | |||
| 22ec8d942d | |||
| 9f9a0fb451 | |||
| b6d6583e77 | |||
| a8fd715822 | |||
| f9530c32eb | |||
| f15ef5a275 | |||
| 3a726a3334 | |||
| b21e1f0c2e | |||
| f63249327d | |||
| bb8dab05ba | |||
| 3081e426bd | |||
| 8a275c2a32 | |||
| 8469ebd2e1 | |||
| 760ba4b6d2 | |||
| 20d4c90e60 | |||
| fafbb12a08 | |||
| 01aa0da5cc | |||
| b580978f8e | |||
| 1c01397c0d | |||
| 90a6605efd | |||
| c22d865283 | |||
| 49067f0218 | |||
| d79425d529 | |||
| 4491d23bea | |||
| 1e154ced86 | |||
| bcc51aec12 | |||
| 8d15563f15 | |||
| d7a592c635 | |||
| 5e137ba658 | |||
| c99416c7f8 |
@@ -57,6 +57,8 @@
|
|||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"framer-motion": "^12.23.5",
|
"framer-motion": "^12.23.5",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
|
"iron-session": "^8.0.4",
|
||||||
|
"jose": "^6.1.0",
|
||||||
"jotai": "^2.12.3",
|
"jotai": "^2.12.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"motion": "^12.4.1",
|
"motion": "^12.4.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "15.1.6",
|
"next": "^15.5.2",
|
||||||
"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",
|
||||||
|
|||||||
@@ -3,126 +3,108 @@
|
|||||||
"id": "cmds9h9ko000pvnberdjnx64b",
|
"id": "cmds9h9ko000pvnberdjnx64b",
|
||||||
"name": "1.1 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA",
|
"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>",
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA</p>",
|
||||||
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmds9sjmz000svnbesv2133of",
|
"id": "cmds9sjmz000svnbesv2133of",
|
||||||
"name": "1.2 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA",
|
"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>",
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA</p>",
|
||||||
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmds9tcpi000vvnbev3ebtlnt",
|
"id": "cmds9tcpi000vvnbev3ebtlnt",
|
||||||
"name": "1.3 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
|
"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>",
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN</p>",
|
||||||
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmds9twvj000yvnbep0pq8dzf",
|
"id": "cmds9twvj000yvnbep0pq8dzf",
|
||||||
"name": "1.4 PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA",
|
"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>",
|
"deskripsi": "<p>PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA</p>",
|
||||||
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmds9ugap0011vnbe118yv871",
|
"id": "cmds9ugap0011vnbe118yv871",
|
||||||
"name": "1.5 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA",
|
"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>",
|
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA</p>",
|
||||||
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
|
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsa35310014vnbe6qy6l1rz",
|
"id": "cmdsa35310014vnbe6qy6l1rz",
|
||||||
"name": "2.1 ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA",
|
"name": "2.1 ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA",
|
||||||
"deskripsi": "<p>ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA</p>",
|
"deskripsi": "<p>ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA</p>",
|
||||||
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
|
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsa46590017vnbepp3noso1",
|
"id": "cmdsa46590017vnbepp3noso1",
|
||||||
"name": "2.2 ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH",
|
"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>",
|
"deskripsi": "<p>ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH</p>",
|
||||||
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
|
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsa5m7z001avnbe4cvfrcz0",
|
"id": "cmdsa5m7z001avnbe4cvfrcz0",
|
||||||
"name": "2.3 TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI",
|
"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>",
|
"deskripsi": "<p>TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI</p>",
|
||||||
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
|
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsa8q5q001dvnbemch8j24x",
|
"id": "cmdsa8q5q001dvnbemch8j24x",
|
||||||
"name": "3.1 ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT",
|
"name": "3.1 ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT",
|
||||||
"deskripsi": "<p>ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT</p>",
|
"deskripsi": "<p>ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT</p>",
|
||||||
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsa9lbi001gvnbequn2ba7m",
|
"id": "cmdsa9lbi001gvnbequn2ba7m",
|
||||||
"name": "3.2 ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA",
|
"name": "3.2 ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA",
|
||||||
"deskripsi": "<p>ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA</p>",
|
"deskripsi": "<p>ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA</p>",
|
||||||
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsaa7aq001jvnbeizh04e67",
|
"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",
|
"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>",
|
"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",
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsaaw8d001mvnbek3tfefrk",
|
"id": "cmdsaaw8d001mvnbek3tfefrk",
|
||||||
"name": "3.4 ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT",
|
"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>",
|
"deskripsi": "<p>ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT</p>",
|
||||||
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsabhif001pvnbepm06hry6",
|
"id": "cmdsabhif001pvnbepm06hry6",
|
||||||
"name": "3.5 ADANYA MAKLUMAT PELAYANAN",
|
"name": "3.5 ADANYA MAKLUMAT PELAYANAN",
|
||||||
"deskripsi": "<p>ADANYA MAKLUMAT PELAYANAN</p>",
|
"deskripsi": "<p>ADANYA MAKLUMAT PELAYANAN</p>",
|
||||||
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
|
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsag40b001svnbe7krq9khc",
|
"id": "cmdsag40b001svnbe7krq9khc",
|
||||||
"name": "4.1 ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA",
|
"name": "4.1 ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA",
|
||||||
"deskripsi": "<p>ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA</p>",
|
"deskripsi": "<p>ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA</p>",
|
||||||
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
|
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsagkaf001vvnbejo26w8sa",
|
"id": "cmdsagkaf001vvnbejo26w8sa",
|
||||||
"name": "4.2 ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
|
"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>",
|
"deskripsi": "<p>ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN</p>",
|
||||||
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
|
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsah4qe001yvnbeiy3mwrvb",
|
"id": "cmdsah4qe001yvnbeiy3mwrvb",
|
||||||
"name": "4.3 ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA",
|
"name": "4.3 ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA",
|
||||||
"deskripsi": "<p>ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA</p>",
|
"deskripsi": "<p>ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA</p>",
|
||||||
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
|
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsak5vn0021vnbemg86aab4",
|
"id": "cmdsak5vn0021vnbemg86aab4",
|
||||||
"name": "5.1 ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
|
"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>",
|
"deskripsi": "<p>ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI</p>",
|
||||||
"kategoriId": "cmds9govb000mvnbesq8b4y99",
|
"kategoriId": "cmds9govb000mvnbesq8b4y99"
|
||||||
"fileId": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmdsalc800024vnbezgulhgrb",
|
"id": "cmdsalc800024vnbezgulhgrb",
|
||||||
"name": "5.2 ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
|
"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>",
|
"deskripsi": "<p>ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI</p>",
|
||||||
"kategoriId": "cmds9govb000mvnbesq8b4y99",
|
"kategoriId": "cmds9govb000mvnbesq8b4y99"
|
||||||
"fileId": ""
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "cmeppcwzk0000vn5exmudcipd",
|
||||||
"jenisInformasi": "Peraturan Desa",
|
"jenisInformasi": "Potensi Desa",
|
||||||
"deskripsi": "Dokumen yang berisi kebijakan dan regulasi desa",
|
"deskripsi": "<p>“Potensi desa adalah segenap sumber daya alam dan sumber daya manusia yang dimiliki desa sebagai modal dasar yang perlu dikelola dan dikembangkan bagi kelangsungan dan perkembangan desa. Adapun potensi yang dimiliki Desa Darmasaba yaitu:</p><ol><li><p>TPS3R Pudak Mesari</p></li><li><p>Bumdes Pudak Mesari</p></li><li><p>Pertanian</p></li><li><p>Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa</p></li><li><p>Taman Beji Cengana</p></li><li><p>Dam Tanah Putih</p></li><li><p>Gumuh Sari Water Park</p></li><li><p>UMKM</p></li><li><p>Kawasan Kuliner</p></li><li><p>IKM berbasis Pengolahan Pangan</p></li><li><p>Genteng</p></li><li><p>Peternakan Ikan Lele</p></li><li><p>Pemotongan Daging”</p></li></ol>",
|
||||||
"tanggal": "15 Januari 2024"
|
"tanggal": "2021-05-25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmeppieay0001vn5e8qe658ub",
|
||||||
|
"jenisInformasi": "Layanan Surat Keterangan Desa",
|
||||||
|
"deskripsi": "<p>“Desa Darmasaba menyediakan berbagai jenis layanan surat keterangan untuk kebutuhan administratif, antara lain:</p><ul><li><p>Surat Keterangan Domisili Organisasi</p></li><li><p>Surat Keterangan Penghasilan</p></li><li><p>Surat Keterangan Tidak Mampu</p></li><li><p>Surat Keterangan Kelahiran</p></li><li><p>Surat Keterangan Usaha</p></li><li><p>Surat Keterangan Tempat Usaha</p></li><li><p>Surat Keterangan Belum Kawin</p></li><li><p>Surat Keterangan Kelakuan Baik (Pengantar SKCK)</p></li><li><p>Surat Keterangan Kematian</p></li><li><p>Surat Keterangan Perbedaan Biodata Diri</p></li><li><p>Surat Keterangan Yatim/Piatu/Yatim Piatu<br>Untuk surat keterangan lainnya, masyarakat dapat berkonsultasi langsung ke kantor Perbekel Darmasaba.”<br><em>(Sumber: Laman Layanan Desa Darmasaba)</em></p></li></ul>",
|
||||||
|
"tanggal": "2025-02-21"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "cmdpm429r0000vnndkcwslt0h",
|
|
||||||
"name": "warga"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
29
prisma/data/user/roles.json
Normal file
29
prisma/data/user/roles.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "ADMIN DESA",
|
||||||
|
"description": "Administrator Desa",
|
||||||
|
"permissions": ["manage_users", "manage_content", "view_reports"],
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "ADMIN KESEHATAN",
|
||||||
|
"description": "Administrator Bidang Kesehatan",
|
||||||
|
"permissions": ["manage_health_data", "view_reports"],
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "ADMIN SEKOLAH",
|
||||||
|
"description": "Administrator Sekolah",
|
||||||
|
"permissions": ["manage_school_data", "view_reports"],
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
32
prisma/data/user/users.json
Normal file
32
prisma/data/user/users.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"nama": "Admin Desa",
|
||||||
|
"nomor": "089647037426",
|
||||||
|
"roleId": "1",
|
||||||
|
"isActive": true,
|
||||||
|
"lastLogin": "2025-08-31T10:00:00.000Z",
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"nama": "Admin Kesehatan",
|
||||||
|
"nomor": "082339004198",
|
||||||
|
"roleId": "2",
|
||||||
|
"isActive": true,
|
||||||
|
"lastLogin": null,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"nama": "Admin Sekolah",
|
||||||
|
"nomor": "085237157222",
|
||||||
|
"roleId": "3",
|
||||||
|
"isActive": true,
|
||||||
|
"lastLogin": null,
|
||||||
|
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||||
|
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -85,7 +85,6 @@ model FileStorage {
|
|||||||
KontakItem KontakItem[]
|
KontakItem KontakItem[]
|
||||||
Pegawai Pegawai[]
|
Pegawai Pegawai[]
|
||||||
DesaDigital DesaDigital[]
|
DesaDigital DesaDigital[]
|
||||||
KolaborasiInovasi KolaborasiInovasi[]
|
|
||||||
InfoTekno InfoTekno[]
|
InfoTekno InfoTekno[]
|
||||||
PengaduanMasyarakat PengaduanMasyarakat[]
|
PengaduanMasyarakat PengaduanMasyarakat[]
|
||||||
KegiatanDesa KegiatanDesa[]
|
KegiatanDesa KegiatanDesa[]
|
||||||
@@ -100,6 +99,8 @@ model FileStorage {
|
|||||||
DataPerpustakaan DataPerpustakaan[]
|
DataPerpustakaan DataPerpustakaan[]
|
||||||
PegawaiPPID PegawaiPPID[]
|
PegawaiPPID PegawaiPPID[]
|
||||||
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
|
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
|
||||||
|
|
||||||
|
MitraKolaborasi MitraKolaborasi[]
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -201,8 +202,8 @@ model PrestasiDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
|
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String
|
kategoriId 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())
|
||||||
@@ -223,7 +224,7 @@ model KategoriPrestasiDesa {
|
|||||||
model Responden {
|
model Responden {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
tanggal DateTime // misal: 2025-05-01
|
tanggal DateTime @db.Date // misal: 2025-05-01
|
||||||
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
|
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
|
||||||
jenisKelaminId String
|
jenisKelaminId String
|
||||||
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
|
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
|
||||||
@@ -292,6 +293,9 @@ model PosisiOrganisasiPPID {
|
|||||||
pegawai PegawaiPPID[]
|
pegawai PegawaiPPID[]
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
parentId String?
|
parentId String?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
}
|
}
|
||||||
@@ -911,26 +915,56 @@ 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 ========================================= //
|
||||||
model GrafikKepuasan {
|
model GrafikKepuasan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
label String
|
nama String
|
||||||
jumlah String
|
tanggal DateTime
|
||||||
createdAt DateTime @default(now())
|
jenisKelamin String
|
||||||
updatedAt DateTime @updatedAt
|
alamat String
|
||||||
deletedAt DateTime @default(now())
|
penyakit String
|
||||||
isActive Boolean @default(true)
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= ARTIKEL KESEHATAN ========================================= //
|
// ========================================= ARTIKEL KESEHATAN ========================================= //
|
||||||
@@ -1027,16 +1061,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 ========================================= //
|
||||||
@@ -1516,7 +1551,7 @@ model DataDemografiPekerjaan {
|
|||||||
model DetailDataPengangguran {
|
model DetailDataPengangguran {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
month String @db.VarChar(20)
|
month String @db.VarChar(20)
|
||||||
year Int
|
year DateTime
|
||||||
totalUnemployment Int
|
totalUnemployment Int
|
||||||
educatedUnemployment Int
|
educatedUnemployment Int
|
||||||
uneducatedUnemployment Int
|
uneducatedUnemployment Int
|
||||||
@@ -1604,18 +1639,27 @@ model ProgramKreatif {
|
|||||||
|
|
||||||
// ========================================= KOLABORASI INOVASI ========================================= //
|
// ========================================= KOLABORASI INOVASI ========================================= //
|
||||||
model KolaborasiInovasi {
|
model KolaborasiInovasi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
tahun Int
|
tahun Int
|
||||||
slug String @db.Text //deskripsi singkat
|
slug String @db.Text //deskripsi singkat
|
||||||
deskripsi String @db.Text //deskripsi panjang
|
deskripsi String @db.Text //deskripsi panjang
|
||||||
kolaborator String
|
kolaborator 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)
|
|
||||||
|
model MitraKolaborasi {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
|
imageId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
|
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
|
||||||
@@ -2059,26 +2103,66 @@ model KategoriBuku {
|
|||||||
DataPerpustakaan DataPerpustakaan[]
|
DataPerpustakaan DataPerpustakaan[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================= USER ========================================= //
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
nama String
|
username String
|
||||||
email String @unique
|
nomor String @unique
|
||||||
password String
|
role Role @relation(fields: [roleId], references: [id])
|
||||||
role Role @relation(fields: [roleId], references: [id])
|
roleId String @default("1")
|
||||||
roleId String
|
instansi String?
|
||||||
isActive Boolean @default(true)
|
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||||
createdAt DateTime @default(now())
|
isActive Boolean @default(true)
|
||||||
updatedAt DateTime @updatedAt
|
lastLogin DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
||||||
|
description String?
|
||||||
|
permissions Json // Menyimpan permission dalam format JSON
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
users User[]
|
||||||
|
|
||||||
|
@@map("roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
model KodeOtp {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
nomor String
|
||||||
isActive Boolean @default(true)
|
otp Int
|
||||||
User User[]
|
}
|
||||||
|
|
||||||
|
// Tabel untuk menyimpan permission
|
||||||
|
model Permission {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
description String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
token String
|
||||||
|
expires DateTime?
|
||||||
|
active Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= DATA PENDIDIKAN ========================================= //
|
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||||
|
|||||||
452
prisma/seed.ts
452
prisma/seed.ts
@@ -1,57 +1,119 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import profilePejabatDesa from "./data/landing-page/profile/profile.json";
|
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 programInovasi from "./data/landing-page/profile/programInovasi.json";
|
||||||
import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
|
import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
|
||||||
|
import desaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/desaantiKorpusi.json";
|
||||||
|
import kategoriDesaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json";
|
||||||
import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json";
|
import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json";
|
||||||
import apbdes from "./data/landing-page/apbdes/apbdes.json";
|
import apbdes from "./data/landing-page/apbdes/apbdes.json";
|
||||||
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
|
import kategoriPrestasiDesa from "./data/landing-page/prestasi-desa/kategori-prestasi.json";
|
||||||
import categoryPengumuman from "./data/category-pengumuman.json";
|
import prestasiDesa from "./data/landing-page/prestasi-desa/prestasi-desa.json";
|
||||||
import kategoriBerita from "./data/kategori-berita.json";
|
import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
|
||||||
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
|
||||||
import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json";
|
|
||||||
import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
|
|
||||||
import layanan from "./data/list-layanan.json";
|
|
||||||
import potensi from "./data/list-potensi.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 pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
|
||||||
|
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
|
||||||
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
|
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
|
||||||
|
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
|
||||||
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
|
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
|
||||||
|
import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json"
|
||||||
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
|
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
|
||||||
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
|
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
|
||||||
|
import categoryPengumuman from "./data/category-pengumuman.json";
|
||||||
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
|
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
|
||||||
|
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
|
||||||
|
import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
|
||||||
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
|
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
|
||||||
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
|
|
||||||
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
|
|
||||||
import lambangDesa from "./data/desa/profile/lambang_desa.json";
|
import lambangDesa from "./data/desa/profile/lambang_desa.json";
|
||||||
import maskotDesa from "./data/desa/profile/maskot_desa.json";
|
import maskotDesa from "./data/desa/profile/maskot_desa.json";
|
||||||
import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
|
import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
|
||||||
|
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
|
||||||
|
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
|
||||||
|
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
|
||||||
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
|
import 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 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 posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
|
||||||
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
import kategoriBerita from "./data/kategori-berita.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 materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||||
|
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||||
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
import 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 tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||||
|
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
|
||||||
|
import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json";
|
||||||
|
import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
|
||||||
|
import potensi from "./data/list-potensi.json";
|
||||||
|
import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
|
||||||
|
import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
|
||||||
|
import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
|
||||||
|
import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
|
||||||
|
import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
|
||||||
import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
import 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 tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||||
import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
|
import roles from "./data/user/roles.json";
|
||||||
import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
|
import users from "./data/user/users.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 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 () => {
|
||||||
|
// =========== USER & ROLE ===========
|
||||||
|
// In your seed.ts
|
||||||
|
// =========== ROLES ===========
|
||||||
|
console.log("🔄 Seeding roles...");
|
||||||
|
for (const r of roles) {
|
||||||
|
await prisma.role.upsert({
|
||||||
|
where: { id: r.id },
|
||||||
|
update: {
|
||||||
|
name: r.name,
|
||||||
|
description: r.description,
|
||||||
|
permissions: r.permissions,
|
||||||
|
isActive: r.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
description: r.description,
|
||||||
|
permissions: r.permissions,
|
||||||
|
isActive: r.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Roles seeded");
|
||||||
|
|
||||||
|
// =========== USERS ===========
|
||||||
|
console.log("🔄 Seeding users...");
|
||||||
|
for (const u of users) {
|
||||||
|
// First verify the role exists
|
||||||
|
const roleExists = await prisma.role.findUnique({
|
||||||
|
where: { id: u.roleId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!roleExists) {
|
||||||
|
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: u.id },
|
||||||
|
update: {
|
||||||
|
username: u.nama,
|
||||||
|
nomor: u.nomor,
|
||||||
|
roleId: u.roleId,
|
||||||
|
isActive: u.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: u.id,
|
||||||
|
username: u.nama,
|
||||||
|
nomor: u.nomor,
|
||||||
|
roleId: u.roleId,
|
||||||
|
isActive: u.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Users seeded");
|
||||||
// =========== LANDING PAGE ===========
|
// =========== LANDING PAGE ===========
|
||||||
// =========== PROFILE ===========
|
// =========== SUBMENU PROFILE ===========
|
||||||
|
// =========== PROFILE PEJABAT DESA ===========
|
||||||
for (const p of profilePejabatDesa) {
|
for (const p of profilePejabatDesa) {
|
||||||
await prisma.pejabatDesa.upsert({
|
await prisma.pejabatDesa.upsert({
|
||||||
where: { id: p.id },
|
where: { id: p.id },
|
||||||
@@ -106,6 +168,90 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
}
|
}
|
||||||
console.log("media sosial success ...");
|
console.log("media sosial success ...");
|
||||||
|
|
||||||
|
// =========== SUBMENU DESA ANTI KORUPSI ===========
|
||||||
|
// =========== KATEGORI DESA ANTI KORUPSI ===========
|
||||||
|
for (const k of kategoriDesaAntiKorupsi) {
|
||||||
|
await prisma.kategoriDesaAntiKorupsi.upsert({
|
||||||
|
where: { id: k.id },
|
||||||
|
update: {
|
||||||
|
name: k.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
name: k.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("kategori desa anti korupsi success ...");
|
||||||
|
|
||||||
|
// =========== DESA ANTI KORUPSI ===========
|
||||||
|
for (const p of desaAntiKorupsi) {
|
||||||
|
await prisma.desaAntiKorupsi.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
kategoriId: p.kategoriId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
kategoriId: p.kategoriId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("desa anti korupsi success ...");
|
||||||
|
|
||||||
|
// =========== KATEGORI DESA ANTI KORUPSI ===========
|
||||||
|
for (const p of kategoriDesaAntiKorupsi) {
|
||||||
|
await prisma.kategoriDesaAntiKorupsi.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("desa anti korupsi success ...");
|
||||||
|
|
||||||
|
// =========== KATEGORI PRESTASI DESA===========
|
||||||
|
for (const c of kategoriPrestasiDesa) {
|
||||||
|
await prisma.kategoriPrestasiDesa.upsert({
|
||||||
|
where: { id: c.id },
|
||||||
|
update: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("kategori prestasi desa success ...");
|
||||||
|
|
||||||
|
// =========== PRESTASI DESA===========
|
||||||
|
for (const p of prestasiDesa) {
|
||||||
|
await prisma.prestasiDesa.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
kategoriId: p.kategoriId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
kategoriId: p.kategoriId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("prestasi desa success ...");
|
||||||
|
|
||||||
// =========== PENGHARGAAN ===========
|
// =========== PENGHARGAAN ===========
|
||||||
for (const p of penghargaan) {
|
for (const p of penghargaan) {
|
||||||
await prisma.penghargaan.upsert({
|
await prisma.penghargaan.upsert({
|
||||||
@@ -125,8 +271,8 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
}
|
}
|
||||||
console.log("penghargaan success ...");
|
console.log("penghargaan success ...");
|
||||||
|
|
||||||
// =========== LAYANAN DESA ===========
|
// =========== LAYANAN DESA ===========
|
||||||
for (const p of pelayananSuratKeterangan) {
|
for (const p of pelayananSuratKeterangan) {
|
||||||
await prisma.pelayananSuratKeterangan.upsert({
|
await prisma.pelayananSuratKeterangan.upsert({
|
||||||
where: { id: p.id },
|
where: { id: p.id },
|
||||||
update: {
|
update: {
|
||||||
@@ -160,23 +306,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
}
|
}
|
||||||
console.log("pelayanan surat keterangan success ...");
|
console.log("pelayanan surat keterangan success ...");
|
||||||
|
|
||||||
// =========== LAYANAN ===========
|
|
||||||
for (const l of layanan) {
|
|
||||||
await prisma.layanan.upsert({
|
|
||||||
where: {
|
|
||||||
name: l.name,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: l.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
name: l.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("layanan success ...");
|
|
||||||
|
|
||||||
// =========== SDGSDesa ===========
|
// =========== SDGSDesa ===========
|
||||||
for (const l of sdgsDesa) {
|
for (const l of sdgsDesa) {
|
||||||
await prisma.sDGSDesa.upsert({
|
await prisma.sDGSDesa.upsert({
|
||||||
@@ -217,6 +346,9 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
|
|
||||||
console.log("sdgs desa success ...");
|
console.log("sdgs desa success ...");
|
||||||
|
|
||||||
|
// =========== MENU DESA ===========
|
||||||
|
// =========== SUBMENU PROFILE ===========
|
||||||
|
// =========== SEJARAH DESA ===========
|
||||||
for (const l of sejarahDesa) {
|
for (const l of sejarahDesa) {
|
||||||
await prisma.sejarahDesa.upsert({
|
await prisma.sejarahDesa.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -236,6 +368,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
|
|
||||||
console.log("sejarah desa success ...");
|
console.log("sejarah desa success ...");
|
||||||
|
|
||||||
|
// =========== MASKOT DESA ===========
|
||||||
for (const l of maskotDesa) {
|
for (const l of maskotDesa) {
|
||||||
await prisma.maskotDesa.upsert({
|
await prisma.maskotDesa.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -255,6 +388,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
|
|
||||||
console.log("maskot desa success ...");
|
console.log("maskot desa success ...");
|
||||||
|
|
||||||
|
// =========== LAMBANG DESA ===========
|
||||||
for (const l of lambangDesa) {
|
for (const l of lambangDesa) {
|
||||||
await prisma.lambangDesa.upsert({
|
await prisma.lambangDesa.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -274,6 +408,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
|
|
||||||
console.log("lambang desa success ...");
|
console.log("lambang desa success ...");
|
||||||
|
|
||||||
|
// =========== PROFIL PERBEKEL ===========
|
||||||
for (const c of profilPerbekel) {
|
for (const c of profilPerbekel) {
|
||||||
await prisma.profilPerbekel.upsert({
|
await prisma.profilPerbekel.upsert({
|
||||||
where: { id: c.id },
|
where: { id: c.id },
|
||||||
@@ -298,6 +433,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
"✅ profilePerbekel seeded without imageId (editable later via UI)"
|
"✅ profilePerbekel seeded without imageId (editable later via UI)"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// =========== VISI MISI DESA ===========
|
||||||
for (const l of visiMisiDesa) {
|
for (const l of visiMisiDesa) {
|
||||||
await prisma.visiMisiDesa.upsert({
|
await prisma.visiMisiDesa.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -317,63 +453,134 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
|
|
||||||
console.log("visi misi desa success ...");
|
console.log("visi misi desa success ...");
|
||||||
|
|
||||||
// Flatten the nested array structure for posisiOrganisasiPPID
|
// =========== MENU PPID ===========
|
||||||
const flattenedPosisiOrganisasiPPID = posisiOrganisasiPPID.flat();
|
// =========== SUBMENU PROFILE PPID ===========
|
||||||
|
for (const c of profilePPID) {
|
||||||
|
await prisma.profilePPID.upsert({
|
||||||
|
where: { id: c.id },
|
||||||
|
update: {
|
||||||
|
name: c.name,
|
||||||
|
biodata: c.biodata,
|
||||||
|
riwayat: c.riwayat,
|
||||||
|
pengalaman: c.pengalaman,
|
||||||
|
unggulan: c.unggulan,
|
||||||
|
// imageId tidak di-update
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
biodata: c.biodata,
|
||||||
|
riwayat: c.riwayat,
|
||||||
|
pengalaman: c.pengalaman,
|
||||||
|
unggulan: c.unggulan,
|
||||||
|
// imageId tidak di-create
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
|
||||||
|
|
||||||
|
// =========== SUBMENU STRUKTUR PPID ===========
|
||||||
|
// =========== POSISI ORGANISASI PPID ===========
|
||||||
|
|
||||||
|
const flattenedPosisi = posisiOrganisasiPPID.flat();
|
||||||
|
|
||||||
|
// ✅ Urutkan berdasarkan hierarki
|
||||||
|
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
||||||
|
|
||||||
|
for (const p of sortedPosisi) {
|
||||||
|
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||||
|
|
||||||
|
if (p.parentId) {
|
||||||
|
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||||
|
if (!parentExists) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of flattenedPosisiOrganisasiPPID) {
|
|
||||||
await prisma.posisiOrganisasiPPID.upsert({
|
await prisma.posisiOrganisasiPPID.upsert({
|
||||||
where: {
|
where: { id: p.id },
|
||||||
id: p.id,
|
update: p,
|
||||||
},
|
create: p,
|
||||||
update: {
|
|
||||||
nama: p.nama,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
hierarki: p.hierarki,
|
|
||||||
parentId: p.parentId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
nama: p.nama,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
hierarki: p.hierarki,
|
|
||||||
parentId: p.parentId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("posisi organisasi success ...");
|
console.log("posisi organisasi berhasil");
|
||||||
|
|
||||||
// Flatten the nested array structure for pegawaiPPID
|
// =========== PEGAWAI PPID ===========
|
||||||
const flattenedPegawaiPPID = pegawaiPPID.flat();
|
const flattenedPegawai = pegawaiPPID.flat();
|
||||||
|
for (const p of flattenedPegawai) {
|
||||||
for (const p of flattenedPegawaiPPID) {
|
|
||||||
await prisma.pegawaiPPID.upsert({
|
await prisma.pegawaiPPID.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: p,
|
||||||
|
create: p,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("pegawai berhasil");
|
||||||
|
|
||||||
|
// =========== SUBMENU VISI MISI PPID ===========
|
||||||
|
|
||||||
|
for (const v of visiMisiPPID) {
|
||||||
|
await prisma.visiMisiPPID.upsert({
|
||||||
where: {
|
where: {
|
||||||
id: p.id,
|
id: v.id,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
namaLengkap: p.namaLengkap,
|
misi: v.misi,
|
||||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
visi: v.visi,
|
||||||
email: p.email,
|
|
||||||
gelarAkademik: p.gelarAkademik,
|
|
||||||
telepon: p.telepon,
|
|
||||||
alamat: p.alamat,
|
|
||||||
posisiId: p.posisiId,
|
|
||||||
isActive: p.isActive,
|
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
id: p.id,
|
id: v.id,
|
||||||
namaLengkap: p.namaLengkap,
|
misi: v.misi,
|
||||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
visi: v.visi,
|
||||||
email: p.email,
|
|
||||||
gelarAkademik: p.gelarAkademik,
|
|
||||||
telepon: p.telepon,
|
|
||||||
alamat: p.alamat,
|
|
||||||
posisiId: p.posisiId,
|
|
||||||
isActive: p.isActive,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("pegawai success ...");
|
console.log("visi misi PPID success ...");
|
||||||
|
|
||||||
|
// =========== SUBMENU DASAR HUKUM PPID ===========
|
||||||
|
for (const v of dasarHukumPPID) {
|
||||||
|
await prisma.dasarHukumPPID.upsert({
|
||||||
|
where: {
|
||||||
|
id: v.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: v.judul,
|
||||||
|
content: v.content,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: v.id,
|
||||||
|
judul: v.judul,
|
||||||
|
content: v.content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("dasar hukum PPID success ...");
|
||||||
|
|
||||||
|
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
|
||||||
|
for (const v of daftarInformasiPublik) {
|
||||||
|
// Convert string date to Date object
|
||||||
|
const tanggal = new Date(v.tanggal);
|
||||||
|
|
||||||
|
await prisma.daftarInformasiPublik.upsert({
|
||||||
|
where: {
|
||||||
|
id: v.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
jenisInformasi: v.jenisInformasi,
|
||||||
|
deskripsi: v.deskripsi,
|
||||||
|
tanggal: tanggal,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: v.id,
|
||||||
|
jenisInformasi: v.jenisInformasi,
|
||||||
|
deskripsi: v.deskripsi,
|
||||||
|
tanggal: tanggal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("daftar informasi publik PPID success ...");
|
||||||
|
|
||||||
for (const l of pelayananPerizinanBerusaha) {
|
for (const l of pelayananPerizinanBerusaha) {
|
||||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
await prisma.pelayananPerizinanBerusaha.upsert({
|
||||||
@@ -507,48 +714,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
}
|
}
|
||||||
console.log("cara memperoleh salinan informasi success ...");
|
console.log("cara memperoleh salinan informasi success ...");
|
||||||
|
|
||||||
for (const c of profilePPID) {
|
|
||||||
await prisma.profilePPID.upsert({
|
|
||||||
where: { id: c.id },
|
|
||||||
update: {
|
|
||||||
name: c.name,
|
|
||||||
biodata: c.biodata,
|
|
||||||
riwayat: c.riwayat,
|
|
||||||
pengalaman: c.pengalaman,
|
|
||||||
unggulan: c.unggulan,
|
|
||||||
// imageId tidak di-update
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
biodata: c.biodata,
|
|
||||||
riwayat: c.riwayat,
|
|
||||||
pengalaman: c.pengalaman,
|
|
||||||
unggulan: c.unggulan,
|
|
||||||
// imageId tidak di-create
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
|
|
||||||
|
|
||||||
for (const v of visiMisiPPID) {
|
|
||||||
await prisma.visiMisiPPID.upsert({
|
|
||||||
where: {
|
|
||||||
id: v.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
misi: v.misi,
|
|
||||||
visi: v.visi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: v.id,
|
|
||||||
misi: v.misi,
|
|
||||||
visi: v.visi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("visi misi PPID success ...");
|
|
||||||
|
|
||||||
for (const j of jenisKelamin) {
|
for (const j of jenisKelamin) {
|
||||||
await prisma.jenisKelaminResponden.upsert({
|
await prisma.jenisKelaminResponden.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -597,24 +762,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
}
|
}
|
||||||
console.log("umur responden success ...");
|
console.log("umur responden success ...");
|
||||||
|
|
||||||
for (const v of dasarHukumPPID) {
|
|
||||||
await prisma.dasarHukumPPID.upsert({
|
|
||||||
where: {
|
|
||||||
id: v.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: v.judul,
|
|
||||||
content: v.content,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: v.id,
|
|
||||||
judul: v.judul,
|
|
||||||
content: v.content,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("dasar hukum PPID success ...");
|
|
||||||
|
|
||||||
for (const k of kategoriProduk) {
|
for (const k of kategoriProduk) {
|
||||||
await prisma.kategoriProduk.upsert({
|
await prisma.kategoriProduk.upsert({
|
||||||
where: {
|
where: {
|
||||||
@@ -702,9 +849,12 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
console.log("hubungan organisasi success ...");
|
console.log("hubungan organisasi success ...");
|
||||||
|
|
||||||
for (const d of detailDataPengangguran) {
|
for (const d of detailDataPengangguran) {
|
||||||
|
// Convert the year to a Date object (using January 1st of the year as the date)
|
||||||
|
const yearAsDate = new Date(d.year, 0, 1);
|
||||||
|
|
||||||
await prisma.detailDataPengangguran.upsert({
|
await prisma.detailDataPengangguran.upsert({
|
||||||
where: {
|
where: {
|
||||||
month_year: { month: d.month, year: d.year },
|
month_year: { month: d.month, year: yearAsDate },
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
totalUnemployment: d.totalUnemployment,
|
totalUnemployment: d.totalUnemployment,
|
||||||
@@ -714,7 +864,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
|
|||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
month: d.month,
|
month: d.month,
|
||||||
year: d.year,
|
year: yearAsDate,
|
||||||
totalUnemployment: d.totalUnemployment,
|
totalUnemployment: d.totalUnemployment,
|
||||||
educatedUnemployment: d.educatedUnemployment,
|
educatedUnemployment: d.educatedUnemployment,
|
||||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||||
|
|||||||
62
src/app/admin/(dashboard)/_com/LeafletMultiMarkerMap.tsx
Normal file
62
src/app/admin/(dashboard)/_com/LeafletMultiMarkerMap.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
||||||
|
import { LatLngExpression } from 'leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
type MarkerData = {
|
||||||
|
position: [number, number];
|
||||||
|
popup: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
center: [number, number];
|
||||||
|
markers: MarkerData[];
|
||||||
|
zoom?: number;
|
||||||
|
scrollWheelZoom?: boolean;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LeafletMultiMarkerMap({
|
||||||
|
center,
|
||||||
|
markers,
|
||||||
|
zoom = 13,
|
||||||
|
scrollWheelZoom = true,
|
||||||
|
className = '',
|
||||||
|
style = { height: '100%', width: '100%', zIndex: 0 },
|
||||||
|
}: Props) {
|
||||||
|
// Fix for default marker icons in Next.js
|
||||||
|
useEffect(() => {
|
||||||
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} style={style}>
|
||||||
|
<MapContainer
|
||||||
|
center={center as LatLngExpression}
|
||||||
|
zoom={zoom}
|
||||||
|
scrollWheelZoom={scrollWheelZoom}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
{markers.map((marker, index) => (
|
||||||
|
<Marker key={index} position={marker.position as LatLngExpression}>
|
||||||
|
<Popup>{marker.popup}</Popup>
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -74,18 +74,18 @@ const berita = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
berita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
berita.findMany.page = page;
|
berita.findMany.page = page;
|
||||||
berita.findMany.search = search;
|
berita.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
if (kategori) query.kategori = kategori;
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
|
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;
|
||||||
@@ -368,11 +368,37 @@ const kategoriBerita = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
kategoriBerita.findMany.data = res.data?.data ?? [];
|
kategoriBerita.findMany.page = page;
|
||||||
|
kategoriBerita.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.kategoriberita[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriBerita.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriBerita.findMany.totalPages =
|
||||||
|
res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriBerita.findMany.data = [];
|
||||||
|
kategoriBerita.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori berita paginated:", err);
|
||||||
|
kategoriBerita.findMany.data = [];
|
||||||
|
kategoriBerita.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriBerita.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const templateTelunjukSaktiDesaForm = z.object({
|
|||||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const templatePelayananPerizinanBerusaha = z.object({
|
const templatePelayananPerizinanBerusaha = 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"),
|
||||||
@@ -72,7 +71,6 @@ const pelayananPendudukNonPermanenForm = {
|
|||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const suratKeterangan = proxy({
|
const suratKeterangan = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: { ...suratKeteranganForm },
|
form: { ...suratKeteranganForm },
|
||||||
@@ -113,16 +111,21 @@ const suratKeterangan = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
suratKeterangan.findMany.loading = true; // Use the full path to access the property
|
||||||
suratKeterangan.findMany.page = page;
|
suratKeterangan.findMany.page = page;
|
||||||
|
suratKeterangan.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
suratKeterangan.findMany.data = res.data.data || [];
|
suratKeterangan.findMany.data = res.data.data || [];
|
||||||
suratKeterangan.findMany.total = res.data.total || 0;
|
suratKeterangan.findMany.total = res.data.total || 0;
|
||||||
@@ -341,28 +344,34 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
pelayananTelunjukSaktiDesa.findMany.page = page;
|
pelayananTelunjukSaktiDesa.findMany.page = page;
|
||||||
|
pelayananTelunjukSaktiDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
|
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
|
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = res.data.totalPages || 1;
|
pelayananTelunjukSaktiDesa.findMany.totalPages =
|
||||||
|
res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load telunjuk sakti desa:", res.data?.message);
|
console.error("Failed to load surat keterangan:", res.data?.message);
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = [];
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
suratKeterangan.findMany.total = 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
suratKeterangan.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading telunjuk sakti desa:", error);
|
console.error("Error loading surat keterangan:", error);
|
||||||
pelayananTelunjukSaktiDesa.findMany.data = [];
|
pelayananTelunjukSaktiDesa.findMany.data = [];
|
||||||
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
pelayananTelunjukSaktiDesa.findMany.total = 0;
|
||||||
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
|
||||||
@@ -410,7 +419,9 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success(result.message || "Telunjuk Sakti Desa berhasil dihapus");
|
toast.success(
|
||||||
|
result.message || "Telunjuk Sakti Desa berhasil dihapus"
|
||||||
|
);
|
||||||
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
|
toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
|
||||||
@@ -501,7 +512,9 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
}
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message || "Telunjuk Sakti Desa berhasil diupdate");
|
toast.success(
|
||||||
|
result.message || "Telunjuk Sakti Desa berhasil diupdate"
|
||||||
|
);
|
||||||
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -522,7 +535,7 @@ const pelayananTelunjukSaktiDesa = proxy({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const pelayananPerizinanBerusaha = proxy({
|
const pelayananPerizinanBerusaha = proxy({
|
||||||
findById: {
|
findById: {
|
||||||
@@ -596,9 +609,7 @@ const pelayananPerizinanBerusaha = proxy({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
console.error("Error fetching pelayanan perizinan berusaha:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
? error.message
|
|
||||||
: "Gagal memuat data"
|
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -713,9 +724,7 @@ const pelayananPendudukNonPermanen = proxy({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching pelayanan penduduk non permanen:", error);
|
console.error("Error fetching pelayanan penduduk non permanen:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
? error.message
|
|
||||||
: "Gagal memuat data"
|
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,16 +56,21 @@ const penghargaanState = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
penghargaanState.findMany.loading = true; // Use the full path to access the property
|
||||||
penghargaanState.findMany.page = page;
|
penghargaanState.findMany.page = page;
|
||||||
|
penghargaanState.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.desa.penghargaan[
|
const res = await ApiFetch.api.desa.penghargaan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
penghargaanState.findMany.data = res.data.data || [];
|
penghargaanState.findMany.data = res.data.data || [];
|
||||||
penghargaanState.findMany.total = res.data.total || 0;
|
penghargaanState.findMany.total = res.data.total || 0;
|
||||||
|
|||||||
@@ -55,11 +55,39 @@ const category = proxy({
|
|||||||
pengumumans: number;
|
pengumumans: number;
|
||||||
};
|
};
|
||||||
})[],
|
})[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
if (res.status === 200) {
|
category.findMany.loading = true; // Use the full path to access the property
|
||||||
category.findMany.data = res.data?.data ?? [];
|
category.findMany.page = page;
|
||||||
|
category.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.desa.kategoripengumuman[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query: { page, limit },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
category.findMany.data = res.data.data || [];
|
||||||
|
category.findMany.total = res.data.total || 0;
|
||||||
|
category.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load potensi desa:", res.data?.message);
|
||||||
|
category.findMany.data = [];
|
||||||
|
category.findMany.total = 0;
|
||||||
|
category.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading potensi desa:", error);
|
||||||
|
category.findMany.data = [];
|
||||||
|
category.findMany.total = 0;
|
||||||
|
category.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
category.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,9 +56,11 @@ const potensiDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
potensiDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
potensiDesa.findMany.page = page;
|
potensiDesa.findMany.page = page;
|
||||||
|
potensiDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.desa.potensi[
|
const res = await ApiFetch.api.desa.potensi[
|
||||||
"find-many"
|
"find-many"
|
||||||
@@ -298,11 +300,34 @@ const kategoriPotensi = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
kategoriPotensi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
kategoriPotensi.findMany.data = res.data?.data ?? [];
|
kategoriPotensi.findMany.page = page;
|
||||||
|
kategoriPotensi.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriPotensi.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriPotensi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriPotensi.findMany.data = [];
|
||||||
|
kategoriPotensi.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori potensi paginated:", err);
|
||||||
|
kategoriPotensi.findMany.data = [];
|
||||||
|
kategoriPotensi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriPotensi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -61,13 +62,39 @@ const lowonganKerjaState = proxy({
|
|||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.LowonganPekerjaanGetPayload<{
|
| Prisma.LowonganPekerjaanGetPayload<{
|
||||||
omit: { isActive: true };
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
lowonganKerjaState.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
lowonganKerjaState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
lowonganKerjaState.findMany.page = page;
|
||||||
|
lowonganKerjaState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
lowonganKerjaState.findMany.data = res.data.data ?? [];
|
||||||
|
lowonganKerjaState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
lowonganKerjaState.findMany.data = [];
|
||||||
|
lowonganKerjaState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch lowongan kerja paginated:", err);
|
||||||
|
lowonganKerjaState.findMany.data = [];
|
||||||
|
lowonganKerjaState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
lowonganKerjaState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -53,22 +54,47 @@ const pasarDesa = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Array<
|
data: null as
|
||||||
Prisma.PasarDesaGetPayload<{
|
| Prisma.PasarDesaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
image: true;
|
image: true;
|
||||||
KategoriToPasar: {
|
KategoriToPasar: {
|
||||||
include: {
|
include: {
|
||||||
kategori: true;
|
kategori: true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
}>[]
|
||||||
}>
|
| null,
|
||||||
> | null,
|
page: 1,
|
||||||
async load() {
|
totalPages: 1,
|
||||||
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
pasarDesa.findMany.data = res.data?.data ?? [];
|
load: async (page = 1, limit = 10, search = "", categoryId?: string) => {
|
||||||
|
pasarDesa.findMany.loading = true;
|
||||||
|
pasarDesa.findMany.page = page;
|
||||||
|
pasarDesa.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (categoryId) query.categoryId = categoryId;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pasarDesa.findMany.data = res.data.data ?? [];
|
||||||
|
pasarDesa.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
pasarDesa.findMany.data = [];
|
||||||
|
pasarDesa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch keamanan lingkungan paginated:", err);
|
||||||
|
pasarDesa.findMany.data = [];
|
||||||
|
pasarDesa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
pasarDesa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -272,14 +298,41 @@ const kategoriProduk = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Array<{
|
data: null as
|
||||||
id: string;
|
| Prisma.KategoriProdukGetPayload<{
|
||||||
nama: string;
|
omit: {
|
||||||
}> | null,
|
isActive: true;
|
||||||
async load() {
|
};
|
||||||
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
|
}>[]
|
||||||
if (res.status === 200) {
|
| null,
|
||||||
kategoriProduk.findMany.data = res.data?.data ?? [];
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search2: "",
|
||||||
|
load: async (page = 1, limit = 10, search2 = "") => {
|
||||||
|
kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
kategoriProduk.findMany.page = page;
|
||||||
|
kategoriProduk.findMany.search2 = search2;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search2) query.search2 = search2;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriProduk.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriProduk.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriProduk.findMany.data = [];
|
||||||
|
kategoriProduk.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori produk paginated:", err);
|
||||||
|
kategoriProduk.findMany.data = [];
|
||||||
|
kategoriProduk.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriProduk.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -11,8 +12,7 @@ const templateForm = z.object({
|
|||||||
statistik: z.object({
|
statistik: z.object({
|
||||||
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
|
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
|
||||||
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
|
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
|
||||||
})
|
}),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
@@ -21,8 +21,8 @@ const defaultForm = {
|
|||||||
ikonUrl: "",
|
ikonUrl: "",
|
||||||
statistik: {
|
statistik: {
|
||||||
tahun: "",
|
tahun: "",
|
||||||
jumlah: ""
|
jumlah: "",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const programKemiskinanState = proxy({
|
const programKemiskinanState = proxy({
|
||||||
@@ -64,12 +64,35 @@ const programKemiskinanState = proxy({
|
|||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.ekonomi.programkemiskinan[
|
totalPages: 1,
|
||||||
"find-many"
|
search: "",
|
||||||
].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
programKemiskinanState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
programKemiskinanState.findMany.data = res.data?.data ?? [];
|
programKemiskinanState.findMany.page = page;
|
||||||
|
programKemiskinanState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ekonomi.programkemiskinan[
|
||||||
|
"find-many"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
programKemiskinanState.findMany.data = res.data.data ?? [];
|
||||||
|
programKemiskinanState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
programKemiskinanState.findMany.data = [];
|
||||||
|
programKemiskinanState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch program kemiskinan paginated:", err);
|
||||||
|
programKemiskinanState.findMany.data = [];
|
||||||
|
programKemiskinanState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
programKemiskinanState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -55,10 +56,34 @@ const desaDigitalState = proxy({
|
|||||||
};
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
desaDigitalState.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
desaDigitalState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
desaDigitalState.findMany.page = page;
|
||||||
|
desaDigitalState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
desaDigitalState.findMany.data = res.data.data ?? [];
|
||||||
|
desaDigitalState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
desaDigitalState.findMany.data = [];
|
||||||
|
desaDigitalState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch desa digital paginated:", err);
|
||||||
|
desaDigitalState.findMany.data = [];
|
||||||
|
desaDigitalState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
desaDigitalState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -55,10 +56,34 @@ const infoTeknoState = proxy({
|
|||||||
};
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.inovasi.infotekno["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
infoTeknoState.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
infoTeknoState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
infoTeknoState.findMany.page = page;
|
||||||
|
infoTeknoState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.inovasi.infotekno["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
infoTeknoState.findMany.data = res.data.data ?? [];
|
||||||
|
infoTeknoState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
infoTeknoState.findMany.data = [];
|
||||||
|
infoTeknoState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch info teknologi paginated:", err);
|
||||||
|
infoTeknoState.findMany.data = [];
|
||||||
|
infoTeknoState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
infoTeknoState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ 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, "Nama minimal 1 karakter"),
|
name: z.string().min(1, "Nama kolaborasi inovasi harus diisi"),
|
||||||
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
|
tahun: z.number().min(1900, "Tahun tidak valid").max(new Date().getFullYear() + 1, "Tahun tidak boleh lebih dari tahun depan"),
|
||||||
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
|
slug: z.string().min(1, "Slug harus dihasilkan otomatis"),
|
||||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
|
||||||
kolaborator: z.string().min(1, "Kolaborator minimal 1 karakter"),
|
kolaborator: z.string().min(1, "Kolaborator harus diisi"),
|
||||||
imageId: z.string().min(1, "Image ID minimal 1 karakter"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
@@ -20,7 +19,6 @@ const defaultForm = {
|
|||||||
slug: "",
|
slug: "",
|
||||||
deskripsi: "",
|
deskripsi: "",
|
||||||
kolaborator: "",
|
kolaborator: "",
|
||||||
imageId: "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const kolaborasiInovasiState = proxy({
|
const kolaborasiInovasiState = proxy({
|
||||||
@@ -28,27 +26,37 @@ const kolaborasiInovasiState = proxy({
|
|||||||
form: { ...defaultForm },
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templateForm.safeParse(kolaborasiInovasiState.create.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate form
|
||||||
|
const validation = templateForm.safeParse(kolaborasiInovasiState.create.form);
|
||||||
|
if (!validation.success) {
|
||||||
|
const errorMessages = validation.error.issues
|
||||||
|
.map(issue => `- ${issue.path.join('.')}: ${issue.message}`)
|
||||||
|
.join('\n');
|
||||||
|
return toast.error(`Validasi gagal:\n${errorMessages}`);
|
||||||
|
}
|
||||||
|
|
||||||
kolaborasiInovasiState.create.loading = true;
|
kolaborasiInovasiState.create.loading = true;
|
||||||
|
|
||||||
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
|
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
|
||||||
kolaborasiInovasiState.create.form
|
kolaborasiInovasiState.create.form
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
kolaborasiInovasiState.findMany.load();
|
await kolaborasiInovasiState.findMany.load();
|
||||||
return toast.success("success create");
|
return { success: true, data: res.data };
|
||||||
}
|
}
|
||||||
console.log(res);
|
|
||||||
return toast.error("failed create");
|
console.error('Create failed:', res);
|
||||||
|
toast.error(res.data?.message || "Gagal menyimpan data");
|
||||||
|
return { success: false, error: res.data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log((error as Error).message);
|
console.error('Error in create:', error);
|
||||||
|
toast.error("Terjadi kesalahan saat menyimpan data");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
} finally {
|
} finally {
|
||||||
kolaborasiInovasiState.create.loading = false;
|
kolaborasiInovasiState.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -60,13 +68,21 @@ const kolaborasiInovasiState = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => {
|
search: "",
|
||||||
// Change to arrow function
|
year: "",
|
||||||
kolaborasiInovasiState.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "", year?: string) => {
|
||||||
|
kolaborasiInovasiState.findMany.loading = true;
|
||||||
kolaborasiInovasiState.findMany.page = page;
|
kolaborasiInovasiState.findMany.page = page;
|
||||||
|
kolaborasiInovasiState.findMany.search = search;
|
||||||
|
kolaborasiInovasiState.findMany.year = year || "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (year) query.year = year;
|
||||||
|
|
||||||
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
|
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
@@ -124,7 +140,6 @@ const kolaborasiInovasiState = proxy({
|
|||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi,
|
||||||
kolaborator: data.kolaborator,
|
kolaborator: data.kolaborator,
|
||||||
imageId: data.imageId,
|
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
@@ -179,7 +194,7 @@ const kolaborasiInovasiState = proxy({
|
|||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.KolaborasiInovasiGetPayload<{
|
data: null as Prisma.KolaborasiInovasiGetPayload<{
|
||||||
include: { image: true };
|
omit: { isActive: true };
|
||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
229
src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts
Normal file
229
src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const mitraKolaborasiForm = z.object({
|
||||||
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
|
imageId: z.string().nonempty(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
name: "",
|
||||||
|
imageId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mitraKolaborasi = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = mitraKolaborasiForm.safeParse(mitraKolaborasi.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mitraKolaborasi.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.inovasi.mitrakolaborasi["create"].post(
|
||||||
|
mitraKolaborasi.create.form
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
mitraKolaborasi.findMany.load();
|
||||||
|
return toast.success("mitraKolaborasi berhasil disimpan!");
|
||||||
|
}
|
||||||
|
return toast.error("Gagal menyimpan mitraKolaborasi");
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
mitraKolaborasi.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
mitraKolaborasi.create.form = { ...defaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.MitraKolaborasiGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
mitraKolaborasi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
mitraKolaborasi.findMany.page = page;
|
||||||
|
mitraKolaborasi.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.inovasi.mitrakolaborasi["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
mitraKolaborasi.findMany.data = res.data.data ?? [];
|
||||||
|
mitraKolaborasi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
mitraKolaborasi.findMany.data = [];
|
||||||
|
mitraKolaborasi.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch mitraKolaborasi paginated:", err);
|
||||||
|
mitraKolaborasi.findMany.data = [];
|
||||||
|
mitraKolaborasi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
mitraKolaborasi.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.MitraKolaborasiGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/inovasi/mitrakolaborasi/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
mitraKolaborasi.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch mitraKolaborasi:", res.statusText);
|
||||||
|
mitraKolaborasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching mitraKolaborasi:", error);
|
||||||
|
mitraKolaborasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
try {
|
||||||
|
mitraKolaborasi.delete.loading = true;
|
||||||
|
const response = await fetch(`/api/inovasi/mitrakolaborasi/del/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(result.message || "mitraKolaborasi berhasil dihapus");
|
||||||
|
await mitraKolaborasi.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result.message || "Gagal menghapus mitraKolaborasi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus mitraKolaborasi");
|
||||||
|
} finally {
|
||||||
|
mitraKolaborasi.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/inovasi/mitrakolaborasi/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
name: data.name,
|
||||||
|
imageId: data.imageId,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading mitraKolaborasi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = mitraKolaborasiForm.safeParse(mitraKolaborasi.update.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mitraKolaborasi.update.loading = true;
|
||||||
|
const response = await fetch(`/api/inovasi/mitrakolaborasi/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: this.form.name,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message || "mitraKolaborasi berhasil diupdate");
|
||||||
|
await mitraKolaborasi.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal mengupdate mitraKolaborasi");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating mitraKolaborasi:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal mengupdate mitraKolaborasi"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
mitraKolaborasi.update.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
mitraKolaborasi.update.id = "";
|
||||||
|
mitraKolaborasi.update.form = { ...defaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default mitraKolaborasi;
|
||||||
@@ -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";
|
||||||
@@ -53,15 +54,39 @@ const keamananLingkunganState = proxy({
|
|||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.KeamananLingkunganGetPayload<{
|
| Prisma.KeamananLingkunganGetPayload<{
|
||||||
include: { image: true };
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.keamanan.keamananlingkungan[
|
totalPages: 1,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
keamananLingkunganState.findMany.data = res.data?.data ?? [];
|
keamananLingkunganState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
keamananLingkunganState.findMany.page = page;
|
||||||
|
keamananLingkunganState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.keamanan.keamananlingkungan["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
keamananLingkunganState.findMany.data = res.data.data ?? [];
|
||||||
|
keamananLingkunganState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
keamananLingkunganState.findMany.data = [];
|
||||||
|
keamananLingkunganState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch keamanan lingkungan paginated:", err);
|
||||||
|
keamananLingkunganState.findMany.data = [];
|
||||||
|
keamananLingkunganState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
keamananLingkunganState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -63,13 +64,41 @@ const polsekTerdekatState = proxy({
|
|||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.PolsekTerdekatGetPayload<{
|
| Prisma.PolsekTerdekatGetPayload<{
|
||||||
include: { layananPolsek: true };
|
include: {
|
||||||
|
layananPolsek: true;
|
||||||
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
polsekTerdekatState.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
polsekTerdekatState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
polsekTerdekatState.findMany.page = page;
|
||||||
|
polsekTerdekatState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get(
|
||||||
|
{ query }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
polsekTerdekatState.findMany.data = res.data.data ?? [];
|
||||||
|
polsekTerdekatState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
polsekTerdekatState.findMany.data = [];
|
||||||
|
polsekTerdekatState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch polsek terdekat paginated:", err);
|
||||||
|
polsekTerdekatState.findMany.data = [];
|
||||||
|
polsekTerdekatState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
polsekTerdekatState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -237,6 +266,29 @@ const polsekTerdekatState = proxy({
|
|||||||
polsekTerdekatState.edit.form = { ...defaultForm };
|
polsekTerdekatState.edit.form = { ...defaultForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findFirst: {
|
||||||
|
data: null as Prisma.PolsekTerdekatGetPayload<{
|
||||||
|
include: {
|
||||||
|
layananPolsek: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
load: async () => { // Changed to arrow function
|
||||||
|
polsekTerdekatState.findFirst.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.keamanan.polsekterdekat["find-first"].get();
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
polsekTerdekatState.findFirst.data = res.data.data || null;
|
||||||
|
} else {
|
||||||
|
polsekTerdekatState.findFirst.data = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch polsek terdekat terbaru:", err);
|
||||||
|
} finally {
|
||||||
|
polsekTerdekatState.findFirst.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default polsekTerdekatState;
|
export default polsekTerdekatState;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -53,15 +54,39 @@ const tipsKeamananState = proxy({
|
|||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.MenuTipsKeamananGetPayload<{
|
| Prisma.MenuTipsKeamananGetPayload<{
|
||||||
include: { image: true };
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.keamanan.menutipskeamanan[
|
totalPages: 1,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
tipsKeamananState.findMany.data = res.data?.data ?? [];
|
tipsKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
tipsKeamananState.findMany.page = page;
|
||||||
|
tipsKeamananState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.keamanan.menutipskeamanan["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
tipsKeamananState.findMany.data = res.data.data ?? [];
|
||||||
|
tipsKeamananState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
tipsKeamananState.findMany.data = [];
|
||||||
|
tipsKeamananState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch menu tips keamanan paginated:", err);
|
||||||
|
tipsKeamananState.findMany.data = [];
|
||||||
|
tipsKeamananState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
tipsKeamananState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { toast } from "react-toastify";
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
//fasilitas kesehatan aja
|
||||||
// Validasi form
|
// Validasi form
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(1, "Nama harus diisi"),
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
@@ -61,7 +62,7 @@ const defaultForm = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const fasilitasKesehatanState = proxy({
|
const fasilitasKesehatan = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: { ...defaultForm },
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -86,7 +87,7 @@ const fasilitasKesehatanState = proxy({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Berhasil menambahkan fasilitas kesehatan");
|
toast.success("Berhasil menambahkan fasilitas kesehatan");
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
await fasilitasKesehatanState.findMany.load();
|
await fasilitasKesehatan.findMany.load();
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -102,7 +103,6 @@ const fasilitasKesehatanState = proxy({
|
|||||||
this.form = { ...defaultForm };
|
this.form = { ...defaultForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.FasilitasKesehatanGetPayload<{
|
| Prisma.FasilitasKesehatanGetPayload<{
|
||||||
@@ -156,7 +156,7 @@ const fasilitasKesehatanState = proxy({
|
|||||||
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
|
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
fasilitasKesehatanState.findUnique.data = data.data ?? null;
|
fasilitasKesehatan.findUnique.data = data.data ?? null;
|
||||||
} else {
|
} else {
|
||||||
toast.error("Gagal load data fasilitas kesehatan");
|
toast.error("Gagal load data fasilitas kesehatan");
|
||||||
}
|
}
|
||||||
@@ -176,8 +176,8 @@ const fasilitasKesehatanState = proxy({
|
|||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
|
|
||||||
fasilitasKesehatanState.edit.id = data.id;
|
fasilitasKesehatan.edit.id = data.id;
|
||||||
fasilitasKesehatanState.edit.form = {
|
fasilitasKesehatan.edit.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
informasiUmum: {
|
informasiUmum: {
|
||||||
fasilitas: data.informasiumum.fasilitas,
|
fasilitas: data.informasiumum.fasilitas,
|
||||||
@@ -205,7 +205,7 @@ const fasilitasKesehatanState = proxy({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
const cek = templateForm.safeParse(fasilitasKesehatanState.edit.form);
|
const cek = templateForm.safeParse(fasilitasKesehatan.edit.form);
|
||||||
if (!cek.success) {
|
if (!cek.success) {
|
||||||
const errMsg = cek.error.issues
|
const errMsg = cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}: ${v.message}`)
|
.map((v) => `${v.path.join(".")}: ${v.message}`)
|
||||||
@@ -215,42 +215,38 @@ const fasilitasKesehatanState = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fasilitasKesehatanState.edit.loading = true;
|
fasilitasKesehatan.edit.loading = true;
|
||||||
const payload = {
|
const payload = {
|
||||||
name: fasilitasKesehatanState.edit.form.name,
|
name: fasilitasKesehatan.edit.form.name,
|
||||||
informasiUmum: {
|
informasiUmum: {
|
||||||
fasilitas:
|
fasilitas: fasilitasKesehatan.edit.form.informasiUmum.fasilitas,
|
||||||
fasilitasKesehatanState.edit.form.informasiUmum.fasilitas,
|
alamat: fasilitasKesehatan.edit.form.informasiUmum.alamat,
|
||||||
alamat: fasilitasKesehatanState.edit.form.informasiUmum.alamat,
|
|
||||||
jamOperasional:
|
jamOperasional:
|
||||||
fasilitasKesehatanState.edit.form.informasiUmum.jamOperasional,
|
fasilitasKesehatan.edit.form.informasiUmum.jamOperasional,
|
||||||
},
|
},
|
||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: fasilitasKesehatanState.edit.form.layananUnggulan.content,
|
content: fasilitasKesehatan.edit.form.layananUnggulan.content,
|
||||||
},
|
},
|
||||||
dokterdanTenagaMedis: {
|
dokterdanTenagaMedis: {
|
||||||
name: fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.name,
|
name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
|
||||||
specialist:
|
specialist:
|
||||||
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.specialist,
|
fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
|
||||||
jadwal:
|
jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
|
||||||
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.jadwal,
|
|
||||||
},
|
},
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content:
|
content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
|
||||||
fasilitasKesehatanState.edit.form.fasilitasPendukung.content,
|
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content:
|
content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
|
||||||
fasilitasKesehatanState.edit.form.prosedurPendaftaran.content,
|
|
||||||
},
|
},
|
||||||
tarifDanLayanan: {
|
tarifDanLayanan: {
|
||||||
layanan: fasilitasKesehatanState.edit.form.tarifDanLayanan.layanan,
|
layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
|
||||||
tarif: fasilitasKesehatanState.edit.form.tarifDanLayanan.tarif,
|
tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatanState.edit.id}`,
|
`/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatan.edit.id}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -264,7 +260,7 @@ const fasilitasKesehatanState = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Berhasil update fasilitas kesehatan");
|
toast.success("Berhasil update fasilitas kesehatan");
|
||||||
await fasilitasKesehatanState.findMany.load();
|
await fasilitasKesehatan.findMany.load();
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -272,37 +268,297 @@ const fasilitasKesehatanState = proxy({
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
fasilitasKesehatanState.edit.loading = false;
|
fasilitasKesehatan.edit.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
fasilitasKesehatanState.edit.id = "";
|
fasilitasKesehatan.edit.id = "";
|
||||||
fasilitasKesehatanState.edit.form = { ...defaultForm };
|
fasilitasKesehatan.edit.form = { ...defaultForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId(id: string){
|
async byId(id: string) {
|
||||||
try {
|
try {
|
||||||
fasilitasKesehatanState.delete.loading = true;
|
fasilitasKesehatan.delete.loading = true;
|
||||||
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/del/${id}`, {
|
const res = await fetch(
|
||||||
method: "DELETE",
|
`/api/kesehatan/fasilitas-kesehatan/del/${id}`,
|
||||||
});
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
if (res.ok && result.success) {
|
if (res.ok && result.success) {
|
||||||
toast.success("Fasilitas kesehatan berhasil dihapus");
|
toast.success("Fasilitas kesehatan berhasil dihapus");
|
||||||
await fasilitasKesehatanState.findMany.load();
|
await fasilitasKesehatan.findMany.load();
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.message || "Gagal menghapus");
|
toast.error(result.message || "Gagal menghapus");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Terjadi kesalahan saat menghapus");
|
toast.error("Terjadi kesalahan saat menghapus");
|
||||||
} finally {
|
} finally {
|
||||||
fasilitasKesehatanState.delete.loading = false;
|
fasilitasKesehatan.delete.loading = false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//dokter & tenaga medis
|
||||||
|
const templateDokterForm = z.object({
|
||||||
|
name: z.string().min(1, "Nama tidak boleh kosong"),
|
||||||
|
specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
|
||||||
|
jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultDokterForm = {
|
||||||
|
name: "",
|
||||||
|
specialist: "",
|
||||||
|
jadwal: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const dokter = proxy({
|
||||||
|
create: {
|
||||||
|
create: {
|
||||||
|
form: defaultDokterForm,
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateDokterForm.safeParse(dokter.create.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dokter.create.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.kesehatan.doktertenagamedis[
|
||||||
|
"create"
|
||||||
|
].post(dokter.create.create.form);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const id = res.data?.data;
|
||||||
|
if (id) {
|
||||||
|
toast.success("Success create");
|
||||||
|
dokter.create.create.form = { ...defaultDokterForm };
|
||||||
|
dokter.findMany.load();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast.error("failed create");
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
dokter.create.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.DokterdanTenagaMedisGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
dokter.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
dokter.findMany.page = page;
|
||||||
|
dokter.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.doktertenagamedis[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
dokter.findMany.data = res.data.data ?? [];
|
||||||
|
dokter.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
dokter.findMany.data = [];
|
||||||
|
dokter.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch dokter tenaga medis paginated:", err);
|
||||||
|
dokter.findMany.data = [];
|
||||||
|
dokter.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
dokter.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.DokterdanTenagaMedisGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
dokter.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch dokter dan tenaga medis",
|
||||||
|
res.statusText
|
||||||
|
);
|
||||||
|
dokter.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching dokter dan tenaga medis", error);
|
||||||
|
dokter.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultDokterForm },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
name: data.name,
|
||||||
|
specialist: data.specialist,
|
||||||
|
jadwal: data.jadwal,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading dokter dan tenaga medis:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit() {
|
||||||
|
const id = this.id;
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
name: this.form.name,
|
||||||
|
specialist: this.form.specialist,
|
||||||
|
jadwal: this.form.jadwal,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cek = templateDokterForm.safeParse(formData);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || !result?.success) {
|
||||||
|
throw new Error(result?.message || "Gagal update data");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Berhasil update data!");
|
||||||
|
await dokter.findMany.load();
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
toast.error("Gagal update data dokter dan tenaga medis");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
return toast.warn("ID tidak valid");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dokter.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/kesehatan/doktertenagamedis/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(
|
||||||
|
result.message || "Dokter dan tenaga medis berhasil dihapus"
|
||||||
|
);
|
||||||
|
await dokter.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
result?.message || "Gagal menghapus dokter dan tenaga medis"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus dokter dan tenaga medis");
|
||||||
|
} finally {
|
||||||
|
dokter.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fasilitasKesehatanState = proxy({
|
||||||
|
fasilitasKesehatan,
|
||||||
|
dokter
|
||||||
|
});
|
||||||
|
|
||||||
export default fasilitasKesehatanState;
|
export default fasilitasKesehatanState;
|
||||||
|
|||||||
@@ -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,20 +6,19 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateGrafikKepuasan = z.object({
|
const templateGrafikKepuasan = z.object({
|
||||||
label: z.string().min(2, "Label harus diisi"),
|
nama: z.string().min(2, "Nama harus diisi"),
|
||||||
jumlah: z.string().min(1, "Jumlah harus diisi"),
|
tanggal: z.string().min(1, "Tanggal harus diisi"),
|
||||||
|
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
|
||||||
|
alamat: z.string().min(1, "Alamat harus diisi"),
|
||||||
|
penyakit: z.string().min(1, "Penyakit harus diisi"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GrafikKepuasan = Prisma.GrafikKepuasanGetPayload<{
|
const defaultForm = {
|
||||||
select: {
|
nama: "",
|
||||||
label: true;
|
tanggal: "",
|
||||||
jumlah: true;
|
jenisKelamin: "",
|
||||||
};
|
alamat: "",
|
||||||
}>;
|
penyakit: "",
|
||||||
|
|
||||||
const defaultForm: GrafikKepuasan = {
|
|
||||||
label: "",
|
|
||||||
jumlah: ""
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const grafikkepuasan = proxy({
|
const grafikkepuasan = proxy({
|
||||||
@@ -36,16 +36,15 @@ const grafikkepuasan = proxy({
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
grafikkepuasan.create.loading = true;
|
grafikkepuasan.create.loading = true;
|
||||||
const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(grafikkepuasan.create.form);
|
const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(
|
||||||
|
grafikkepuasan.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");
|
||||||
grafikkepuasan.create.form = {
|
grafikkepuasan.create.form = { ...defaultForm };
|
||||||
label: "",
|
|
||||||
jumlah: "",
|
|
||||||
};
|
|
||||||
grafikkepuasan.findMany.load();
|
grafikkepuasan.findMany.load();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -62,21 +61,49 @@ const grafikkepuasan = proxy({
|
|||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.GrafikKepuasanGetPayload<{ omit: { isActive: true } }>[]
|
| Prisma.GrafikKepuasanGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.kesehatan.grafikkepuasan[
|
totalPages: 1,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
grafikkepuasan.findMany.data = res.data?.data ?? [];
|
grafikkepuasan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
grafikkepuasan.findMany.page = page;
|
||||||
|
grafikkepuasan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.grafikkepuasan[
|
||||||
|
"find-many"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
grafikkepuasan.findMany.data = res.data.data ?? [];
|
||||||
|
grafikkepuasan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
grafikkepuasan.findMany.data = [];
|
||||||
|
grafikkepuasan.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch berita paginated:", err);
|
||||||
|
grafikkepuasan.findMany.data = [];
|
||||||
|
grafikkepuasan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
grafikkepuasan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.GrafikKepuasanGetPayload<{
|
data: null as Prisma.GrafikKepuasanGetPayload<{
|
||||||
omit: { isActive: true }
|
omit: { isActive: true };
|
||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`);
|
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`);
|
||||||
@@ -95,88 +122,137 @@ const grafikkepuasan = proxy({
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
id: "",
|
id: "",
|
||||||
form: {...defaultForm},
|
form: { ...defaultForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId() {
|
async load(id: string) {
|
||||||
},
|
if (!id) {
|
||||||
async submit() {
|
toast.warn("ID tidak valid");
|
||||||
const id = this.id;
|
return null;
|
||||||
if (!id) {
|
|
||||||
toast.warn("ID tidak valid");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const cek = templateGrafikKepuasan.safeParse(grafikkepuasan.update.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/kesehatan/grafikkepuasan/${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!");
|
try {
|
||||||
|
const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ✅ Optional: refresh list kalau kamu langsung ke halaman list
|
if (!response.ok) {
|
||||||
await grafikkepuasan.findMany.load();
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
return result.data;
|
const result = await response.json();
|
||||||
} catch (error) {
|
|
||||||
console.error("Error update data:", error);
|
|
||||||
toast.error("Gagal update data grafik kepuasan");
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
loading: false,
|
|
||||||
async byId(id: string) {
|
|
||||||
if (!id) {
|
|
||||||
return toast.warn("ID tidak valid");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
grafikkepuasan.delete.loading = true;
|
|
||||||
|
|
||||||
const response = await fetch(`/api/kesehatan/grafikkepuasan/del/${id}`, {
|
if (result?.success) {
|
||||||
method: "DELETE",
|
const data = result.data;
|
||||||
headers: {
|
this.id = data.id;
|
||||||
"Content-Type": "application/json",
|
this.form = {
|
||||||
},
|
nama: data.nama,
|
||||||
});
|
tanggal: data.tanggal,
|
||||||
|
jenisKelamin: data.jenisKelamin,
|
||||||
const result = await response.json();
|
alamat: data.alamat,
|
||||||
|
penyakit: data.penyakit,
|
||||||
if (response.ok && result?.success) {
|
};
|
||||||
toast.success(
|
return data; // Return the loaded data
|
||||||
result.message || "Grafik kepuasan berhasil dihapus"
|
} else {
|
||||||
);
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
await grafikkepuasan.findMany.load(); // refresh list
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
|
console.error("Error loading grafik kepuasan:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
result?.message || "Gagal menghapus grafik kepuasan"
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
);
|
);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error("Gagal delete:", error);
|
async submit() {
|
||||||
toast.error("Terjadi kesalahan saat menghapus grafik kepuasan");
|
const id = this.id;
|
||||||
} finally {
|
if (!id) {
|
||||||
grafikkepuasan.delete.loading = false;
|
toast.warn("ID tidak valid");
|
||||||
}
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const formData = {
|
||||||
|
nama: this.form.nama,
|
||||||
|
tanggal: this.form.tanggal,
|
||||||
|
jenisKelamin: this.form.jenisKelamin,
|
||||||
|
alamat: this.form.alamat,
|
||||||
|
penyakit: this.form.penyakit,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cek = templateGrafikKepuasan.safeParse(formData);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || !result?.success) {
|
||||||
|
throw new Error(result?.message || "Gagal update data");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Berhasil update data!");
|
||||||
|
await grafikkepuasan.findMany.load();
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
toast.error("Gagal update data grafik kepuasan");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
return toast.warn("ID tidak valid");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
grafikkepuasan.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/kesehatan/grafikkepuasan/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "Grafik kepuasan berhasil dihapus");
|
||||||
|
await grafikkepuasan.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus grafik kepuasan");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus grafik kepuasan");
|
||||||
|
} finally {
|
||||||
|
grafikkepuasan.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default grafikkepuasan;
|
export default grafikkepuasan;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -20,17 +21,41 @@ const defaultForm = {
|
|||||||
|
|
||||||
const infoWabahPenyakit = proxy({
|
const infoWabahPenyakit = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.InfoWabahPenyakitGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.InfoWabahPenyakitGetPayload<{
|
||||||
image: true;
|
include: {
|
||||||
};
|
image: true;
|
||||||
}>[],
|
};
|
||||||
async load() {
|
}>[]
|
||||||
const res = await ApiFetch.api.kesehatan.infowabahpenyakit[
|
| null,
|
||||||
"find-many"
|
page: 1,
|
||||||
].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
infoWabahPenyakit.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
infoWabahPenyakit.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
infoWabahPenyakit.findMany.page = page;
|
||||||
|
infoWabahPenyakit.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.infowabahpenyakit["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
infoWabahPenyakit.findMany.data = res.data.data ?? [];
|
||||||
|
infoWabahPenyakit.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
infoWabahPenyakit.findMany.data = [];
|
||||||
|
infoWabahPenyakit.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch info wabah penyakit paginated:", err);
|
||||||
|
infoWabahPenyakit.findMany.data = [];
|
||||||
|
infoWabahPenyakit.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
infoWabahPenyakit.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,204 +6,241 @@ 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, "Judul minimal 3 karakter"),
|
name: 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"),
|
||||||
imageId: z.string().nonempty(),
|
imageId: z.string().nonempty(),
|
||||||
})
|
|
||||||
|
|
||||||
const defaultForm = {
|
|
||||||
name: "",
|
|
||||||
deskripsi: "",
|
|
||||||
imageId: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
const kontakDarurat = proxy({
|
|
||||||
findMany: {
|
|
||||||
data: [] as Prisma.KontakDaruratGetPayload<{
|
|
||||||
include: {
|
|
||||||
image: true;
|
|
||||||
};
|
|
||||||
}>[],
|
|
||||||
async load() {
|
|
||||||
const res = await ApiFetch.api.kesehatan.kontakdarurat[
|
|
||||||
"find-many"
|
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
kontakDarurat.findMany.data = res.data?.data ?? [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create:{
|
|
||||||
form: {...defaultForm},
|
|
||||||
loading: false,
|
|
||||||
async create() {
|
|
||||||
const cek = templateForm.safeParse(kontakDarurat.create.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
kontakDarurat.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.kesehatan.kontakdarurat[
|
|
||||||
"create"
|
|
||||||
].post(kontakDarurat.create.form);
|
|
||||||
if (res.status === 200) {
|
|
||||||
kontakDarurat.findMany.load();
|
|
||||||
return toast.success("Kontak Darurat berhasil disimpan!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return toast.error("Gagal menyimpan kontak darurat");
|
|
||||||
} catch (error) {
|
|
||||||
console.log((error as Error).message);
|
|
||||||
} finally {
|
|
||||||
kontakDarurat.create.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetForm() {
|
|
||||||
kontakDarurat.create.form = {...defaultForm};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findUnique: {
|
|
||||||
data: null as Prisma.KontakDaruratGetPayload<{
|
|
||||||
include: {
|
|
||||||
image: true;
|
|
||||||
};
|
|
||||||
}> | null,
|
|
||||||
async load(id: string) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/kesehatan/kontakdarurat/${id}`);
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
kontakDarurat.findUnique.data = data.data ?? null;
|
|
||||||
} else {
|
|
||||||
console.error("Failed to fetch data", res.status, res.statusText);
|
|
||||||
kontakDarurat.findUnique.data = null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
kontakDarurat.findUnique.data = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
loading: false,
|
|
||||||
async byId(id: string) {
|
|
||||||
try {
|
|
||||||
kontakDarurat.delete.loading = true;
|
|
||||||
const response = await fetch(`/api/kesehatan/kontakdarurat/del/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "Kontak darurat berhasil dihapus");
|
|
||||||
await kontakDarurat.findMany.load(); // refresh list
|
|
||||||
} else {
|
|
||||||
toast.error(result?.message || "Gagal menghapus kontak darurat");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Gagal delete:", error);
|
|
||||||
toast.error("Terjadi kesalahan saat menghapus kontak darurat");
|
|
||||||
} finally {
|
|
||||||
kontakDarurat.delete.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
edit: {
|
|
||||||
id: "",
|
|
||||||
form: { ...defaultForm },
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
async load(id: string) {
|
|
||||||
if (!id) {
|
|
||||||
toast.warn("ID tidak valid");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/kesehatan/kontakdarurat/${id}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
|
||||||
this.form = {
|
|
||||||
name: data.name,
|
|
||||||
deskripsi: data.deskripsi,
|
|
||||||
imageId: data.imageId,
|
|
||||||
};
|
|
||||||
return data; // Return the loaded data
|
|
||||||
} else {
|
|
||||||
throw new Error(result?.message || "Gagal memuat data");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching kontak darurat:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async update() {
|
|
||||||
const cek = templateForm.safeParse(kontakDarurat.edit.form);
|
|
||||||
if (!cek.success) {
|
|
||||||
const err = `[${cek.error.issues
|
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
kontakDarurat.edit.loading = true;
|
|
||||||
const response = await fetch(`/api/kesehatan/kontakdarurat/${this.id}`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
|
||||||
deskripsi: this.form.deskripsi,
|
|
||||||
imageId: this.form.imageId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
toast.success(result.message || "Kontak darurat berhasil diupdate");
|
|
||||||
await kontakDarurat.findMany.load();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.message || "Gagal update kontak darurat");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Gagal update:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate kontak darurat");
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
kontakDarurat.edit.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
kontakDarurat.edit.id = "";
|
|
||||||
kontakDarurat.edit.form = { ...defaultForm };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default kontakDarurat
|
const defaultForm = {
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imageId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const kontakDarurat = proxy({
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.KontakDaruratGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
kontakDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
kontakDarurat.findMany.page = page;
|
||||||
|
kontakDarurat.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.kontakdarurat[
|
||||||
|
"find-many"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kontakDarurat.findMany.data = res.data.data ?? [];
|
||||||
|
kontakDarurat.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kontakDarurat.findMany.data = [];
|
||||||
|
kontakDarurat.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kontak darurat paginated:", err);
|
||||||
|
kontakDarurat.findMany.data = [];
|
||||||
|
kontakDarurat.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kontakDarurat.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateForm.safeParse(kontakDarurat.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
kontakDarurat.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.kesehatan.kontakdarurat["create"].post(
|
||||||
|
kontakDarurat.create.form
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
kontakDarurat.findMany.load();
|
||||||
|
return toast.success("Kontak Darurat berhasil disimpan!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast.error("Gagal menyimpan kontak darurat");
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
kontakDarurat.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
kontakDarurat.create.form = { ...defaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.KontakDaruratGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/kesehatan/kontakdarurat/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
kontakDarurat.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
kontakDarurat.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
kontakDarurat.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
try {
|
||||||
|
kontakDarurat.delete.loading = true;
|
||||||
|
const response = await fetch(`/api/kesehatan/kontakdarurat/del/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "Kontak darurat berhasil dihapus");
|
||||||
|
await kontakDarurat.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus kontak darurat");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus kontak darurat");
|
||||||
|
} finally {
|
||||||
|
kontakDarurat.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultForm },
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/kesehatan/kontakdarurat/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
name: data.name,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
imageId: data.imageId,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching kontak darurat:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
const cek = templateForm.safeParse(kontakDarurat.edit.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
kontakDarurat.edit.loading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/kesehatan/kontakdarurat/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: this.form.name,
|
||||||
|
deskripsi: this.form.deskripsi,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message || "Kontak darurat berhasil diupdate");
|
||||||
|
await kontakDarurat.findMany.load();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update kontak darurat");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal update:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat mengupdate kontak darurat"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
kontakDarurat.edit.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
kontakDarurat.edit.id = "";
|
||||||
|
kontakDarurat.edit.form = { ...defaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default kontakDarurat;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -17,21 +18,45 @@ const defaultForm = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const penangananDarurat = proxy({
|
const penangananDarurat = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.PenangananDaruratGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.PenangananDaruratGetPayload<{
|
||||||
image: true;
|
include: {
|
||||||
};
|
image: true;
|
||||||
}>[],
|
};
|
||||||
async load() {
|
}>[]
|
||||||
const res = await ApiFetch.api.kesehatan.penanganandarurat[
|
| null,
|
||||||
"find-many"
|
page: 1,
|
||||||
].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
penangananDarurat.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
}
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
},
|
penangananDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
penangananDarurat.findMany.page = page;
|
||||||
|
penangananDarurat.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.penanganandarurat["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
penangananDarurat.findMany.data = res.data.data ?? [];
|
||||||
|
penangananDarurat.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
penangananDarurat.findMany.data = [];
|
||||||
|
penangananDarurat.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch berita paginated:", err);
|
||||||
|
penangananDarurat.findMany.data = [];
|
||||||
|
penangananDarurat.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
penangananDarurat.findMany.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
create:{
|
create:{
|
||||||
form: {...defaultForm},
|
form: {...defaultForm},
|
||||||
loading: false,
|
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";
|
||||||
@@ -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";
|
||||||
@@ -20,17 +21,43 @@ const defaultForm = {
|
|||||||
|
|
||||||
const programKesehatan = proxy({
|
const programKesehatan = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.ProgramKesehatanGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.ProgramKesehatanGetPayload<{
|
||||||
image: true;
|
include: {
|
||||||
};
|
image: true;
|
||||||
}>[],
|
};
|
||||||
async load() {
|
}>[]
|
||||||
const res = await ApiFetch.api.kesehatan.programkesehatan[
|
| null,
|
||||||
"find-many"
|
page: 1,
|
||||||
].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
programKesehatan.findMany.data = res.data?.data ?? [];
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
programKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
programKesehatan.findMany.page = page;
|
||||||
|
programKesehatan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.programkesehatan[
|
||||||
|
"find-many"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
programKesehatan.findMany.data = res.data.data ?? [];
|
||||||
|
programKesehatan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
programKesehatan.findMany.data = [];
|
||||||
|
programKesehatan.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch berita paginated:", err);
|
||||||
|
programKesehatan.findMany.data = [];
|
||||||
|
programKesehatan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
programKesehatan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -97,12 +124,15 @@ const programKesehatan = proxy({
|
|||||||
try {
|
try {
|
||||||
programKesehatan.delete.loading = true;
|
programKesehatan.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/kesehatan/programkesehatan/del/${id}`, {
|
const response = await fetch(
|
||||||
method: "DELETE",
|
`/api/kesehatan/programkesehatan/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) {
|
||||||
@@ -156,57 +186,70 @@ const programKesehatan = proxy({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching program kesehatan:", error);
|
console.error("Error fetching program kesehatan:", 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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
const cek = templateForm.safeParse(programKesehatan.edit.form);
|
const cek = templateForm.safeParse(programKesehatan.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(".")}`)
|
||||||
.join("\n")}] required`;
|
.join("\n")}] required`;
|
||||||
return toast.error(err);
|
return toast.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
programKesehatan.edit.loading = true;
|
programKesehatan.edit.loading = true;
|
||||||
const response = await fetch(`/api/kesehatan/programkesehatan/${this.id}`, {
|
const response = await fetch(
|
||||||
method: "PUT",
|
`/api/kesehatan/programkesehatan/${this.id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
name: this.form.name,
|
},
|
||||||
deskripsiSingkat: this.form.deskripsiSingkat,
|
body: JSON.stringify({
|
||||||
deskripsi: this.form.deskripsi,
|
name: this.form.name,
|
||||||
imageId: this.form.imageId,
|
deskripsiSingkat: this.form.deskripsiSingkat,
|
||||||
}),
|
deskripsi: this.form.deskripsi,
|
||||||
});
|
imageId: this.form.imageId,
|
||||||
if (!response.ok) {
|
}),
|
||||||
const errorData = await response.json().catch(() => ({}));
|
}
|
||||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
);
|
||||||
}
|
if (!response.ok) {
|
||||||
const result = await response.json();
|
const errorData = await response.json().catch(() => ({}));
|
||||||
if (result.success) {
|
throw new Error(
|
||||||
toast.success(result.message || "Program kesehatan berhasil diupdate");
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
await programKesehatan.findMany.load();
|
);
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.message || "Gagal update program kesehatan");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Gagal update:", error);
|
|
||||||
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate program kesehatan");
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
programKesehatan.edit.loading = false;
|
|
||||||
}
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(
|
||||||
|
result.message || "Program kesehatan berhasil diupdate"
|
||||||
|
);
|
||||||
|
await programKesehatan.findMany.load();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update program kesehatan");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal update:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat mengupdate program kesehatan"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
programKesehatan.edit.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
programKesehatan.edit.id = "";
|
programKesehatan.edit.id = "";
|
||||||
programKesehatan.edit.form = { ...defaultForm };
|
programKesehatan.edit.form = { ...defaultForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -163,13 +164,43 @@ const puskesmasState = proxy({
|
|||||||
},
|
},
|
||||||
|
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Prisma.PuskesmasGetPayload<{
|
data: null as
|
||||||
include: { image: true; jam: true; kontak: true };
|
| Prisma.PuskesmasGetPayload<{
|
||||||
}>[] | null,
|
include: {
|
||||||
async load() {
|
image: true;
|
||||||
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get();
|
jam: true;
|
||||||
if (res.status === 200) {
|
kontak: true;
|
||||||
puskesmasState.findMany.data = res.data?.data ?? [];
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
puskesmasState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
puskesmasState.findMany.page = page;
|
||||||
|
puskesmasState.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
puskesmasState.findMany.data = res.data.data ?? [];
|
||||||
|
puskesmasState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
puskesmasState.findMany.data = [];
|
||||||
|
puskesmasState.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch berita paginated:", err);
|
||||||
|
puskesmasState.findMany.data = [];
|
||||||
|
puskesmasState.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
puskesmasState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -50,18 +51,50 @@ const apbdes = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as Array<
|
data: null as
|
||||||
Prisma.APBDesGetPayload<{
|
| Prisma.APBDesGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
image: true;
|
image: true;
|
||||||
file: true;
|
file: true;
|
||||||
};
|
};
|
||||||
}>
|
}>[]
|
||||||
> | null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.landingpage.apbdes["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
total: 0,
|
||||||
apbdes.findMany.data = res.data?.data ?? [];
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
|
apbdes.findMany.loading = true; // Use the full path to access the property
|
||||||
|
apbdes.findMany.page = page;
|
||||||
|
apbdes.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.landingpage.apbdes[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
|
query
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
apbdes.findMany.data = res.data.data || [];
|
||||||
|
apbdes.findMany.total = res.data.total || 0;
|
||||||
|
apbdes.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load pegawai:", res.data?.message);
|
||||||
|
apbdes.findMany.data = [];
|
||||||
|
apbdes.findMany.total = 0;
|
||||||
|
apbdes.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
|
apbdes.findMany.data = [];
|
||||||
|
apbdes.findMany.total = 0;
|
||||||
|
apbdes.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
apbdes.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,16 +60,22 @@ const desaAntikorupsi = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
|
||||||
desaAntikorupsi.findMany.page = page;
|
desaAntikorupsi.findMany.page = page;
|
||||||
|
desaAntikorupsi.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
const res = await ApiFetch.api.landingpage.desaantikorupsi[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
desaAntikorupsi.findMany.data = res.data.data || [];
|
desaAntikorupsi.findMany.data = res.data.data || [];
|
||||||
desaAntikorupsi.findMany.total = res.data.total || 0;
|
desaAntikorupsi.findMany.total = res.data.total || 0;
|
||||||
@@ -305,20 +311,25 @@ const kategoriDesaAntiKorupsi = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
|
||||||
kategoriDesaAntiKorupsi.findMany.page = page;
|
kategoriDesaAntiKorupsi.findMany.page = page;
|
||||||
|
kategoriDesaAntiKorupsi.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.landingpage.kategoridak[
|
const query: any = { page, limit };
|
||||||
"findMany"
|
if (search) query.search = search;
|
||||||
].get({
|
|
||||||
query: { page, limit },
|
const res = await ApiFetch.api.landingpage.kategoridak["findMany"].get({
|
||||||
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
kategoriDesaAntiKorupsi.findMany.data = res.data.data || [];
|
kategoriDesaAntiKorupsi.findMany.data = res.data.data || [];
|
||||||
kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0;
|
kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0;
|
||||||
kategoriDesaAntiKorupsi.findMany.totalPages = res.data.totalPages || 1;
|
kategoriDesaAntiKorupsi.findMany.totalPages =
|
||||||
|
res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load media sosial:", res.data?.message);
|
console.error("Failed to load media sosial:", res.data?.message);
|
||||||
kategoriDesaAntiKorupsi.findMany.data = [];
|
kategoriDesaAntiKorupsi.findMany.data = [];
|
||||||
@@ -363,27 +374,30 @@ const kategoriDesaAntiKorupsi = proxy({
|
|||||||
try {
|
try {
|
||||||
kategoriDesaAntiKorupsi.delete.loading = true;
|
kategoriDesaAntiKorupsi.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/landingpage/kategoridak/del/${id}`, {
|
||||||
`/api/landingpage/kategoridak/del/${id}`,
|
method: "DELETE",
|
||||||
{
|
headers: {
|
||||||
method: "DELETE",
|
"Content-Type": "application/json",
|
||||||
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 || "Kategori desa anti korupsi berhasil dihapus");
|
toast.success(
|
||||||
|
result.message || "Kategori desa anti korupsi berhasil dihapus"
|
||||||
|
);
|
||||||
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
|
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus kategori desa anti korupsi");
|
toast.error(
|
||||||
|
result?.message || "Gagal menghapus kategori desa anti korupsi"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
console.error("Gagal delete:", error);
|
||||||
toast.error("Terjadi kesalahan saat menghapus kategori desa anti korupsi");
|
toast.error(
|
||||||
|
"Terjadi kesalahan saat menghapus kategori desa anti korupsi"
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
kategoriDesaAntiKorupsi.delete.loading = false;
|
kategoriDesaAntiKorupsi.delete.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,13 @@ const responden = proxy({
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(this.form),
|
body: JSON.stringify({
|
||||||
|
name: this.form.name,
|
||||||
|
tanggal: this.form.tanggal,
|
||||||
|
jenisKelaminId: this.form.jenisKelaminId,
|
||||||
|
ratingId: this.form.ratingId,
|
||||||
|
kelompokUmurId: this.form.kelompokUmurId,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!response.ok || !result?.success) {
|
if (!response.ok || !result?.success) {
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -58,16 +59,43 @@ const prestasiDesa = proxy({
|
|||||||
Prisma.PrestasiDesaGetPayload<{
|
Prisma.PrestasiDesaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
image: true;
|
image: true;
|
||||||
kategori: true;
|
kategori: {
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
name: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.landingpage.prestasidesa[
|
totalPages: 1,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
prestasiDesa.findMany.data = res.data?.data ?? [];
|
prestasiDesa.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
prestasiDesa.findMany.page = page;
|
||||||
|
prestasiDesa.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
prestasiDesa.findMany.data = res.data.data ?? [];
|
||||||
|
prestasiDesa.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
prestasiDesa.findMany.data = [];
|
||||||
|
prestasiDesa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch prestasi desa paginated:", err);
|
||||||
|
prestasiDesa.findMany.data = [];
|
||||||
|
prestasiDesa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
prestasiDesa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -283,12 +311,34 @@ const kategoriPrestasi = proxy({
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}> | null,
|
}> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.landingpage.kategoriprestasi[
|
totalPages: 1,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
kategoriPrestasi.findMany.data = res.data?.data ?? [];
|
kategoriPrestasi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
kategoriPrestasi.findMany.page = page;
|
||||||
|
kategoriPrestasi.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.landingpage.kategoriprestasi["find-many"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriPrestasi.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriPrestasi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriPrestasi.findMany.data = [];
|
||||||
|
kategoriPrestasi.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kategori prestasi paginated:", err);
|
||||||
|
kategoriPrestasi.findMany.data = [];
|
||||||
|
kategoriPrestasi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriPrestasi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -65,14 +65,19 @@ const programInovasi = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
programInovasi.findMany.loading = true; // Use the full path to access the property
|
programInovasi.findMany.loading = true; // Use the full path to access the property
|
||||||
programInovasi.findMany.page = page;
|
programInovasi.findMany.page = page;
|
||||||
|
programInovasi.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.programinovasi[
|
const res = await ApiFetch.api.landingpage.programinovasi[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
@@ -482,14 +487,19 @@ const mediaSosial = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
||||||
mediaSosial.findMany.page = page;
|
mediaSosial.findMany.page = page;
|
||||||
try {
|
mediaSosial.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.mediasosial[
|
const res = await ApiFetch.api.landingpage.mediasosial[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
|||||||
@@ -58,14 +58,19 @@ const sdgsDesa = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
sdgsDesa.findMany.loading = true; // Use the full path to access the property
|
sdgsDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
sdgsDesa.findMany.page = page;
|
sdgsDesa.findMany.page = page;
|
||||||
|
sdgsDesa.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
const res = await ApiFetch.api.landingpage.sdgsdesa[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
|||||||
@@ -56,13 +56,17 @@ const dataLingkunganDesaState = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => {
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
// Change to arrow function
|
// Change to arrow function
|
||||||
dataLingkunganDesaState.findMany.loading = true; // Use the full path to access the property
|
dataLingkunganDesaState.findMany.loading = true; // Use the full path to access the property
|
||||||
dataLingkunganDesaState.findMany.page = page;
|
dataLingkunganDesaState.findMany.page = page;
|
||||||
|
dataLingkunganDesaState.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.lingkungan.datalingkungandesa["find-many"].get({
|
const res = await ApiFetch.api.lingkungan.datalingkungandesa["find-many"].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -67,10 +68,46 @@ const kegiatanDesa = proxy({
|
|||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
total: 0,
|
||||||
kegiatanDesa.findMany.data = res.data?.data ?? [];
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
kegiatanDesa.findMany.loading = true; // Use the full path to access the property
|
||||||
|
kegiatanDesa.findMany.page = page;
|
||||||
|
kegiatanDesa.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (kategori) query.kategori = kategori;
|
||||||
|
const res = await ApiFetch.api.lingkungan.kegiatandesa[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kegiatanDesa.findMany.data = res.data.data || [];
|
||||||
|
kegiatanDesa.findMany.total = res.data.total || 0;
|
||||||
|
kegiatanDesa.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load kegiatan desa:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
kegiatanDesa.findMany.data = [];
|
||||||
|
kegiatanDesa.findMany.total = 0;
|
||||||
|
kegiatanDesa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading kegiatan desa:", error);
|
||||||
|
kegiatanDesa.findMany.data = [];
|
||||||
|
kegiatanDesa.findMany.total = 0;
|
||||||
|
kegiatanDesa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kegiatanDesa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -244,6 +281,35 @@ const kegiatanDesa = proxy({
|
|||||||
kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
|
kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
findFirst: {
|
||||||
|
data: null as Prisma.KegiatanDesaGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
kategoriKegiatan: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
// findFirst.load()
|
||||||
|
async load(kategori?: string) {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-first"].get({
|
||||||
|
query: kategori ? { kategori } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
this.data = res.data.data || null;
|
||||||
|
} else {
|
||||||
|
this.data = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch kegiatan desa terbaru:", err);
|
||||||
|
this.data = null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========================================= KATEGORI kegiatan ========================================= //
|
// ========================================= KATEGORI kegiatan ========================================= //
|
||||||
@@ -269,9 +335,7 @@ const kategoriKegiatan = proxy({
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
kategoriKegiatan.create.loading = true;
|
kategoriKegiatan.create.loading = true;
|
||||||
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
|
const res = await ApiFetch.api.lingkungan.kategorikegiatan["create"].post(kategoriKegiatan.create.form);
|
||||||
"create"
|
|
||||||
].post(kategoriKegiatan.create.form);
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
kategoriKegiatan.findMany.load();
|
kategoriKegiatan.findMany.load();
|
||||||
return toast.success("Data berhasil ditambahkan");
|
return toast.success("Data berhasil ditambahkan");
|
||||||
@@ -305,9 +369,7 @@ const kategoriKegiatan = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/lingkungan/kategorikegiatan/${id}`);
|
||||||
`/api/lingkungan/kategorikegiatan/${id}`
|
|
||||||
);
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
kategoriKegiatan.findUnique.data = data.data ?? null;
|
kategoriKegiatan.findUnique.data = data.data ?? null;
|
||||||
@@ -367,15 +429,12 @@ const kategoriKegiatan = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/lingkungan/kategorikegiatan/${id}`, {
|
||||||
`/api/lingkungan/kategorikegiatan/${id}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,15 +52,19 @@ const pengelolaanSampah = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => {
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
// Change to arrow function
|
// Change to arrow function
|
||||||
pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
|
pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
|
||||||
pengelolaanSampah.findMany.page = page;
|
pengelolaanSampah.findMany.page = page;
|
||||||
|
pengelolaanSampah.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
|
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get({
|
].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
@@ -265,7 +269,7 @@ const keteranganSampah = proxy({
|
|||||||
try {
|
try {
|
||||||
keteranganSampah.create.loading = true;
|
keteranganSampah.create.loading = true;
|
||||||
const res =
|
const res =
|
||||||
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
|
await ApiFetch.api.lingkungan.keteranganbankterdekat[
|
||||||
"create"
|
"create"
|
||||||
].post(keteranganSampah.create.form);
|
].post(keteranganSampah.create.form);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -287,14 +291,47 @@ const keteranganSampah = proxy({
|
|||||||
omit: { isActive: true };
|
omit: { isActive: true };
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
keteranganSampah.findMany.data = res.data?.data ?? [];
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
}
|
// Change to arrow function
|
||||||
},
|
keteranganSampah.findMany.loading = true; // Use the full path to access the property
|
||||||
|
keteranganSampah.findMany.page = page;
|
||||||
|
keteranganSampah.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
keteranganSampah.findMany.data = res.data.data || [];
|
||||||
|
keteranganSampah.findMany.total = res.data.total || 0;
|
||||||
|
keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load keterangan bank sampah terdekat:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
keteranganSampah.findMany.data = [];
|
||||||
|
keteranganSampah.findMany.total = 0;
|
||||||
|
keteranganSampah.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading keterangan bank sampah terdekat:", error);
|
||||||
|
keteranganSampah.findMany.data = [];
|
||||||
|
keteranganSampah.findMany.total = 0;
|
||||||
|
keteranganSampah.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
keteranganSampah.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
|
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
|
||||||
@@ -302,7 +339,7 @@ const keteranganSampah = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`);
|
const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
keteranganSampah.findUnique.data = data.data ?? null;
|
keteranganSampah.findUnique.data = data.data ?? null;
|
||||||
@@ -324,7 +361,7 @@ const keteranganSampah = proxy({
|
|||||||
try {
|
try {
|
||||||
keteranganSampah.delete.loading = true;
|
keteranganSampah.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, {
|
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/del/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -359,7 +396,7 @@ const keteranganSampah = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, {
|
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -404,7 +441,7 @@ const keteranganSampah = proxy({
|
|||||||
try {
|
try {
|
||||||
keteranganSampah.edit.loading = true;
|
keteranganSampah.edit.loading = true;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`,
|
`/api/lingkungan/keteranganbankterdekat/${this.id}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -56,13 +56,17 @@ const programPenghijauanState = proxy({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => {
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
// Change to arrow function
|
// Change to arrow function
|
||||||
programPenghijauanState.findMany.loading = true; // Use the full path to access the property
|
programPenghijauanState.findMany.loading = true; // Use the full path to access the property
|
||||||
programPenghijauanState.findMany.page = page;
|
programPenghijauanState.findMany.page = page;
|
||||||
|
programPenghijauanState.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
const res = await ApiFetch.api.lingkungan.programpenghijauan["find-many"].get({
|
const res = await ApiFetch.api.lingkungan.programpenghijauan["find-many"].get({
|
||||||
query: { page, limit },
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
// ========================================= BEASISWA PENDAFTAR ========================================= //
|
||||||
|
|
||||||
const templateBeasiswaPendaftar = z.object({
|
const templateBeasiswaPendaftar = z.object({
|
||||||
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
||||||
nik: z.string().min(1, "NIK harus diisi"),
|
nik: z.string().min(1, "NIK harus diisi"),
|
||||||
@@ -76,13 +79,34 @@ const beasiswaPendaftar = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
"findMany"
|
beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
].get();
|
beasiswaPendaftar.findMany.page = page;
|
||||||
if (res.status === 200) {
|
beasiswaPendaftar.findMany.search = search;
|
||||||
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
beasiswaPendaftar.findMany.data = res.data.data ?? [];
|
||||||
|
beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
beasiswaPendaftar.findMany.data = [];
|
||||||
|
beasiswaPendaftar.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch beasiswa pendaftar paginated:", err);
|
||||||
|
beasiswaPendaftar.findMany.data = [];
|
||||||
|
beasiswaPendaftar.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
beasiswaPendaftar.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -275,8 +299,260 @@ const beasiswaPendaftar = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========================================= KEUNGGULAN PROGRAM ========================================= //
|
||||||
|
const templateKeunggulanProgram = z.object({
|
||||||
|
judul: z.string().min(1, "Judul harus diisi"),
|
||||||
|
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultKeunggulanProgram = {
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const keunggulanProgram = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...defaultKeunggulanProgram },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateKeunggulanProgram.safeParse(
|
||||||
|
keunggulanProgram.create.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keunggulanProgram.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram[
|
||||||
|
"create"
|
||||||
|
].post(keunggulanProgram.create.form);
|
||||||
|
if (res.status === 200) {
|
||||||
|
keunggulanProgram.findMany.load();
|
||||||
|
return toast.success("Data Berhasil Dibuat, 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 {
|
||||||
|
keunggulanProgram.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: [] as Prisma.KeunggulanProgramGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
keunggulanProgram.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
keunggulanProgram.findMany.page = page;
|
||||||
|
keunggulanProgram.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
keunggulanProgram.findMany.data = res.data.data ?? [];
|
||||||
|
keunggulanProgram.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
keunggulanProgram.findMany.data = [];
|
||||||
|
keunggulanProgram.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch keunggulan program paginated:", err);
|
||||||
|
keunggulanProgram.findMany.data = [];
|
||||||
|
keunggulanProgram.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.KeunggulanProgramGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
keunggulanProgram.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch data", res.status, res.statusText);
|
||||||
|
keunggulanProgram.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
keunggulanProgram.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async delete(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
|
try {
|
||||||
|
keunggulanProgram.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(result.message || "Keunggulan Program berhasil dihapus");
|
||||||
|
await keunggulanProgram.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(result?.message || "Gagal menghapus keunggulan program");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus keunggulan program");
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultKeunggulanProgram },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
judul: data.judul,
|
||||||
|
deskripsi: data.deskripsi,
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading keunggulan program:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = templateKeunggulanProgram.safeParse(
|
||||||
|
keunggulanProgram.update.form
|
||||||
|
);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keunggulanProgram.update.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/pendidikan/beasiswa/keunggulanprogram/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
judul: this.form.judul,
|
||||||
|
deskripsi: this.form.deskripsi,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(
|
||||||
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Berhasil update keunggulan program");
|
||||||
|
await keunggulanProgram.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update keunggulan program");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating keunggulan program:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update keunggulan program"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
keunggulanProgram.update.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
keunggulanProgram.update.id = "";
|
||||||
|
keunggulanProgram.update.form = { ...defaultKeunggulanProgram };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const beasiswaDesaState = proxy({
|
const beasiswaDesaState = proxy({
|
||||||
beasiswaPendaftar,
|
beasiswaPendaftar,
|
||||||
|
keunggulanProgram
|
||||||
});
|
});
|
||||||
|
|
||||||
export default beasiswaDesaState;
|
export default beasiswaDesaState;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -51,13 +52,46 @@ const jenjangPendidikan = proxy({
|
|||||||
id: string;
|
id: string;
|
||||||
nama: string;
|
nama: string;
|
||||||
}> | null,
|
}> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res =
|
totalPages: 1,
|
||||||
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
|
total: 0,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
jenjangPendidikan.findMany.data = res.data?.data ?? [];
|
// Change to arrow function
|
||||||
|
jenjangPendidikan.findMany.loading = true; // Use the full path to access the property
|
||||||
|
jenjangPendidikan.findMany.page = page;
|
||||||
|
jenjangPendidikan.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
const res =
|
||||||
|
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
jenjangPendidikan.findMany.data = res.data.data || [];
|
||||||
|
jenjangPendidikan.findMany.total = res.data.total || 0;
|
||||||
|
jenjangPendidikan.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load jenjang pendidikan:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
jenjangPendidikan.findMany.data = [];
|
||||||
|
jenjangPendidikan.findMany.total = 0;
|
||||||
|
jenjangPendidikan.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading jenjang pendidikan:", error);
|
||||||
|
jenjangPendidikan.findMany.data = [];
|
||||||
|
jenjangPendidikan.findMany.total = 0;
|
||||||
|
jenjangPendidikan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
jenjangPendidikan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -299,18 +333,64 @@ const lembagaPendidikan = proxy({
|
|||||||
Prisma.LembagaGetPayload<{
|
Prisma.LembagaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
jenjangPendidikan: true;
|
jenjangPendidikan: true;
|
||||||
siswa: true;
|
|
||||||
pengajar: true;
|
|
||||||
};
|
};
|
||||||
}>
|
}> & {
|
||||||
|
siswa?: [];
|
||||||
|
pengajar?: [];
|
||||||
|
}
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res =
|
totalPages: 1,
|
||||||
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
|
total: 0,
|
||||||
"find-many"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||||
lembagaPendidikan.findMany.data = res.data?.data ?? [];
|
lembagaPendidikan.findMany.loading = true;
|
||||||
|
lembagaPendidikan.findMany.page = page;
|
||||||
|
lembagaPendidikan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
...(search && { search }),
|
||||||
|
...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Fetching lembaga with query:', query);
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
|
||||||
|
|
||||||
|
console.log('API Response:', res);
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
const data = Array.isArray(res.data.data) ? res.data.data : [];
|
||||||
|
const total = typeof res.data.total === 'number' ? res.data.total : 0;
|
||||||
|
const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
|
||||||
|
|
||||||
|
lembagaPendidikan.findMany.data = data;
|
||||||
|
lembagaPendidikan.findMany.total = total;
|
||||||
|
lembagaPendidikan.findMany.totalPages = totalPages;
|
||||||
|
|
||||||
|
console.log('Successfully loaded lembaga data:', {
|
||||||
|
count: data.length,
|
||||||
|
total,
|
||||||
|
totalPages
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load lembaga pendidikan:",
|
||||||
|
res.data?.message || 'No error message provided'
|
||||||
|
);
|
||||||
|
throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading lembaga pendidikan:", error);
|
||||||
|
lembagaPendidikan.findMany.data = [];
|
||||||
|
lembagaPendidikan.findMany.total = 0;
|
||||||
|
lembagaPendidikan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
lembagaPendidikan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -554,16 +634,55 @@ const siswa = proxy({
|
|||||||
data: null as Array<
|
data: null as Array<
|
||||||
Prisma.SiswaGetPayload<{
|
Prisma.SiswaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
lembaga: true;
|
lembaga: {
|
||||||
|
include: {
|
||||||
|
jenjangPendidikan: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
siswa.findMany.data = res.data?.data ?? [];
|
jenjangPendidikan: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||||
|
siswa.findMany.loading = true;
|
||||||
|
siswa.findMany.page = page;
|
||||||
|
siswa.findMany.search = search;
|
||||||
|
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
|
||||||
|
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
siswa.findMany.data = res.data.data || [];
|
||||||
|
siswa.findMany.total = res.data.total || 0;
|
||||||
|
siswa.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load siswa:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
siswa.findMany.data = [];
|
||||||
|
siswa.findMany.total = 0;
|
||||||
|
siswa.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading siswa:", error);
|
||||||
|
siswa.findMany.data = [];
|
||||||
|
siswa.findMany.total = 0;
|
||||||
|
siswa.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
siswa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -794,16 +913,56 @@ const pengajar = proxy({
|
|||||||
data: null as Array<
|
data: null as Array<
|
||||||
Prisma.PengajarGetPayload<{
|
Prisma.PengajarGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
lembaga: true;
|
lembaga: {
|
||||||
|
include: {
|
||||||
|
jenjangPendidikan: true
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
> | null,
|
> | null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get();
|
loading: false,
|
||||||
if (res.status === 200) {
|
search: "",
|
||||||
pengajar.findMany.data = res.data?.data ?? [];
|
jenjangPendidikan: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pengajar.findMany.loading = true; // Use the full path to access the property
|
||||||
|
pengajar.findMany.page = page;
|
||||||
|
pengajar.findMany.search = search;
|
||||||
|
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
|
||||||
|
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pengajar.findMany.data = res.data.data || [];
|
||||||
|
pengajar.findMany.total = res.data.total || 0;
|
||||||
|
pengajar.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to load pengajar:",
|
||||||
|
res.data?.message
|
||||||
|
);
|
||||||
|
pengajar.findMany.data = [];
|
||||||
|
pengajar.findMany.total = 0;
|
||||||
|
pengajar.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pengajar:", error);
|
||||||
|
pengajar.findMany.data = [];
|
||||||
|
pengajar.findMany.total = 0;
|
||||||
|
pengajar.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
pengajar.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -815,7 +974,9 @@ const pengajar = proxy({
|
|||||||
}> | null,
|
}> | null,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`);
|
const res = await fetch(
|
||||||
|
`/api/pendidikan/infosekolahpaud/pengajar/${id}`
|
||||||
|
);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
pengajar.findUnique.data = data.data ?? null;
|
pengajar.findUnique.data = data.data ?? null;
|
||||||
@@ -948,7 +1109,8 @@ const pengajar = proxy({
|
|||||||
result
|
result
|
||||||
);
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
result?.message || `Gagal mengupdate pengajar (${response.status})`
|
result?.message ||
|
||||||
|
`Gagal mengupdate pengajar (${response.status})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,23 +55,46 @@ const dataPerpustakaan = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.DataPerpustakaanGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.DataPerpustakaanGetPayload<{
|
||||||
kategori: true;
|
include: {
|
||||||
image: true;
|
image: true;
|
||||||
};
|
kategori: true;
|
||||||
}>[],
|
};
|
||||||
loading: false,
|
}>[]
|
||||||
async load() {
|
| null,
|
||||||
const res =
|
page: 1,
|
||||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
totalPages: 1,
|
||||||
"findMany"
|
loading: false,
|
||||||
].get();
|
search: "",
|
||||||
if (res.status === 200) {
|
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||||
dataPerpustakaan.findMany.data = res.data?.data ?? [];
|
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
}
|
dataPerpustakaan.findMany.page = page;
|
||||||
|
dataPerpustakaan.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
if (kategori) query.kategori = kategori;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||||
|
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
dataPerpustakaan.findMany.data = [];
|
||||||
|
dataPerpustakaan.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||||
|
dataPerpustakaan.findMany.data = [];
|
||||||
|
dataPerpustakaan.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
dataPerpustakaan.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.DataPerpustakaanGetPayload<{
|
data: null as Prisma.DataPerpustakaanGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
@@ -293,14 +317,34 @@ const kategoriBuku = proxy({
|
|||||||
isActive: true;
|
isActive: true;
|
||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
const res =
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
|
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
"findMany"
|
kategoriBuku.findMany.page = page;
|
||||||
].get();
|
kategoriBuku.findMany.search = search;
|
||||||
if (res.status === 200) {
|
|
||||||
kategoriBuku.findMany.data = res.data?.data ?? [];
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
kategoriBuku.findMany.data = res.data.data ?? [];
|
||||||
|
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
kategoriBuku.findMany.data = [];
|
||||||
|
kategoriBuku.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch data kategori buku paginated:", err);
|
||||||
|
kategoriBuku.findMany.data = [];
|
||||||
|
kategoriBuku.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
kategoriBuku.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,35 +49,38 @@ const daftarInformasiPublik = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as any[] | null,
|
data: null as
|
||||||
|
| Prisma.DaftarInformasiPublikGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
search: "",
|
||||||
daftarInformasiPublik.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
daftarInformasiPublik.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
daftarInformasiPublik.findMany.page = page;
|
daftarInformasiPublik.findMany.page = page;
|
||||||
try {
|
daftarInformasiPublik.findMany.search = search;
|
||||||
const res = await ApiFetch.api.ppid.daftarinformasipublik[
|
|
||||||
"find-many"
|
|
||||||
].get({
|
|
||||||
query: { page, limit },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
daftarInformasiPublik.findMany.data = res.data.data || [];
|
daftarInformasiPublik.findMany.data = res.data.data ?? [];
|
||||||
daftarInformasiPublik.findMany.total = res.data.total || 0;
|
daftarInformasiPublik.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
daftarInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load daftar informasi publik:", res.data?.message);
|
|
||||||
daftarInformasiPublik.findMany.data = [];
|
daftarInformasiPublik.findMany.data = [];
|
||||||
daftarInformasiPublik.findMany.total = 0;
|
|
||||||
daftarInformasiPublik.findMany.totalPages = 1;
|
daftarInformasiPublik.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error loading daftar informasi publik:", error);
|
console.error("Gagal fetch daftar informasi publik paginated:", err);
|
||||||
daftarInformasiPublik.findMany.data = [];
|
daftarInformasiPublik.findMany.data = [];
|
||||||
daftarInformasiPublik.findMany.total = 0;
|
|
||||||
daftarInformasiPublik.findMany.totalPages = 1;
|
daftarInformasiPublik.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
daftarInformasiPublik.findMany.loading = false;
|
daftarInformasiPublik.findMany.loading = false;
|
||||||
|
|||||||
@@ -348,18 +348,34 @@ const posisiOrganisasi = proxy({
|
|||||||
deskripsi: string | null;
|
deskripsi: string | null;
|
||||||
hierarki: number;
|
hierarki: number;
|
||||||
}>,
|
}>,
|
||||||
async load() {
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
posisiOrganisasi.findMany.page = page;
|
||||||
|
posisiOrganisasi.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
const query: any = { page, limit };
|
||||||
"find-many"
|
if (search) query.search = search;
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query });
|
||||||
// The API now returns the id field, so we can use it directly
|
|
||||||
this.data = res.data?.data ?? [];
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
posisiOrganisasi.findMany.data = res.data.data ?? [];
|
||||||
|
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
posisiOrganisasi.findMany.data = [];
|
||||||
|
posisiOrganisasi.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Find many error:", error);
|
console.error("Gagal fetch posisi organisasi paginated:", err);
|
||||||
this.data = [];
|
posisiOrganisasi.findMany.data = [];
|
||||||
|
posisiOrganisasi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
posisiOrganisasi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -438,9 +454,9 @@ const pegawai = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
pegawai.create.loading = true;
|
pegawai.create.loading = true;
|
||||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(
|
||||||
"create"
|
pegawai.create.form
|
||||||
].post(pegawai.create.form);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Pegawai berhasil ditambahkan");
|
toast.success("Pegawai berhasil ditambahkan");
|
||||||
await pegawai.findMany.load();
|
await pegawai.findMany.load();
|
||||||
@@ -457,42 +473,55 @@ const pegawai = proxy({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// In struktur-organisasi.ts
|
// In struktur-organisasi.ts
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as any[] | null,
|
data: null as
|
||||||
page: 1,
|
| Prisma.PegawaiPPIDGetPayload<{
|
||||||
totalPages: 1,
|
include: {
|
||||||
total: 0,
|
image: true;
|
||||||
loading: false,
|
posisi: true;
|
||||||
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;
|
| null,
|
||||||
try {
|
page: 1,
|
||||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get({
|
loading: false,
|
||||||
query: { page, limit },
|
search: "",
|
||||||
});
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
pegawai.findMany.loading = true; // Use the full path to access the property
|
||||||
|
pegawai.findMany.page = page;
|
||||||
|
pegawai.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||||
pegawai.findMany.data = res.data.data || [];
|
"find-many"
|
||||||
pegawai.findMany.total = res.data.total || 0;
|
].get({
|
||||||
pegawai.findMany.totalPages = res.data.totalPages || 1;
|
query,
|
||||||
} else {
|
});
|
||||||
console.error("Failed to load pegawai:", res.data?.message);
|
|
||||||
|
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.data = [];
|
||||||
pegawai.findMany.total = 0;
|
pegawai.findMany.total = 0;
|
||||||
pegawai.findMany.totalPages = 1;
|
pegawai.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
pegawai.findMany.loading = false;
|
||||||
}
|
}
|
||||||
} 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: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
| (Prisma.PegawaiGetPayload<{
|
| (Prisma.PegawaiGetPayload<{
|
||||||
@@ -521,12 +550,9 @@ findMany: {
|
|||||||
if (!id) return toast.warn("ID tidak valid");
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
try {
|
try {
|
||||||
pegawai.delete.loading = true;
|
pegawai.delete.loading = true;
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, {
|
||||||
`/api/ppid/strukturppid/pegawai/del/${id}`,
|
method: "DELETE",
|
||||||
{
|
});
|
||||||
method: "DELETE",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success(json.message ?? "Berhasil hapus pegawai");
|
toast.success(json.message ?? "Berhasil hapus pegawai");
|
||||||
@@ -555,15 +581,12 @@ findMany: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/ppid/strukturppid/pegawai/${id}`, {
|
||||||
`/api/ppid/strukturppid/pegawai/${id}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"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}`);
|
||||||
@@ -677,7 +700,7 @@ findMany: {
|
|||||||
const stateStrukturPPID = proxy({
|
const stateStrukturPPID = proxy({
|
||||||
stateStruktur,
|
stateStruktur,
|
||||||
posisiOrganisasi,
|
posisiOrganisasi,
|
||||||
pegawai
|
pegawai,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default stateStrukturPPID;
|
export default stateStrukturPPID;
|
||||||
|
|||||||
@@ -1,124 +1,43 @@
|
|||||||
import { proxy } from 'valtio'
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { toast } from 'react-toastify'
|
import { proxy } from "valtio";
|
||||||
import ApiFetch from '@/lib/api-fetch'
|
import { toast } from "react-toastify";
|
||||||
import { Prisma } from '@prisma/client'
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { z } from 'zod'
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
// 1. Validasi Zod
|
// State Valtio
|
||||||
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({
|
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
|
// Find Many
|
||||||
findMany: {
|
findMany: {
|
||||||
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
search: "",
|
||||||
this.loading = true
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
userState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
userState.findMany.page = page;
|
||||||
|
userState.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await ApiFetch.api.user.findMany.get()
|
const query: any = { page, limit };
|
||||||
if (res.status === 200) {
|
if (search) query.search = search;
|
||||||
this.data = res.data?.data || []
|
|
||||||
|
const res = await ApiFetch.api.user["findMany"].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
userState.findMany.data = res.data.data ?? [];
|
||||||
|
userState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
userState.findMany.data = [];
|
||||||
|
userState.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error(e)
|
console.error("Gagal fetch user paginated:", err);
|
||||||
toast.error('Gagal muat data user')
|
userState.findMany.data = [];
|
||||||
|
userState.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
userState.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -128,71 +47,20 @@ const userState = proxy({
|
|||||||
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
||||||
loading: false,
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
this.loading = true
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/findUnique/${id}`)
|
const res = await fetch(`/api/user/findUnique/${id}`);
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
this.data = data.data
|
this.data = data.data;
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message)
|
toast.error(data.message);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
toast.error('Gagal ambil data user')
|
toast.error("Gagal ambil data user");
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -201,35 +69,37 @@ const userState = proxy({
|
|||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async submit(id: string) {
|
async submit(id: string) {
|
||||||
this.loading = true
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/user/del/${id}`, {
|
const res = await fetch(`/api/user/del/${id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
});
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success('User dinonaktifkan')
|
toast.success("User dinonaktifkan");
|
||||||
userState.findMany.load()
|
userState.findMany.load();
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || 'Gagal hapus')
|
toast.error(data.message || "Gagal hapus");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
toast.error('Gagal hapus user')
|
toast.error("Gagal hapus user");
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const templateRole = z.object({
|
const templateRole = z.object({
|
||||||
name: z.string().min(1, "Nama harus diisi"),
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
|
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultRole = {
|
const defaultRole = {
|
||||||
name: "",
|
name: "",
|
||||||
|
permissions: [] as string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleState = proxy({
|
const roleState = proxy({
|
||||||
@@ -247,10 +117,9 @@ const roleState = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
roleState.create.loading = true;
|
roleState.create.loading = true;
|
||||||
const res =
|
const res = await ApiFetch.api.role["create"].post(
|
||||||
await ApiFetch.api.role[
|
roleState.create.form
|
||||||
"create"
|
);
|
||||||
].post(roleState.create.form);
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
roleState.findMany.load();
|
roleState.findMany.load();
|
||||||
return toast.success("Data role Berhasil Dibuat");
|
return toast.success("Data role Berhasil Dibuat");
|
||||||
@@ -273,10 +142,7 @@ const roleState = proxy({
|
|||||||
}>[],
|
}>[],
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
async load() {
|
||||||
const res =
|
const res = await ApiFetch.api.role["findMany"].get();
|
||||||
await ApiFetch.api.role[
|
|
||||||
"findMany"
|
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
roleState.findMany.data = res.data?.data ?? [];
|
roleState.findMany.data = res.data?.data ?? [];
|
||||||
}
|
}
|
||||||
@@ -291,9 +157,7 @@ const roleState = proxy({
|
|||||||
loading: false,
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/role/${id}`);
|
||||||
`/api/role/${id}`
|
|
||||||
);
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
roleState.findUnique.data = data.data ?? null;
|
roleState.findUnique.data = data.data ?? null;
|
||||||
@@ -315,22 +179,17 @@ const roleState = proxy({
|
|||||||
try {
|
try {
|
||||||
roleState.delete.loading = true;
|
roleState.delete.loading = true;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/role/del/${id}`, {
|
||||||
`/api/role/del/${id}`,
|
method: "DELETE",
|
||||||
{
|
headers: {
|
||||||
method: "DELETE",
|
"Content-Type": "application/json",
|
||||||
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(
|
toast.success(result.message || "Data role berhasil dihapus");
|
||||||
result.message || "Data role berhasil dihapus"
|
|
||||||
);
|
|
||||||
await roleState.findMany.load(); // refresh list
|
await roleState.findMany.load(); // refresh list
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus Data role");
|
toast.error(result?.message || "Gagal menghapus Data role");
|
||||||
@@ -354,15 +213,12 @@ const roleState = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/role/${id}`, {
|
||||||
`/api/role/${id}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"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}`);
|
||||||
}
|
}
|
||||||
@@ -374,6 +230,7 @@ const roleState = proxy({
|
|||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
permissions: data.permissions,
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -400,18 +257,16 @@ const roleState = proxy({
|
|||||||
try {
|
try {
|
||||||
roleState.update.loading = true;
|
roleState.update.loading = true;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/role/${this.id}`, {
|
||||||
`/api/role/${this.id}`,
|
method: "PUT",
|
||||||
{
|
headers: {
|
||||||
method: "PUT",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({
|
||||||
},
|
name: this.form.name,
|
||||||
body: JSON.stringify({
|
permissions: this.form.permissions,
|
||||||
name: this.form.name,
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
@@ -451,6 +306,6 @@ const roleState = proxy({
|
|||||||
const user = proxy({
|
const user = proxy({
|
||||||
userState,
|
userState,
|
||||||
roleState,
|
roleState,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default user
|
export default user;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } 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';
|
||||||
|
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,26 +13,35 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Pelayanan Surat Keterangan",
|
label: "Pelayanan Surat Keterangan",
|
||||||
value: "pelayanansuratketerangan",
|
value: "pelayanansuratketerangan",
|
||||||
href: "/admin/desa/layanan/pelayanan_surat_keterangan"
|
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
||||||
|
icon: <IconFileText size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan terkait surat keterangan resmi desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Perizinan Berusaha",
|
label: "Pelayanan Perizinan Berusaha",
|
||||||
value: "pelayananperizinanusaha",
|
value: "pelayananperizinanusaha",
|
||||||
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha"
|
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
||||||
|
icon: <IconBuildingStore size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan untuk izin usaha masyarakat"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Telunjuk Sakti Desa",
|
label: "Pelayanan Telunjuk Sakti Desa",
|
||||||
value: "pelayanantelunjuksaktidesa",
|
value: "pelayanantelunjuksaktidesa",
|
||||||
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa"
|
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
||||||
|
icon: <IconSparkles size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Layanan inovasi khusus desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Penduduk Non-Permanent",
|
label: "Pelayanan Penduduk Non-Permanent",
|
||||||
value: "pelayanantelunjuknonpermanent",
|
value: "pelayanannonpermanent",
|
||||||
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent"
|
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
||||||
|
icon: <IconUsers size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Pendataan penduduk non-permanent"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -49,24 +59,65 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Layanan</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsLayanan;
|
export default LayoutTabsLayanan;
|
||||||
|
|||||||
@@ -1,63 +1,110 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } 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';
|
||||||
|
import { IconNews, IconCategory } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsBerita({ 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 Berita",
|
label: "List Berita",
|
||||||
value: "list_berita",
|
value: "list_berita",
|
||||||
href: "/admin/desa/berita/list-berita"
|
href: "/admin/desa/berita/list-berita",
|
||||||
|
icon: <IconNews size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat dan kelola semua berita desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Berita",
|
label: "Kategori Berita",
|
||||||
value: "kategori_berita",
|
value: "kategori_berita",
|
||||||
href: "/admin/desa/berita/kategori-berita"
|
href: "/admin/desa/berita/kategori-berita",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori berita desa"
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href)
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
setActiveTab(value)
|
setActiveTab(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname)
|
const match = tabs.find(tab => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value)
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname])
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant="pills"
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsBerita;
|
export default LayoutTabsBerita;
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
@@ -10,67 +19,102 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriBerita() {
|
function EditKategoriBerita() {
|
||||||
const editState = useProxy(stateDashboardBerita.kategoriBerita)
|
const editState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: editState.update.form.name || '',
|
name: editState.update.form.name || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
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 {
|
try {
|
||||||
editState.update.form = {
|
const data = await editState.update.load(id);
|
||||||
...editState.update.form,
|
if (data) {
|
||||||
name: formData.name,
|
setFormData({
|
||||||
};
|
name: data.name || '',
|
||||||
await editState.update.update();
|
});
|
||||||
toast.success('Kategori Berita berhasil diperbarui!');
|
}
|
||||||
router.push('/admin/desa/berita/kategori-berita');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating kategori Berita:', error);
|
console.error('Error loading kategori Berita:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
|
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 (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Edit Kategori Berita</Title>
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Kategori Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form Wrapper */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Kategori Berita"
|
||||||
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>}
|
required
|
||||||
placeholder="masukkan nama kategori Berita"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriBerita() {
|
function CreateKategoriBerita() {
|
||||||
const createState = useProxy(stateDashboardBerita.kategoriBerita)
|
const createState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/berita/kategori-berita")
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Berita</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
|
||||||
placeholder='Masukkan nama kategori Berita'
|
placeholder="Masukkan nama kategori berita"
|
||||||
value={createState.create.form.name}
|
value={createState.create.form.name || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.name = e.target.value)}
|
||||||
createState.create.form.name = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
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 {
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import stateDashboardBerita from '../../../_state/desa/berita';
|
import stateDashboardBerita from '../../../_state/desa/berita';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function KategoriBerita() {
|
function KategoriBerita() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Berita'
|
title="Kategori Berita"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama kategori berita..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -30,99 +45,155 @@ function KategoriBerita() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListKategoriBerita({ search }: { search: string }) {
|
function ListKategoriBerita({ search }: { search: string }) {
|
||||||
const listDataState = useProxy(stateDashboardBerita.kategoriBerita)
|
const listDataState = useProxy(stateDashboardBerita.kategoriBerita);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
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 {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
} = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
|
load(page, 10, search);
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<JudulList
|
<Title order={4}>Daftar Kategori Berita</Title>
|
||||||
title='List Kategori Berita'
|
<Tooltip label="Tambah Kategori Berita" withArrow>
|
||||||
href='/admin/desa/berita/kategori-berita/create'
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
color="blue"
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
variant="light"
|
||||||
<TableThead>
|
onClick={() =>
|
||||||
<TableTr>
|
router.push('/admin/desa/berita/kategori-berita/create')
|
||||||
<TableTh>No</TableTh>
|
}
|
||||||
<TableTh>Nama</TableTh>
|
>
|
||||||
<TableTh>Edit</TableTh>
|
Tambah Baru
|
||||||
<TableTh>Hapus</TableTh>
|
</Button>
|
||||||
</TableTr>
|
</Tooltip>
|
||||||
</TableThead>
|
</Group>
|
||||||
<TableTbody>
|
|
||||||
{filteredData.map((item, index) => (
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
|
<TableTh style={{ width: '50%' }}>Nama</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Edit</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fz="sm">{index + 1}</Text>
|
||||||
<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>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
color='red'
|
{item.name}
|
||||||
disabled={listDataState.delete.loading}
|
</Text>
|
||||||
onClick={() => {
|
</TableTd>
|
||||||
setSelectedId(item.id)
|
<TableTd>
|
||||||
setModalHapus(true)
|
<Tooltip label="Edit Kategori Berita" withArrow>
|
||||||
}}>
|
<Button
|
||||||
<IconTrash size={20} />
|
variant="light"
|
||||||
</Button>
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/berita/kategori-berita/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Tooltip label="Hapus Kategori Berita" withArrow>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
disabled={listDataState.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data kategori berita yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10, search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
text='Apakah anda yakin ingin menghapus kategori Berita ini?'
|
text="Apakah anda yakin ingin menghapus kategori berita ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KategoriBerita;
|
export default KategoriBerita;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* 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,
|
||||||
@@ -12,18 +16,14 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
|
Tooltip,
|
||||||
} 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 stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
|
|
||||||
|
|
||||||
|
|
||||||
function EditBerita() {
|
function EditBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
@@ -33,29 +33,29 @@ function EditBerita() {
|
|||||||
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({
|
const [formData, setFormData] = useState({
|
||||||
judul: beritaState.berita.edit.form.judul || '',
|
judul: beritaState.berita.edit.form.judul || "",
|
||||||
deskripsi: beritaState.berita.edit.form.deskripsi || '',
|
deskripsi: beritaState.berita.edit.form.deskripsi || "",
|
||||||
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '',
|
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
|
||||||
content: beritaState.berita.edit.form.content || '',
|
content: beritaState.berita.edit.form.content || "",
|
||||||
imageId: beritaState.berita.edit.form.imageId || ''
|
imageId: beritaState.berita.edit.form.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load berita by id saat pertama kali
|
// Load berita by id saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load()
|
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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy
|
const data = await stateDashboardBerita.berita.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
judul: data.judul || '',
|
judul: data.judul || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
kategoriBeritaId: data.kategoriBeritaId || '',
|
kategoriBeritaId: data.kategoriBeritaId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
@@ -69,31 +69,26 @@ function EditBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadBerita();
|
loadBerita();
|
||||||
}, [params?.id]); // ✅ hapus beritaState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update global state with form data
|
|
||||||
beritaState.berita.edit.form = {
|
beritaState.berita.edit.form = {
|
||||||
...beritaState.berita.edit.form,
|
...beritaState.berita.edit.form,
|
||||||
judul: formData.judul,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
content: formData.content,
|
|
||||||
kategoriBeritaId: formData.kategoriBeritaId || '',
|
|
||||||
imageId: formData.imageId // Keep existing imageId if not changed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Jika ada file baru, upload
|
|
||||||
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,
|
||||||
|
});
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar");
|
return toast.error("Gagal upload gambar");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update imageId in global state
|
|
||||||
beritaState.berita.edit.form.imageId = uploaded.id;
|
beritaState.berita.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,52 +102,111 @@ function EditBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button
|
||||||
</Button>
|
variant="subtle"
|
||||||
</Box>
|
onClick={() => router.back()}
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
p="xs"
|
||||||
<Stack gap={"xs"}>
|
radius="md"
|
||||||
<Title order={3}>Edit Berita</Title>
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
setFormData({ ...formData, judul: e.target.value })
|
||||||
placeholder="masukkan judul"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
setFormData({ ...formData, deskripsi: e.target.value })
|
||||||
placeholder="masukkan deskripsi"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FileInput
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</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>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Berita
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ "image/*": [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar wajib
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 220,
|
||||||
|
objectFit: "contain",
|
||||||
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -164,13 +218,15 @@ function EditBerita() {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.kategoriBeritaId}
|
value={formData.kategoriBeritaId}
|
||||||
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })}
|
onChange={(val) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
setFormData({ ...formData, kategoriBeritaId: val || "" })
|
||||||
placeholder='Pilih kategori'
|
}
|
||||||
|
label="Kategori"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
beritaState.kategoriBerita.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.name
|
label: v.name,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
@@ -179,7 +235,20 @@ function EditBerita() {
|
|||||||
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit Berita</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
|
||||||
|
color: "#fff",
|
||||||
|
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } 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, IconTrash } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -12,107 +11,146 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
|
||||||
function DetailBerita() {
|
function DetailBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
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(() => {
|
||||||
beritaState.berita.findUnique.load(params?.id as string)
|
beritaState.berita.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
beritaState.berita.delete.byId(selectedId)
|
beritaState.berita.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/berita/list-berita")
|
router.push("/admin/desa/berita/list-berita");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!beritaState.berita.findUnique.data) {
|
if (!beritaState.berita.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = beritaState.berita.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Back */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Berita</Text>
|
Kembali
|
||||||
{beritaState.berita.findUnique.data ? (
|
</Button>
|
||||||
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
{/* Detail Berita */}
|
||||||
<Box>
|
<Paper
|
||||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
withBorder
|
||||||
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text>
|
w={{ base: "100%", md: "70%" }}
|
||||||
</Box>
|
bg={colors['white-1']}
|
||||||
<Box>
|
p="lg"
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
radius="md"
|
||||||
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text>
|
shadow="sm"
|
||||||
</Box>
|
>
|
||||||
<Box>
|
<Stack gap="md">
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text>
|
Detail Berita
|
||||||
</Box>
|
</Text>
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" />
|
<Stack gap="sm">
|
||||||
</Box>
|
<Box>
|
||||||
<Box>
|
<Text fz="lg" fw="bold">Kategori</Text>
|
||||||
<Text fw={"bold"} fz={"lg"}>Konten</Text>
|
<Text fz="md" c="dimmed">{data.kategoriBerita?.name || '-'}</Text>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} />
|
</Box>
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.judul || 'Gambar Berita'}
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Konten</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Berita" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (beritaState.berita.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(beritaState.berita.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Berita" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (beritaState.berita.findUnique.data) {
|
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
||||||
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`);
|
variant="light"
|
||||||
}
|
radius="md"
|
||||||
}}
|
size="md"
|
||||||
disabled={!beritaState.berita.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal 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 berita ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailBerita;
|
export default DetailBerita;
|
||||||
|
|||||||
@@ -3,46 +3,54 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
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, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
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';
|
||||||
|
|
||||||
|
|
||||||
export default function CreateBerita() {
|
export default function CreateBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
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 router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
beritaState.kategoriBerita.findMany.load()
|
beritaState.kategoriBerita.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
// Reset state di valtio
|
|
||||||
beritaState.berita.create.form = {
|
beritaState.berita.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
kategoriBeritaId: "",
|
kategoriBeritaId: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
content: "",
|
content: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset state lokal
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload gambar dulu
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
@@ -50,40 +58,55 @@ export default function CreateBerita() {
|
|||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar");
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simpan ID gambar ke form
|
|
||||||
beritaState.berita.create.form.imageId = uploaded.id;
|
beritaState.berita.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
// Submit data berita
|
|
||||||
await beritaState.berita.create.create();
|
await beritaState.berita.create.create();
|
||||||
|
|
||||||
// Reset form setelah submit
|
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/berita/list-berita")
|
router.push('/admin/desa/berita/list-berita');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Create Berita</Title>
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Berita
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul berita"
|
||||||
value={beritaState.berita.create.form.judul}
|
value={beritaState.berita.create.form.judul}
|
||||||
onChange={(val) => {
|
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
|
||||||
beritaState.berita.create.form.judul = val.target.value;
|
required
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
|
label="Kategori"
|
||||||
placeholder="Pilih kategori"
|
placeholder="Pilih kategori"
|
||||||
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@@ -92,48 +115,83 @@ export default function CreateBerita() {
|
|||||||
value={beritaState.berita.create.form.kategoriBeritaId || null}
|
value={beritaState.berita.create.form.kategoriBeritaId || null}
|
||||||
onChange={(val: string | null) => {
|
onChange={(val: string | null) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
const selected = beritaState.kategoriBerita.findMany.data?.find((item) => item.id === val);
|
const selected = beritaState.kategoriBerita.findMany.data?.find(
|
||||||
|
(item) => item.id === val
|
||||||
|
);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
beritaState.berita.create.form.kategoriBeritaId = selected.id;
|
beritaState.berita.create.form.kategoriBeritaId = selected.id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
beritaState.berita.create.form.kategoriBeritaId = "";
|
beritaState.berita.create.form.kategoriBeritaId = '';
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
/>
|
required
|
||||||
<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
|
<TextInput
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
|
label="Deskripsi Singkat"
|
||||||
value={file}
|
placeholder="Masukkan deskripsi berita"
|
||||||
onChange={async (e) => {
|
value={beritaState.berita.create.form.deskripsi}
|
||||||
if (!e) return;
|
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
|
||||||
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>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Berita
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={beritaState.berita.create.form.content}
|
value={beritaState.berita.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -141,7 +199,21 @@ export default function CreateBerita() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} 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';
|
||||||
@@ -9,15 +28,13 @@ 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() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Berita'
|
title="Berita"
|
||||||
placeholder='pencarian'
|
placeholder="Cari judul atau kategori berita..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -28,103 +45,125 @@ function Berita() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListBerita({ search }: { search: string }) {
|
function ListBerita({ search }: { search: string }) {
|
||||||
const beritaState = useProxy(stateDashboardBerita)
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const {
|
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = beritaState.berita.findMany;
|
|
||||||
|
|
||||||
|
const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
|
||||||
|
|
||||||
// Fetch data when page or search changes
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, search);
|
||||||
}, [page, search]);
|
}, [page, search]);
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return <Skeleton h={500} />;
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={600} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = data || [];
|
const filteredData = data || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors["white-1"]} p={"md"}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<Grid>
|
<Title order={4}>Daftar Berita</Title>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<Tooltip label="Tambah Berita" withArrow>
|
||||||
<Text fz={"xl"} fw={"bold"}>
|
<Button
|
||||||
List Berita
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
</Text>
|
color="blue"
|
||||||
</GridCol>
|
variant="light"
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
onClick={() => router.push('/admin/desa/berita/list-berita/create')}
|
||||||
<Button
|
|
||||||
onClick={() => router.push("/admin/desa/berita/list-berita/create")}
|
|
||||||
bg={colors["blue-button"]}
|
|
||||||
>
|
|
||||||
<IconCircleDashedPlus size={25} />
|
|
||||||
</Button>
|
|
||||||
</GridCol>
|
|
||||||
</Grid>
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table
|
|
||||||
striped
|
|
||||||
withRowBorders
|
|
||||||
withTableBorder
|
|
||||||
style={{ minWidth: "700px" }}
|
|
||||||
>
|
>
|
||||||
<TableThead>
|
Tambah Baru
|
||||||
<TableTr>
|
</Button>
|
||||||
<TableTh w={250}>Judul</TableTh>
|
</Tooltip>
|
||||||
<TableTh w={250}>Kategori</TableTh>
|
</Group>
|
||||||
<TableTh w={250}>Image</TableTh>
|
|
||||||
<TableTh w={200}>Detail</TableTh>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
</TableTr>
|
<Table highlightOnHover>
|
||||||
</TableThead>
|
<TableThead>
|
||||||
<TableTbody>
|
<TableTr>
|
||||||
{filteredData.map((item) => (
|
<TableTh style={{ width: '30%' }}>Judul</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||||
|
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd style={{ width: '30%' }}>
|
||||||
<Box w={100}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<Text truncate="end" fz={"sm"}>
|
{item.judul}
|
||||||
{item.judul}
|
</Text>
|
||||||
</Text>
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{item.kategoriBerita?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Box
|
||||||
|
w={80}
|
||||||
|
h={80}
|
||||||
|
style={{ borderRadius: 8, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{item.image?.link ? (
|
||||||
|
<Image src={item.image.link} alt="gambar" fit="cover" />
|
||||||
|
) : (
|
||||||
|
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>{item.kategoriBerita?.name}</TableTd>
|
<TableTd style={{ width: '15%' }}>
|
||||||
<TableTd>
|
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button
|
<Button
|
||||||
bg={"green"}
|
variant="light"
|
||||||
|
color="blue"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
router.push(`/admin/desa/berita/list-berita/${item.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconDeviceImacCog size={25} />
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data berita yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Berita;
|
export default Berita;
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
|
||||||
import React from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
|
||||||
|
|
||||||
function DetailFoto() {
|
|
||||||
const fotoState = useProxy(stateGallery.foto)
|
|
||||||
const [modalHapus, setModalHapus] = useState(false);
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
||||||
const params = useParams()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
fotoState.findUnique.load(params?.id as string)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
|
||||||
if (selectedId) {
|
|
||||||
fotoState.delete.byId(selectedId)
|
|
||||||
setModalHapus(false)
|
|
||||||
setSelectedId(null)
|
|
||||||
router.push("/admin/desa/gallery/foto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fotoState.findUnique.data) {
|
|
||||||
return (
|
|
||||||
<Stack py={10}>
|
|
||||||
<Skeleton h={500} />
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
|
||||||
<Stack>
|
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Foto</Text>
|
|
||||||
{fotoState.findUnique.data ? (
|
|
||||||
<Paper key={fotoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
|
||||||
<Text fz={"lg"}>{fotoState.findUnique.data?.name}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Tanggal Foto</Text>
|
|
||||||
<Text fz={"lg"}>{new Date(fotoState.findUnique.data?.createdAt).toDateString()}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: fotoState.findUnique.data?.deskripsi }} />
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
|
||||||
<Image w={{ base: 300, md: 350}} src={fotoState.findUnique.data?.imageGalleryFoto?.link} alt="gambar" />
|
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (fotoState.findUnique.data) {
|
|
||||||
setSelectedId(fotoState.findUnique.data.id);
|
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={fotoState.delete.loading || !fotoState.findUnique.data}
|
|
||||||
color={"red"}
|
|
||||||
>
|
|
||||||
<IconX size={20} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (fotoState.findUnique.data) {
|
|
||||||
router.push(`/admin/desa/gallery/foto/${fotoState.findUnique.data.id}/edit`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!fotoState.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
|
||||||
opened={modalHapus}
|
|
||||||
onClose={() => setModalHapus(false)}
|
|
||||||
onConfirm={handleHapus}
|
|
||||||
text='Apakah anda yakin ingin menghapus berita ini?'
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DetailFoto;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import colors from "@/con/colors";
|
|
||||||
import stateFileStorage from "@/state/state-list-image";
|
import stateFileStorage from "@/state/state-list-image";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
@@ -29,95 +30,128 @@ export default function ListImage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let timeOut: NodeJS.Timer;
|
let timeOut: NodeJS.Timer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p={"lg"}>
|
<Stack p="lg" gap="lg">
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
||||||
<Title order={3}>List Foto</Title>
|
<Title order={2} fw={700}>
|
||||||
|
Galeri Foto
|
||||||
|
</Title>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius={"lg"}
|
radius="xl"
|
||||||
leftSection={<IconSearch />}
|
size="md"
|
||||||
|
placeholder="Cari foto berdasarkan nama..."
|
||||||
|
leftSection={<IconSearch size={18} />}
|
||||||
rightSection={
|
rightSection={
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="transparent"
|
variant="light"
|
||||||
onClick={() => {
|
color="gray"
|
||||||
stateFileStorage.load();
|
radius="xl"
|
||||||
}}
|
onClick={() => stateFileStorage.load()}
|
||||||
>
|
>
|
||||||
<IconX />
|
<IconX size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
}
|
}
|
||||||
placeholder="Pencarian"
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (timeOut) clearTimeout(timeOut);
|
if (timeOut) clearTimeout(timeOut);
|
||||||
timeOut = setTimeout(() => {
|
timeOut = setTimeout(() => {
|
||||||
stateFileStorage.load({ search: e.target.value });
|
stateFileStorage.load({ search: e.target.value });
|
||||||
}, 200);
|
}, 300);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
<SimpleGrid
|
<Paper withBorder radius="lg" p="md" shadow="sm">
|
||||||
cols={{
|
{list && list.length > 0 ? (
|
||||||
base: 3,
|
<SimpleGrid
|
||||||
md: 5,
|
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
|
||||||
lg: 10,
|
spacing="md"
|
||||||
}}
|
verticalSpacing="md"
|
||||||
>
|
>
|
||||||
{list &&
|
{list.map((v, k) => (
|
||||||
list.map((v, k) => {
|
<Card
|
||||||
return (
|
key={k}
|
||||||
<Paper key={k} shadow="sm">
|
withBorder
|
||||||
<Stack pos={"relative"} gap={0} justify="space-between">
|
radius="md"
|
||||||
<motion.div
|
shadow="sm"
|
||||||
onClick={() => {
|
className="hover:shadow-md transition-all duration-200"
|
||||||
// copy to clipboard
|
>
|
||||||
navigator.clipboard.writeText(v.url);
|
<Stack gap="xs">
|
||||||
toast("Berhasil disalin");
|
<motion.div
|
||||||
}}
|
onClick={() => {
|
||||||
whileHover={{ scale: 1.05 }}
|
navigator.clipboard.writeText(v.url);
|
||||||
whileTap={{ scale: 0.8 }}
|
toast("Tautan foto berhasil disalin");
|
||||||
>
|
}}
|
||||||
<Image
|
whileHover={{ scale: 1.05 }}
|
||||||
h={100}
|
whileTap={{ scale: 0.95 }}
|
||||||
src={v.url + "?size=100"}
|
style={{ cursor: "pointer" }}
|
||||||
alt={v.name}
|
>
|
||||||
fit="cover"
|
<Image
|
||||||
loading="lazy"
|
src={`${v.url}?size=200`}
|
||||||
style={{
|
alt={v.name}
|
||||||
objectFit: "cover",
|
radius="md"
|
||||||
objectPosition: "center",
|
h={120}
|
||||||
}}
|
fit="cover"
|
||||||
/>
|
loading="lazy"
|
||||||
</motion.div>
|
/>
|
||||||
<Box p={"md"} h={54}>
|
</motion.div>
|
||||||
<Text lineClamp={2} fz={"xs"}>
|
|
||||||
{v.name}
|
<Box>
|
||||||
</Text>
|
<Text size="sm" fw={500} lineClamp={2}>
|
||||||
</Box>
|
{v.name}
|
||||||
<Group justify="end">
|
</Text>
|
||||||
<IconTrash
|
</Box>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" pt="xs">
|
||||||
|
<Tooltip label="Hapus foto" withArrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
radius="md"
|
||||||
stateFileStorage.del({ name: v.name }).finally(() => {
|
onClick={() => {
|
||||||
toast("Berhasil dihapus");
|
stateFileStorage
|
||||||
});
|
.del({ name: v.name })
|
||||||
}}
|
.finally(() => toast("Foto berhasil dihapus"));
|
||||||
/>
|
}}
|
||||||
</Group>
|
>
|
||||||
</Stack>
|
<IconTrash size={18} />
|
||||||
</Paper>
|
</ActionIcon>
|
||||||
);
|
</Tooltip>
|
||||||
})}
|
</Group>
|
||||||
</SimpleGrid>
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
) : (
|
||||||
|
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||||
|
<Image
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
|
||||||
|
alt="Kosong"
|
||||||
|
w={120}
|
||||||
|
h={120}
|
||||||
|
fit="contain"
|
||||||
|
opacity={0.7}
|
||||||
|
/>
|
||||||
|
<Text c="dimmed" ta="center">
|
||||||
|
Belum ada foto yang tersedia
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
{total && (
|
|
||||||
<Pagination
|
{total && total > 1 && (
|
||||||
total={total}
|
<Flex justify="center">
|
||||||
onChange={(e) => {
|
<Pagination
|
||||||
stateFileStorage.page = e;
|
total={total}
|
||||||
stateFileStorage.load();
|
size="md"
|
||||||
}}
|
radius="md"
|
||||||
/>
|
withEdges
|
||||||
|
onChange={(page) => {
|
||||||
|
stateFileStorage.page = page;
|
||||||
|
stateFileStorage.load();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } 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';
|
||||||
|
import { IconPhoto, IconVideo } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,16 +13,21 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Foto",
|
label: "Foto",
|
||||||
value: "foto",
|
value: "foto",
|
||||||
href: "/admin/desa/gallery/foto"
|
href: "/admin/desa/gallery/foto",
|
||||||
|
icon: <IconPhoto size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola foto-foto galeri desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Video",
|
label: "Video",
|
||||||
value: "video",
|
value: "video",
|
||||||
href: "/admin/desa/gallery/video"
|
href: "/admin/desa/gallery/video",
|
||||||
|
icon: <IconVideo size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola video galeri desa"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -39,24 +45,64 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip
|
||||||
|
key={i}
|
||||||
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
|
>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsGallery;
|
export default LayoutTabsGallery;
|
||||||
|
|||||||
@@ -3,7 +3,16 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
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 { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
@@ -12,9 +21,9 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
||||||
|
|
||||||
function EditVideo() {
|
function EditVideo() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -30,7 +39,7 @@ function EditVideo() {
|
|||||||
const data = await videoState.update.load(id);
|
const data = await videoState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || '',
|
||||||
linkVideo: data.linkVideo || '',
|
linkVideo: data.linkVideo || '',
|
||||||
});
|
});
|
||||||
@@ -66,27 +75,36 @@ function EditVideo() {
|
|||||||
console.error('Error updating video:', error);
|
console.error('Error updating video:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui video');
|
toast.error('Terjadi kesalahan saat memperbarui video');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Button>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Box>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Stack gap={"xs"}>
|
Edit Video
|
||||||
<Title order={4}>Edit Video</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
|
label="Judul Video"
|
||||||
placeholder='Masukkan judul video'
|
placeholder="Masukkan judul video"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@@ -94,36 +112,46 @@ function EditVideo() {
|
|||||||
label="Link Video YouTube"
|
label="Link Video YouTube"
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
value={formData.linkVideo}
|
value={formData.linkVideo}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
|
||||||
setFormData({ ...formData, linkVideo: e.currentTarget.value });
|
|
||||||
}}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{embedLink && (
|
{embedLink && (
|
||||||
<iframe
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
className="rounded"
|
<iframe
|
||||||
width="100%"
|
className="rounded"
|
||||||
height="200"
|
width="100%"
|
||||||
src={embedLink}
|
height="220"
|
||||||
title="Preview Video"
|
src={embedLink}
|
||||||
allowFullScreen
|
title="Preview Video"
|
||||||
></iframe>
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
|
<Title order={6} fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Video
|
||||||
|
</Title>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
setFormData({ ...formData, deskripsi: val });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
<Group justify="right">
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -2,107 +2,145 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
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 { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } 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 DetailVideo() {
|
function DetailVideo() {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
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(() => {
|
||||||
videoState.findUnique.load(params?.id as string)
|
videoState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
videoState.delete.byId(selectedId)
|
videoState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/gallery/video")
|
router.push("/admin/desa/gallery/video");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!videoState.findUnique.data) {
|
if (!videoState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = videoState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Video</Text>
|
Kembali
|
||||||
{videoState.findUnique.data ? (
|
</Button>
|
||||||
<Paper key={videoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
{/* Detail Video */}
|
||||||
<Box>
|
<Paper
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
withBorder
|
||||||
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text>
|
w={{ base: "100%", md: "50%" }}
|
||||||
</Box>
|
bg={colors['white-1']}
|
||||||
<Box>
|
p="lg"
|
||||||
<Text fw={"bold"} fz={"lg"}>Video</Text>
|
radius="md"
|
||||||
<Box component="iframe"
|
shadow="sm"
|
||||||
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)}
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Video
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Video</Text>
|
||||||
|
{data?.linkVideo ? (
|
||||||
|
<Box
|
||||||
|
component="iframe"
|
||||||
|
src={convertToEmbedUrl(data.linkVideo)}
|
||||||
width="100%"
|
width="100%"
|
||||||
height={300}
|
height={300}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
style={{ borderRadius: 8 }}
|
style={{ borderRadius: 8 }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada video</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
</Box>
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Tanggal Video</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.createdAt ? new Date(data.createdAt).toDateString() : '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text>
|
{data?.deskripsi ? (
|
||||||
</Box>
|
<Text
|
||||||
<Box>
|
fz="md"
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
c="dimmed"
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: videoState.findUnique.data?.deskripsi }} />
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
</Box>
|
/>
|
||||||
<Flex gap={"xs"} mt={10}>
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Tombol Aksi */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Video" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (videoState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(videoState.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={videoState.delete.loading || !videoState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Video" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (videoState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/gallery/video/${videoState.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/gallery/video/${data.id}/edit`)
|
||||||
}
|
}
|
||||||
}}
|
variant="light"
|
||||||
disabled={!videoState.findUnique.data}
|
radius="md"
|
||||||
color={"green"}
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -111,17 +149,16 @@ function DetailVideo() {
|
|||||||
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 video ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function convertToEmbedUrl(youtubeUrl: string): string {
|
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL(youtubeUrl);
|
const url = new URL(youtubeUrl);
|
||||||
const videoId = url.searchParams.get("v");
|
const videoId = url.searchParams.get("v");
|
||||||
if (!videoId) return youtubeUrl;
|
if (!videoId) return youtubeUrl;
|
||||||
|
|
||||||
return `https://www.youtube.com/embed/${videoId}`;
|
return `https://www.youtube.com/embed/${videoId}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error converting YouTube URL to embed:", err);
|
console.error("Error converting YouTube URL to embed:", err);
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
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 { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -10,77 +20,104 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateVideo() {
|
function CreateVideo() {
|
||||||
const videoState = useProxy(stateGallery.video)
|
const videoState = useProxy(stateGallery.video);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [link, setLink] = useState("");
|
const [link, setLink] = useState('');
|
||||||
const embedLink = convertYoutubeUrlToEmbed(link);
|
const embedLink = convertYoutubeUrlToEmbed(link);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
videoState.create.form = {
|
videoState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
linkVideo: "",
|
linkVideo: '',
|
||||||
};
|
};
|
||||||
|
setLink('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!embedLink) {
|
if (!embedLink) {
|
||||||
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga)
|
videoState.create.form.linkVideo = embedLink;
|
||||||
await videoState.create.create();
|
await videoState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/gallery/video");
|
router.push('/admin/desa/gallery/video');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header Back Button + Title */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Video
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Video</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
|
label="Judul Video"
|
||||||
placeholder='Masukkan judul video'
|
placeholder="Masukkan judul video"
|
||||||
value={videoState.create.form.name}
|
value={videoState.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => {
|
||||||
videoState.create.form.name = val.target.value;
|
videoState.create.form.name = e.currentTarget.value;
|
||||||
}}
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<TextInput
|
|
||||||
label="Link Video YouTube"
|
|
||||||
placeholder="https://www.youtube.com/watch?v=abc123"
|
|
||||||
value={link}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLink(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{embedLink && (
|
{/* Link YouTube */}
|
||||||
<iframe
|
<TextInput
|
||||||
style={{ borderRadius: 10, width: "100%", height: 400 }}
|
label="Link Video YouTube"
|
||||||
src={embedLink}
|
placeholder="https://www.youtube.com/watch?v=abc123"
|
||||||
title="Preview Video"
|
value={link}
|
||||||
allowFullScreen
|
onChange={(e) => setLink(e.currentTarget.value)}
|
||||||
></iframe>
|
required
|
||||||
)}
|
/>
|
||||||
</Stack>
|
|
||||||
</Box>
|
{/* Preview Video */}
|
||||||
|
{embedLink && (
|
||||||
|
<Box mt="sm">
|
||||||
|
<iframe
|
||||||
|
style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
width: '100%',
|
||||||
|
height: 400,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
src={embedLink}
|
||||||
|
title="Preview Video"
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Video
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={videoState.create.form.deskripsi}
|
value={videoState.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -88,8 +125,21 @@ function CreateVideo() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
{/* Button Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
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 JudulList from '../../../_com/judulList';
|
|
||||||
import stateGallery from '../../../_state/desa/gallery';
|
import stateGallery from '../../../_state/desa/gallery';
|
||||||
|
|
||||||
function Video() {
|
function Video() {
|
||||||
@@ -15,8 +32,8 @@ function Video() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Video'
|
||||||
placeholder='pencarian'
|
placeholder='Cari judul atau deskripsi video...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -29,6 +46,7 @@ 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 {
|
const {
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
@@ -41,72 +59,104 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
load(page, 10, search)
|
load(page, 10, search)
|
||||||
}, [page, search])
|
}, [page, search])
|
||||||
|
|
||||||
const filteredData = (data || [])
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Box>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Video'
|
<Title order={4}>Daftar Video</Title>
|
||||||
href='/admin/desa/gallery/video/create'
|
<Tooltip label="Tambah Video Baru" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Judul Video</TableTh>
|
onClick={() => router.push('/admin/desa/gallery/video/create')}
|
||||||
<TableTh>Tanggal Video</TableTh>
|
>
|
||||||
<TableTh>Deskripsi Video</TableTh>
|
Tambah Baru
|
||||||
<TableTh>Detail</TableTh>
|
</Button>
|
||||||
</TableTr>
|
</Tooltip>
|
||||||
</TableThead>
|
</Group>
|
||||||
<TableTbody>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
{filteredData.map((item) => (
|
<Table highlightOnHover>
|
||||||
<TableTr key={item.id}>
|
<TableThead>
|
||||||
<TableTd>
|
<TableTr>
|
||||||
<Box w={200}>
|
<TableTh style={{ width: '25%' }}>Judul Video</TableTh>
|
||||||
<Text lineClamp={1}>{item.name}</Text>
|
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||||
</Box>
|
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
|
||||||
</TableTd>
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
|
|
||||||
<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>
|
|
||||||
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
|
|
||||||
<IconDeviceImac size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '30%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">Tidak ada video yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
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, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
@@ -49,52 +49,74 @@ function EditPelayananPendudukNonPermanent() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="xs">
|
||||||
<Box>
|
<Group mb="md">
|
||||||
<Button
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
variant={'subtle'}
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
onClick={() => router.back()}
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
>
|
</Button>
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
</Tooltip>
|
||||||
</Button>
|
<Title order={4} ml="sm" c="dark">
|
||||||
</Box>
|
Edit Pelayanan Penduduk Non Permanent
|
||||||
<Box>
|
</Title>
|
||||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
</Group>
|
||||||
<Stack gap={'xs'}>
|
|
||||||
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
|
<Paper
|
||||||
<Text fw={"bold"}>Judul</Text>
|
w={{ base: "100%", md: "50%" }}
|
||||||
<TextInput
|
bg={colors['white-1']}
|
||||||
value={formData.name}
|
p="md"
|
||||||
onChange={(val) => {
|
radius="md"
|
||||||
setFormData({
|
shadow="sm"
|
||||||
...formData,
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
name: val.target.value,
|
>
|
||||||
})
|
<Stack gap="xs">
|
||||||
}}
|
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
|
||||||
/>
|
|
||||||
<Text fw={"bold"}>Deskripsi</Text>
|
{/* Nama Field */}
|
||||||
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, name: e.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Posisi Field */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(htmlContent) => {
|
||||||
setFormData({
|
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
|
||||||
...formData,
|
|
||||||
deskripsi: val,
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
<Group>
|
|
||||||
<Button
|
{/* Submit Button */}
|
||||||
bg={colors['blue-button']}
|
<Group>
|
||||||
onClick={handleSubmit}
|
<Button
|
||||||
loading={statePendudukNonPermanent.update.loading}
|
bg={colors['blue-button']}
|
||||||
>
|
onClick={handleSubmit}
|
||||||
Submit
|
loading={statePendudukNonPermanent.update.loading}
|
||||||
</Button>
|
disabled={!formData.name}
|
||||||
</Group>
|
>
|
||||||
</Stack>
|
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
</Paper>
|
</Button>
|
||||||
</Box>
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
disabled={statePendudukNonPermanent.update.loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,51 +1,103 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } 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 stateLayananDesa from '../../../_state/desa/layananDesa';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function SuratKeterangan() {
|
function PelayananPendudukNonPermanent() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pelayananPendudukNonPermanen = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
|
const pelayananPendudukNonPermanen = useProxy(
|
||||||
|
stateLayananDesa.pelayananPendudukNonPermanen
|
||||||
|
);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pelayananPendudukNonPermanen.findById.load('1')
|
pelayananPendudukNonPermanen.findById.load('1');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!pelayananPendudukNonPermanen.findById.data) {
|
if (!pelayananPendudukNonPermanen.findById.data) {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack align="center" justify="center" py="xl">
|
||||||
<Skeleton radius={10} h={800} />
|
<Skeleton radius="md" height={800} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = pelayananPendudukNonPermanen.findById.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Stack gap="md">
|
||||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
{/* Header */}
|
||||||
<Box py={15}>
|
<Grid align="center">
|
||||||
<Stack gap={"xs"}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Grid>
|
<Title order={3} c={colors['blue-button']}>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
Preview Pelayanan Penduduk Non Permanen
|
||||||
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit')}>
|
<Tooltip label="Edit Data Pelayanan" withArrow>
|
||||||
<IconEdit size={16} />
|
<Button
|
||||||
</Button>
|
c="green"
|
||||||
</GridCol>
|
variant="light"
|
||||||
</Grid>
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
</Stack>
|
radius="md"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
|
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||||
|
<Center>
|
||||||
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: '1.2rem', md: '1.8rem' }}
|
||||||
|
fw="bold"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
|
||||||
|
<Box mt="lg">
|
||||||
|
<Text
|
||||||
|
py={10}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPendudukNonPermanen.findById.data.name}</Text>
|
|
||||||
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{ __html: pelayananPendudukNonPermanen.findById.data.deskripsi }} />
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Paper>
|
</Stack>
|
||||||
</Box>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuratKeterangan;
|
export default PelayananPendudukNonPermanent;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
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, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
@@ -14,6 +14,7 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
|
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: statePerizinanBerusaha.findById.data?.name || '',
|
name: statePerizinanBerusaha.findById.data?.name || '',
|
||||||
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
|
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
|
||||||
@@ -50,64 +51,81 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
}
|
}
|
||||||
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
|
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap={'xs'}>
|
<Stack gap="xs">
|
||||||
<Box>
|
{/* Header Section */}
|
||||||
<Button
|
<Group mb="md">
|
||||||
variant={'subtle'}
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
onClick={() => router.back()}
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
</Button>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</Box>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Box>
|
Edit Pelayanan Perizinan Berusaha
|
||||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
</Title>
|
||||||
<Stack gap={'xs'}>
|
</Group>
|
||||||
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
|
|
||||||
<Text fw={"bold"}>Judul</Text>
|
{/* Form Section */}
|
||||||
<TextInput
|
<Paper
|
||||||
value={formData.name}
|
w={{ base: "100%", md: "50%" }}
|
||||||
onChange={(val) => {
|
bg={colors['white-1']}
|
||||||
setFormData({
|
p="md"
|
||||||
...formData,
|
radius="md"
|
||||||
name: val.target.value,
|
shadow="sm"
|
||||||
})
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
}}
|
>
|
||||||
/>
|
<Stack gap="xs">
|
||||||
<Text fw={"bold"}>Link</Text>
|
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
|
||||||
<TextInput
|
|
||||||
value={formData.link}
|
{/* Nama Field */}
|
||||||
onChange={(val) => {
|
<TextInput
|
||||||
setFormData({
|
label="Judul"
|
||||||
...formData,
|
placeholder="Masukkan judul"
|
||||||
link: val.target.value,
|
value={formData.name}
|
||||||
})
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
}}
|
required
|
||||||
/>
|
/>
|
||||||
<Text fw={"bold"}>Deskripsi</Text>
|
|
||||||
|
{/* Link Field */}
|
||||||
|
<TextInput
|
||||||
|
label="Link"
|
||||||
|
placeholder="Masukkan link terkait"
|
||||||
|
value={formData.link}
|
||||||
|
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi Field */}
|
||||||
|
<Box>
|
||||||
|
<Title order={6}>Deskripsi</Title>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
deskripsi: val,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
<Group>
|
|
||||||
<Button
|
{/* Action Buttons */}
|
||||||
bg={colors['blue-button']}
|
<Group>
|
||||||
onClick={handleSubmit}
|
<Button
|
||||||
loading={statePerizinanBerusaha.update.loading}
|
bg={colors['blue-button']}
|
||||||
>
|
onClick={handleSubmit}
|
||||||
Submit
|
loading={statePerizinanBerusaha.update.loading}
|
||||||
</Button>
|
disabled={!formData.name}
|
||||||
</Group>
|
>
|
||||||
</Stack>
|
{statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||||
</Paper>
|
</Button>
|
||||||
</Box>
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
disabled={statePerizinanBerusaha.update.loading}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Grid, GridCol, Group, Paper, Skeleton, Stack, Stepper, StepperCompleted, StepperStep, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Stepper,
|
||||||
|
StepperCompleted,
|
||||||
|
StepperStep,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -9,88 +26,158 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
|
||||||
function PerizinanBerusaha() {
|
function PerizinanBerusaha() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pelayananPerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
|
const pelayananPerizinanBerusaha = useProxy(
|
||||||
|
stateLayananDesa.pelayananPerizinanBerusaha
|
||||||
|
);
|
||||||
const [active, setActive] = useState(1);
|
const [active, setActive] = useState(1);
|
||||||
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
|
const nextStep = () =>
|
||||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
setActive((current) => (current < 6 ? current + 1 : current));
|
||||||
|
const prevStep = () =>
|
||||||
|
setActive((current) => (current > 0 ? current - 1 : current));
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pelayananPerizinanBerusaha.findById.load('1')
|
pelayananPerizinanBerusaha.findById.load('1');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if(!pelayananPerizinanBerusaha.findById.data) {
|
if (!pelayananPerizinanBerusaha.findById.data) {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack align="center" justify="center" py="xl">
|
||||||
<Skeleton radius={10} h={800} />
|
<Skeleton radius="md" height={800} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = pelayananPerizinanBerusaha.findById.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Stack gap="md">
|
||||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
{/* Header */}
|
||||||
<Box py={15}>
|
<Grid align="center">
|
||||||
<Stack gap={"xs"}>
|
<GridCol span={{ base: 12, md: 11 }}>
|
||||||
<Grid>
|
<Title order={3} c={colors['blue-button']}>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
Preview Pelayanan Perizinan Berusaha
|
||||||
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha/edit')}>
|
<Tooltip label="Edit Data Perizinan" withArrow>
|
||||||
<IconEdit size={16} />
|
<Button
|
||||||
</Button>
|
c="green"
|
||||||
</GridCol>
|
variant="light"
|
||||||
</Grid>
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
</Stack>
|
radius="md"
|
||||||
</Box>
|
onClick={() =>
|
||||||
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPerizinanBerusaha.findById.data.name}</Text>
|
router.push(
|
||||||
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{__html: pelayananPerizinanBerusaha.findById.data.deskripsi}} />
|
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
|
||||||
<Text py={10} fz={{ base: "sm", md: 'h3' }}>Proses pendaftaran NIB melalui OSS mencakup beberapa langkah umum, seperti:</Text>
|
)
|
||||||
<Box p={"xl"} w={{ base: "100%", md: "100%" }} >
|
|
||||||
<Stepper active={active} onStepClick={setActive} orientation="vertical"
|
|
||||||
styles={{
|
|
||||||
separator: {
|
|
||||||
marginLeft: 25
|
|
||||||
},
|
|
||||||
|
|
||||||
step: {
|
|
||||||
padding: '12px 0'
|
|
||||||
}
|
}
|
||||||
}}>
|
>
|
||||||
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
Edit
|
||||||
Pendaftaran akun pada portal OSS
|
</Button>
|
||||||
</StepperStep>
|
</Tooltip>
|
||||||
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
|
</GridCol>
|
||||||
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
|
</Grid>
|
||||||
</StepperStep>
|
|
||||||
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI ">
|
|
||||||
Memilih KBLI dengan jenis usaha yang akan didaftarkan
|
|
||||||
</StepperStep>
|
|
||||||
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
|
|
||||||
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
|
|
||||||
</StepperStep>
|
|
||||||
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
|
|
||||||
Proses verifikasi dan persetujuan oleh instansi terkait
|
|
||||||
</StepperStep>
|
|
||||||
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
|
|
||||||
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
|
|
||||||
</StepperStep>
|
|
||||||
<StepperCompleted >
|
|
||||||
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
|
|
||||||
</StepperCompleted>
|
|
||||||
</Stepper>
|
|
||||||
|
|
||||||
<Group justify="center" mt="xl">
|
{/* Content */}
|
||||||
<Button variant="default" onClick={prevStep}>Back</Button>
|
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||||
<Button onClick={nextStep}>Next step</Button>
|
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||||
</Group>
|
<Center>
|
||||||
<Text py={35} ta={"justify"} fz={{ base: "sm", md: 'h3' }}>Penting untuk diingat bahwa prosedur dan persyaratan dapat berubah
|
<Text
|
||||||
seiring waktu. Untuk informasi yang lebih akurat dan terkini, saya sarankan untuk mengunjungi situs
|
ta="center"
|
||||||
resmi OSS <a href={pelayananPerizinanBerusaha.findById.data.link}>{pelayananPerizinanBerusaha.findById.data.link}</a> atau menghubungi instansi terkait di pemerintah Indonesia yang bertanggung jawab atas urusan perizinan usaha.</Text>
|
fz={{ base: '1.2rem', md: '1.8rem' }}
|
||||||
|
fw="bold"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<Divider my="md" color={colors['blue-button']} />
|
||||||
|
|
||||||
|
<Box mt="lg">
|
||||||
|
<Text
|
||||||
|
py={10}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
py={10}
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
fw="bold"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
|
||||||
|
umum:
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box p="xl" w="100%">
|
||||||
|
<Stepper
|
||||||
|
active={active}
|
||||||
|
onStepClick={setActive}
|
||||||
|
orientation="vertical"
|
||||||
|
styles={{
|
||||||
|
separator: { marginLeft: 25 },
|
||||||
|
step: { padding: '12px 0' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
|
||||||
|
Pendaftaran akun pada portal OSS
|
||||||
|
</StepperStep>
|
||||||
|
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
|
||||||
|
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
|
||||||
|
</StepperStep>
|
||||||
|
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
|
||||||
|
Memilih KBLI dengan jenis usaha yang akan didaftarkan
|
||||||
|
</StepperStep>
|
||||||
|
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
|
||||||
|
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
|
||||||
|
</StepperStep>
|
||||||
|
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
|
||||||
|
Proses verifikasi dan persetujuan oleh instansi terkait
|
||||||
|
</StepperStep>
|
||||||
|
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
|
||||||
|
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
|
||||||
|
</StepperStep>
|
||||||
|
<StepperCompleted>
|
||||||
|
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
|
||||||
|
</StepperCompleted>
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
<Group justify="center" mt="xl">
|
||||||
|
<Button variant="default" onClick={prevStep}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button onClick={nextStep}>Next step</Button>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
py={35}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: '1rem', md: '1.2rem' }}
|
||||||
|
>
|
||||||
|
Penting untuk diingat bahwa prosedur dan persyaratan dapat
|
||||||
|
berubah seiring waktu. Untuk informasi yang lebih akurat dan
|
||||||
|
terkini, silakan kunjungi situs resmi OSS{' '}
|
||||||
|
<a
|
||||||
|
href={data.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: colors['blue-button'] }}
|
||||||
|
>
|
||||||
|
{data.link}
|
||||||
|
</a>{' '}
|
||||||
|
atau hubungi instansi terkait di pemerintah Indonesia yang
|
||||||
|
bertanggung jawab atas urusan perizinan usaha.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Paper>
|
</Stack>
|
||||||
</Box>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,18 @@ 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, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -13,9 +24,10 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditSuratKeterangan() {
|
function EditSuratKeterangan() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
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 [previewImage2, setPreviewImage2] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
@@ -25,39 +37,32 @@ function EditSuratKeterangan() {
|
|||||||
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,
|
image2Id: stateSurat.edit.form.image2Id,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSurat = async () => {
|
const loadSurat = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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 || "",
|
image2Id: data.image2Id || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.image?.link) {
|
setPreviewImage(data.image?.link || null);
|
||||||
setPreviewImage(data.image.link);
|
setPreviewImage2(data.image2?.link || null);
|
||||||
} else {
|
|
||||||
setPreviewImage(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.image2?.link) {
|
|
||||||
setPreviewImage2(data.image2.link);
|
|
||||||
} else {
|
|
||||||
setPreviewImage2(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading surat:", error);
|
console.error('Error loading surat:', error);
|
||||||
toast.error("Gagal memuat data surat");
|
toast.error('Gagal memuat data surat');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSurat();
|
loadSurat();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
@@ -65,171 +70,199 @@ function EditSuratKeterangan() {
|
|||||||
try {
|
try {
|
||||||
stateSurat.edit.form = {
|
stateSurat.edit.form = {
|
||||||
...stateSurat.edit.form,
|
...stateSurat.edit.form,
|
||||||
name: formData.name,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
};
|
||||||
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 });
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
stateSurat.edit.form.imageId = uploaded.id;
|
stateSurat.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file2) {
|
if (file2) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
|
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
stateSurat.edit.form.image2Id = uploaded.id;
|
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');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating surat:", error);
|
console.error('Error updating surat:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui surat");
|
toast.error('Terjadi kesalahan saat memperbarui surat');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Edit Surat Keterangan</Title>
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Surat Keterangan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Surat Keterangan"
|
||||||
|
placeholder="Masukkan nama surat keterangan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
required
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
|
||||||
placeholder="masukkan nama surat keterangan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
|
||||||
setFormData({ ...formData, deskripsi: htmlContent });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
|
||||||
<Box >
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
|
||||||
const file = files[0]; // Hanya ambil file pertama
|
|
||||||
if (file) {
|
|
||||||
setFile(file);
|
|
||||||
setPreviewImage(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>
|
{/* Upload Gambar 1 */}
|
||||||
<Text size="xl" inline>
|
<Box>
|
||||||
Drag images here or click to select files
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
</Text>
|
Gambar 1
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
</Text>
|
||||||
Attach as many files as you like, each file should not exceed 5mb
|
<Dropzone
|
||||||
</Text>
|
onDrop={(files) => {
|
||||||
</div>
|
const selectedFile = files[0];
|
||||||
</Group>
|
if (selectedFile) {
|
||||||
</Dropzone>
|
setFile(selectedFile);
|
||||||
{previewImage && (
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar wajib
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar 1"
|
||||||
width={280}
|
radius="md"
|
||||||
height={180}
|
style={{
|
||||||
fit="cover"
|
maxHeight: 220,
|
||||||
radius="sm"
|
objectFit: 'contain',
|
||||||
mt="md"
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
</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>
|
{/* Upload Gambar 2 */}
|
||||||
<Text size="xl" inline>
|
<Box>
|
||||||
Drag images here or click to select files
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
</Text>
|
Gambar 2
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
</Text>
|
||||||
Attach as many files as you like, each file should not exceed 5mb
|
<Dropzone
|
||||||
</Text>
|
onDrop={(files) => {
|
||||||
</div>
|
const selectedFile = files[0];
|
||||||
</Group>
|
if (selectedFile) {
|
||||||
</Dropzone>
|
setFile2(selectedFile);
|
||||||
{previewImage2 && (
|
setPreviewImage2(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar wajib
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage2 && (
|
||||||
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage2}
|
src={previewImage2}
|
||||||
alt="Preview"
|
alt="Preview Gambar 2"
|
||||||
width={280}
|
radius="md"
|
||||||
height={180}
|
style={{
|
||||||
fit="cover"
|
maxHeight: 220,
|
||||||
radius="sm"
|
objectFit: 'contain',
|
||||||
mt="md"
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,100 +2,177 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
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, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } 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 DetailSuratKeterangan() {
|
function DetailSuratKeterangan() {
|
||||||
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
|
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
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(() => {
|
||||||
suratKeteranganState.findUnique.load(params?.id as string)
|
suratKeteranganState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
suratKeteranganState.delete.byId(selectedId)
|
suratKeteranganState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!suratKeteranganState.findUnique.data) {
|
if (!suratKeteranganState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
{Array.from({ length: 10 }).map((_, k) => (
|
<Skeleton height={500} radius="md" />
|
||||||
<Skeleton key={k} h={40} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = suratKeteranganState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Surat Keterangan</Text>
|
Kembali
|
||||||
{suratKeteranganState.findUnique.data ? (
|
</Button>
|
||||||
<Paper key={suratKeteranganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Box>
|
withBorder
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama</Text>
|
w={{ base: '100%', md: '60%' }}
|
||||||
<Text fz={"lg"}>{suratKeteranganState.findUnique.data?.name}</Text>
|
bg={colors['white-1']}
|
||||||
</Box>
|
p="lg"
|
||||||
<Box>
|
radius="md"
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
shadow="sm"
|
||||||
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: suratKeteranganState.findUnique.data?.deskripsi }}></Text>
|
>
|
||||||
</Box>
|
<Stack gap="md">
|
||||||
<Box>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
Detail Surat Keterangan
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" />
|
</Text>
|
||||||
</Box>
|
|
||||||
<Box>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
<Stack gap="sm">
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image2?.link} alt="gambar" />
|
<Box>
|
||||||
</Box>
|
<Text fz="lg" fw="bold">
|
||||||
<Flex gap={"xs"} mt={10}>
|
Nama
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data?.deskripsi || '-',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Gambar
|
||||||
|
</Text>
|
||||||
|
{data?.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt="gambar"
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Gambar 2
|
||||||
|
</Text>
|
||||||
|
{data?.image2?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image2.link}
|
||||||
|
alt="gambar"
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Surat" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (suratKeteranganState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(suratKeteranganState.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={suratKeteranganState.delete.loading || !suratKeteranganState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
disabled={suratKeteranganState.delete.loading}
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Surat" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (suratKeteranganState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${suratKeteranganState.findUnique.data.id}/edit`);
|
router.push(
|
||||||
}
|
`/admin/desa/layanan/pelayanan_surat_keterangan/${data.id}/edit`
|
||||||
}}
|
)
|
||||||
disabled={!suratKeteranganState.findUnique.data}
|
}
|
||||||
color={"green"}
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -104,7 +181,7 @@ function DetailSuratKeterangan() {
|
|||||||
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 surat keterangan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
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, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -12,25 +24,25 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function CreateSuratKeterangan() {
|
function CreateSuratKeterangan() {
|
||||||
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
|
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
|
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateSurat.create.form = {
|
stateSurat.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
image2Id: ""
|
image2Id: '',
|
||||||
}
|
};
|
||||||
setPreviewImage(null)
|
setPreviewImage(null);
|
||||||
setPreviewImage2(null)
|
setPreviewImage2(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!previewImage) {
|
if (!previewImage) {
|
||||||
return toast.warn("Pilih file gambar utama terlebih dahulu");
|
return toast.warn('Pilih file gambar utama terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -42,11 +54,10 @@ function CreateSuratKeterangan() {
|
|||||||
|
|
||||||
const uploadedImage1 = res1.data?.data;
|
const uploadedImage1 = res1.data?.data;
|
||||||
if (!uploadedImage1?.id) {
|
if (!uploadedImage1?.id) {
|
||||||
return toast.error("Gagal upload gambar utama");
|
return toast.error('Gagal upload gambar utama');
|
||||||
}
|
}
|
||||||
|
|
||||||
let uploadedImage2 = null;
|
let uploadedImage2 = null;
|
||||||
// Upload gambar kedua jika ada
|
|
||||||
if (previewImage2) {
|
if (previewImage2) {
|
||||||
const res2 = await ApiFetch.api.fileStorage.create.post({
|
const res2 = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: previewImage2.file,
|
file: previewImage2.file,
|
||||||
@@ -55,44 +66,58 @@ function CreateSuratKeterangan() {
|
|||||||
uploadedImage2 = res2.data?.data;
|
uploadedImage2 = res2.data?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set form data
|
|
||||||
stateSurat.create.form.imageId = uploadedImage1.id;
|
stateSurat.create.form.imageId = uploadedImage1.id;
|
||||||
if (uploadedImage2?.id) {
|
if (uploadedImage2?.id) {
|
||||||
stateSurat.create.form.image2Id = uploadedImage2.id;
|
stateSurat.create.form.image2Id = uploadedImage2.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the record
|
|
||||||
await stateSurat.create.create();
|
await stateSurat.create.create();
|
||||||
|
|
||||||
// Reset form dan redirect
|
|
||||||
resetForm();
|
resetForm();
|
||||||
toast.success("Data surat keterangan berhasil ditambahkan");
|
toast.success('Data surat keterangan berhasil ditambahkan');
|
||||||
router.push("/admin/desa/layanan/pelayanan_surat_keterangan");
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating surat keterangan:", error);
|
console.error('Error creating surat keterangan:', error);
|
||||||
toast.error("Terjadi kesalahan saat menambahkan surat keterangan");
|
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Create Surat Keterangan</Title>
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Surat Keterangan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Nama Surat */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateSurat.create.form.name}
|
value={stateSurat.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
|
||||||
stateSurat.create.form.name = val.target.value;
|
label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
|
||||||
}}
|
placeholder="Masukkan nama surat keterangan"
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
required
|
||||||
placeholder="masukkan nama surat keterangan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Konten */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={stateSurat.create.form.deskripsi}
|
value={stateSurat.create.form.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -100,106 +125,124 @@ function CreateSuratKeterangan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Gambar Utama */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Utama</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Utama
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setPreviewImage({
|
setPreviewImage({
|
||||||
file,
|
file,
|
||||||
preview: URL.createObjectURL(file)
|
preview: URL.createObjectURL(file),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{
|
accept={{ 'image/*': [] }}
|
||||||
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
radius="md"
|
||||||
}}
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</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>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Image
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
src={previewImage.preview}
|
<Image
|
||||||
alt="Preview Gambar Utama"
|
src={previewImage.preview}
|
||||||
width={280}
|
alt="Preview Gambar Utama"
|
||||||
height={180}
|
radius="md"
|
||||||
fit="cover"
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
radius="sm"
|
/>
|
||||||
mt="md"
|
</Box>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt="lg">
|
{/* Gambar Tambahan */}
|
||||||
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Tambahan (Opsional)</Text>
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Tambahan (Opsional)
|
||||||
|
</Text>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setPreviewImage2({
|
setPreviewImage2({
|
||||||
file,
|
file,
|
||||||
preview: URL.createObjectURL(file)
|
preview: URL.createObjectURL(file),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{
|
accept={{ 'image/*': [] }}
|
||||||
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
|
radius="md"
|
||||||
}}
|
p="xl"
|
||||||
>
|
>
|
||||||
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Accept>
|
<Dropzone.Accept>
|
||||||
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Accept>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Reject>
|
||||||
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Reject>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</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>
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage2 ? (
|
{previewImage2 ? (
|
||||||
<Image
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
src={previewImage2.preview}
|
<Image
|
||||||
alt="Preview Gambar Tambahan"
|
src={previewImage2.preview}
|
||||||
width={280}
|
alt="Preview Gambar Tambahan"
|
||||||
height={180}
|
radius="md"
|
||||||
fit="cover"
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
radius="sm"
|
/>
|
||||||
mt="md"
|
</Box>
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Text size="sm" c="dimmed" mt="sm">
|
<Text size="sm" c="dimmed" mt="sm" ta="center">
|
||||||
Kosongkan jika tidak ada gambar tambahan
|
Kosongkan jika tidak ada gambar tambahan
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
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';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function SuratKeterangan() {
|
function SuratKeterangan() {
|
||||||
@@ -16,7 +33,7 @@ function SuratKeterangan() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Pelayanan Surat Keterangan'
|
title='Pelayanan Surat Keterangan'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama atau deskripsi...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,8 +44,8 @@ function SuratKeterangan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListSuratKeterangan({ search }: { search: string }) {
|
function ListSuratKeterangan({ search }: { search: string }) {
|
||||||
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
|
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -39,102 +56,111 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
} = suratKeteranganState.findMany;
|
} = suratKeteranganState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
|
const filteredData = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
const keyword = search.toLowerCase();
|
||||||
|
return data.filter(item =>
|
||||||
|
item.name?.toLowerCase().includes(keyword) ||
|
||||||
|
item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
}, [data, search]);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={600} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
if (!data) return [];
|
|
||||||
return data.filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name?.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}, [data, search]);
|
|
||||||
|
|
||||||
// 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'}>
|
|
||||||
<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 withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Surat Keterangan'
|
<Title order={4}>List Surat Keterangan</Title>
|
||||||
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
|
<Tooltip label="Tambah Surat Keterangan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Deskripsi</TableTh>
|
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
|
||||||
<TableTh>Detail</TableTh>
|
}
|
||||||
</TableTr>
|
>
|
||||||
</TableThead>
|
Tambah Baru
|
||||||
<TableTbody>
|
</Button>
|
||||||
{filteredData.map((item) => (
|
</Tooltip>
|
||||||
<TableTr key={item.id}>
|
</Group>
|
||||||
<TableTd>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Box w={200}>
|
<Table highlightOnHover>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
<TableThead>
|
||||||
</Box>
|
<TableTr>
|
||||||
</TableTd>
|
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||||
<TableTd>
|
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
|
||||||
<Box w={300}>
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
</TableTr>
|
||||||
</Box>
|
</TableThead>
|
||||||
</TableTd>
|
<TableTbody>
|
||||||
<TableTd>
|
{filteredData.length > 0 ? (
|
||||||
<Text>
|
filteredData.map((item) => (
|
||||||
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
|
<TableTr key={item.id}>
|
||||||
<IconDeviceImac size={20} />
|
<TableTd style={{ width: '30%' }}>
|
||||||
</Button>
|
<Box w={200}>
|
||||||
</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</TableTd>
|
{item.name}
|
||||||
</TableTr>
|
</Text>
|
||||||
))}
|
</Box>
|
||||||
</TableTbody>
|
</TableTd>
|
||||||
</Table>
|
<TableTd style={{ width: '45%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10, search);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,22 +2,34 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
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,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
|
|
||||||
function EditPelayananTelunjukSakti() {
|
function EditPelayananTelunjukSakti() {
|
||||||
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: stateTelunjukDesa.edit.form.name,
|
name: stateTelunjukDesa.edit.form.name,
|
||||||
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
|
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
|
||||||
link: stateTelunjukDesa.edit.form.link,
|
link: stateTelunjukDesa.edit.form.link,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPelayananTelunjukSakti = async () => {
|
const loadPelayananTelunjukSakti = async () => {
|
||||||
@@ -27,14 +39,14 @@ function EditPelayananTelunjukSakti() {
|
|||||||
const data = await stateTelunjukDesa.edit.load(id);
|
const data = await stateTelunjukDesa.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name,
|
name: data.name || '',
|
||||||
deskripsi: data.deskripsi,
|
deskripsi: data.deskripsi || '',
|
||||||
link: data.link,
|
link: data.link || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pelayanan telunjuk sakti:", error);
|
console.error('Error loading pelayanan telunjuk sakti:', error);
|
||||||
toast.error("Gagal memuat data pelayanan telunjuk sakti");
|
toast.error('Gagal memuat data pelayanan telunjuk sakti');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadPelayananTelunjukSakti();
|
loadPelayananTelunjukSakti();
|
||||||
@@ -44,57 +56,86 @@ function EditPelayananTelunjukSakti() {
|
|||||||
try {
|
try {
|
||||||
stateTelunjukDesa.edit.form = {
|
stateTelunjukDesa.edit.form = {
|
||||||
...stateTelunjukDesa.edit.form,
|
...stateTelunjukDesa.edit.form,
|
||||||
name: formData.name,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
};
|
||||||
link: formData.link,
|
await stateTelunjukDesa.edit.update();
|
||||||
}
|
toast.success('Pelayanan telunjuk sakti berhasil diperbarui!');
|
||||||
await stateTelunjukDesa.edit.update()
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
|
||||||
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
|
|
||||||
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pelayanan telunjuk sakti:", error);
|
console.error('Error updating pelayanan telunjuk sakti:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti");
|
toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Back Button + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Pelayanan Telunjuk Sakti Desa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Nama */}
|
||||||
|
<TextInput
|
||||||
|
label="Nama Pelayanan"
|
||||||
|
placeholder="Masukkan nama pelayanan"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi pakai editor */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
|
||||||
<Stack gap={"xs"}>
|
{/* Link */}
|
||||||
<Title order={3}>Edit Surat Keterangan</Title>
|
<TextInput
|
||||||
<TextInput
|
label="Link"
|
||||||
value={formData.name}
|
placeholder="Masukkan link terkait"
|
||||||
onChange={(val) => {
|
value={formData.link}
|
||||||
setFormData({ ...formData, name: val.target.value });
|
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
||||||
}}
|
/>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
|
|
||||||
placeholder="masukkan nama surat keterangan"
|
{/* Tombol Simpan */}
|
||||||
/>
|
<Group justify="right">
|
||||||
<TextInput
|
<Button
|
||||||
value={formData.deskripsi}
|
onClick={handleSubmit}
|
||||||
onChange={(val) => {
|
radius="md"
|
||||||
setFormData({ ...formData, deskripsi: val.target.value });
|
size="md"
|
||||||
}}
|
style={{
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
placeholder="masukkan tautan link"
|
color: '#fff',
|
||||||
/>
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
<TextInput
|
}}
|
||||||
value={formData.link}
|
>
|
||||||
onChange={(val) => {
|
Simpan
|
||||||
setFormData({ ...formData, link: val.target.value });
|
</Button>
|
||||||
}}
|
</Group>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
</Stack>
|
||||||
placeholder="masukkan link"
|
</Paper>
|
||||||
/>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,109 +2,166 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
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, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } 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 DetailPelayananTelunjukSakti() {
|
function DetailPelayananTelunjukSakti() {
|
||||||
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const telunjukSaktiState = useProxy(
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
stateLayananDesa.pelayananTelunjukSaktiDesa
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
);
|
||||||
const params = useParams()
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const router = useRouter()
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
telunjukSaktiState.findUnique.load(params?.id as string)
|
telunjukSaktiState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
telunjukSaktiState.delete.byId(selectedId)
|
telunjukSaktiState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!telunjukSaktiState.findUnique.data) {
|
if (!telunjukSaktiState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
{Array.from({ length: 10 }).map((_, k) => (
|
<Skeleton height={500} radius="md" />
|
||||||
<Skeleton key={k} h={40} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = telunjukSaktiState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Pelayanan Telunjuk Sakti Desa</Text>
|
Kembali
|
||||||
{telunjukSaktiState.findUnique.data ? (
|
</Button>
|
||||||
<Paper key={telunjukSaktiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Box>
|
withBorder
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama</Text>
|
w={{ base: '100%', md: '60%' }}
|
||||||
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.name}</Text>
|
bg={colors['white-1']}
|
||||||
</Box>
|
p="lg"
|
||||||
<Box>
|
radius="md"
|
||||||
<Text fw={"bold"} fz={"lg"}>Link</Text>
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Pelayanan Telunjuk Sakti Desa
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Nama
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Link
|
||||||
|
</Text>
|
||||||
|
{data?.link ? (
|
||||||
<Text
|
<Text
|
||||||
|
fz="md"
|
||||||
component="a"
|
component="a"
|
||||||
href={telunjukSaktiState.findUnique.data?.link}
|
href={data.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
c="blue"
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{telunjukSaktiState.findUnique.data?.link}
|
{data.link}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
) : (
|
||||||
<Box>
|
<Text fz="sm" c="dimmed">
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
Tidak ada link
|
||||||
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.deskripsi}</Text>
|
</Text>
|
||||||
</Box>
|
)}
|
||||||
<Flex gap={"xs"} mt={10}>
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data?.deskripsi || '-',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Layanan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (telunjukSaktiState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(telunjukSaktiState.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={telunjukSaktiState.delete.loading || !telunjukSaktiState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
disabled={telunjukSaktiState.delete.loading}
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Layanan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (telunjukSaktiState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${telunjukSaktiState.findUnique.data.id}/edit`);
|
router.push(
|
||||||
}
|
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${data.id}/edit`
|
||||||
}}
|
)
|
||||||
disabled={!telunjukSaktiState.findUnique.data}
|
}
|
||||||
color={"green"}
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -113,7 +170,7 @@ function DetailPelayananTelunjukSakti() {
|
|||||||
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 layanan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,64 +1,117 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
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,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
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 { toast } from 'react-toastify';
|
||||||
|
|
||||||
function CreatePelayananTelunjukDesa() {
|
function CreatePelayananTelunjukDesa() {
|
||||||
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateTelunjukDesa.create.form = {
|
stateTelunjukDesa.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
link: "",
|
link: '',
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await stateTelunjukDesa.create.create()
|
try {
|
||||||
resetForm()
|
await stateTelunjukDesa.create.create();
|
||||||
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
|
resetForm();
|
||||||
|
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
|
||||||
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error create pelayanan telunjuk sakti:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menambahkan data');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Create Pelayanan Telunjuk Sakti Desa</Title>
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Pelayanan Telunjuk Sakti Desa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Nama */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.name}
|
value={stateTelunjukDesa.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateTelunjukDesa.create.form.name = val.target.value;
|
stateTelunjukDesa.create.form.name = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>}
|
label={<Text fz="sm" fw="bold">Nama Pelayanan</Text>}
|
||||||
placeholder="masukkan nama pelayanan telunjuk sakti desa"
|
placeholder="Masukkan nama pelayanan telunjuk sakti desa"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.deskripsi}
|
value={stateTelunjukDesa.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateTelunjukDesa.create.form.deskripsi = val.target.value;
|
stateTelunjukDesa.create.form.deskripsi = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
|
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
||||||
placeholder="masukkan tautan link"
|
placeholder="Masukkan deskripsi pelayanan"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateTelunjukDesa.create.form.link}
|
value={stateTelunjukDesa.create.form.link}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateTelunjukDesa.create.form.link = val.target.value;
|
stateTelunjukDesa.create.form.link = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
|
label={<Text fz="sm" fw="bold">Link</Text>}
|
||||||
placeholder="masukkan link"
|
placeholder="Masukkan link pelayanan"
|
||||||
/>
|
/>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,187 @@
|
|||||||
|
// /* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
// 'use client'
|
||||||
|
// import colors from '@/con/colors';
|
||||||
|
// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||||
|
// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
|
// import { useRouter } from 'next/navigation';
|
||||||
|
// import { useEffect, useMemo, useState } from 'react';
|
||||||
|
// import { useProxy } from 'valtio/utils';
|
||||||
|
// import HeaderSearch from '../../../_com/header';
|
||||||
|
// import JudulList from '../../../_com/judulList';
|
||||||
|
// import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
|
// function PelayananTelunjukSakti() {
|
||||||
|
// const [search, setSearch] = useState("");
|
||||||
|
// return (
|
||||||
|
// <Box>
|
||||||
|
// <HeaderSearch
|
||||||
|
// title='Posisi Organisasi'
|
||||||
|
// placeholder='pencarian'
|
||||||
|
// searchIcon={<IconSearch size={20} />}
|
||||||
|
// value={search}
|
||||||
|
// onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
// />
|
||||||
|
// <ListPelayananTelunjukSakti search={search} />
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
||||||
|
// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
||||||
|
// const router = useRouter()
|
||||||
|
|
||||||
|
// const {
|
||||||
|
// data,
|
||||||
|
// page,
|
||||||
|
// totalPages,
|
||||||
|
// loading,
|
||||||
|
// load,
|
||||||
|
// } = telunjukSaktiState.findMany;
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// load(page, 10)
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// const filteredData = useMemo(() => {
|
||||||
|
// if (!data) return [];
|
||||||
|
// return data.filter(item => {
|
||||||
|
// const keyword = search.toLowerCase();
|
||||||
|
// return (
|
||||||
|
// item.name?.toLowerCase().includes(keyword) ||
|
||||||
|
// item.link?.toLowerCase().includes(keyword) ||
|
||||||
|
// item.deskripsi?.toLowerCase().includes(keyword)
|
||||||
|
// );
|
||||||
|
// })
|
||||||
|
// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
||||||
|
// }, [data, search]);
|
||||||
|
|
||||||
|
// if (loading || !data) {
|
||||||
|
// return (
|
||||||
|
// <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 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 (
|
||||||
|
// <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>
|
||||||
|
// {filteredData.map((item) => (
|
||||||
|
// <TableTr key={item.id}>
|
||||||
|
// <TableTd>
|
||||||
|
// <Box w={100}>
|
||||||
|
// <Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
|
||||||
|
// </Box>
|
||||||
|
// </TableTd>
|
||||||
|
// <TableTd>
|
||||||
|
// <Box w={100}>
|
||||||
|
// <a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||||
|
// <Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
||||||
|
// </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>
|
||||||
|
// </Table>
|
||||||
|
// </Paper>
|
||||||
|
// <Center>
|
||||||
|
// <Pagination
|
||||||
|
// value={page}
|
||||||
|
// onChange={(newPage) => {
|
||||||
|
// load(newPage, 10);
|
||||||
|
// window.scrollTo(0, 0);
|
||||||
|
// }}
|
||||||
|
// total={totalPages}
|
||||||
|
// mt="md"
|
||||||
|
// mb="md"
|
||||||
|
// />
|
||||||
|
// </Center>
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default PelayananTelunjukSakti;
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
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';
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function PelayananTelunjukSakti() {
|
function PelayananTelunjukSakti() {
|
||||||
@@ -15,8 +189,8 @@ function PelayananTelunjukSakti() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title="Pelayanan Telunjuk Sakti"
|
||||||
placeholder='pencarian'
|
placeholder="Cari layanan..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,125 +201,113 @@ function PelayananTelunjukSakti() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = telunjukSaktiState.findMany;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = data || [];
|
||||||
if (!data) return [];
|
|
||||||
return data.filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name?.toLowerCase().includes(keyword) ||
|
|
||||||
item.link?.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
|
||||||
}, [data, search]);
|
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={400} radius="md" />
|
||||||
</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 withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Pelayanan Telunjuk Sakti Desa'
|
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
|
||||||
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
<Tooltip label="Tambah Layanan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Link</TableTh>
|
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
|
||||||
<TableTh>Detail</TableTh>
|
}
|
||||||
</TableTr>
|
>
|
||||||
</TableThead>
|
Tambah Baru
|
||||||
<TableTbody>
|
</Button>
|
||||||
{filteredData.map((item) => (
|
</Tooltip>
|
||||||
<TableTr key={item.id}>
|
</Group>
|
||||||
<TableTd>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Box w={100}>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
|
<TableThead>
|
||||||
</Box>
|
<TableTr>
|
||||||
</TableTd>
|
<TableTh style={{ width: '30%' }}>Nama</TableTh>
|
||||||
<TableTd>
|
<TableTh style={{ width: '40%' }}>Link</TableTh>
|
||||||
<Box w={100}>
|
<TableTh style={{ width: '30%' }}>Detail</TableTh>
|
||||||
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
|
||||||
</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>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text></Box>
|
||||||
|
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={200}>
|
||||||
|
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
||||||
|
</a>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data layanan yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10, search);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -153,3 +315,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default PelayananTelunjukSakti;
|
export default PelayananTelunjukSakti;
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,22 @@ 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 {
|
||||||
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { 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';
|
||||||
|
|
||||||
@@ -83,51 +95,104 @@ function EditPenghargaan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Tombol Back + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Edit Penghargaan</Title>
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Penghargaan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Card Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Input Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul"
|
||||||
|
placeholder="Masukkan judul penghargaan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
required
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Input Juara */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Juara"
|
||||||
|
placeholder="Masukkan juara"
|
||||||
value={formData.juara}
|
value={formData.juara}
|
||||||
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
|
required
|
||||||
placeholder="masukkan juara"
|
|
||||||
/>
|
|
||||||
<FileInput
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</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 ? (
|
{/* Upload Gambar */}
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
|
||||||
) : (
|
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
|
||||||
<IconImageInPicture />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Penghargaan
|
||||||
|
</Text>
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => {
|
||||||
|
const selectedFile = files[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': [] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar wajib
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -137,7 +202,21 @@ function EditPenghargaan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit Penghargaan</Button>
|
{/* Tombol Simpan */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,105 +4,166 @@ import penghargaanState from '../../../_state/desa/penghargaan';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
function DetailPenghargaan() {
|
function DetailPenghargaan() {
|
||||||
const statePenghargaan = useProxy(penghargaanState)
|
const statePenghargaan = useProxy(penghargaanState);
|
||||||
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 router = useRouter()
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
statePenghargaan.findUnique.load(params?.id as string)
|
statePenghargaan.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
statePenghargaan.delete.byId(selectedId)
|
statePenghargaan.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/penghargaan")
|
router.push('/admin/desa/penghargaan');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!statePenghargaan.findUnique.data) {
|
if (!statePenghargaan.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = statePenghargaan.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Penghargaan</Text>
|
</Button>
|
||||||
{statePenghargaan.findUnique.data ? (
|
|
||||||
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
withBorder
|
||||||
<Box>
|
w={{ base: '100%', md: '60%' }}
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
bg={colors['white-1']}
|
||||||
<Image w={{ base: 400, md: 400, lg: 400 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" />
|
p="lg"
|
||||||
</Box>
|
radius="md"
|
||||||
<Box>
|
shadow="sm"
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
>
|
||||||
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text>
|
<Stack gap="md">
|
||||||
</Box>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Box>
|
Detail Penghargaan
|
||||||
<Text fw={"bold"} fz={"lg"}>Juara</Text>
|
</Text>
|
||||||
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.juara}</Text>
|
|
||||||
</Box>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Box>
|
<Stack gap="sm">
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
<Box>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} />
|
<Text fz="lg" fw="bold">
|
||||||
</Box>
|
Gambar
|
||||||
<Flex gap={"xs"} mt={10}>
|
</Text>
|
||||||
|
{data.image?.link ? (
|
||||||
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.name || 'Gambar Penghargaan'}
|
||||||
|
w={200}
|
||||||
|
h={200}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Judul
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Juara
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data.juara || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm" mt={10}>
|
||||||
|
<Tooltip label="Hapus Penghargaan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (statePenghargaan.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(statePenghargaan.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={statePenghargaan.delete.loading || !statePenghargaan.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Penghargaan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (statePenghargaan.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/penghargaan/${statePenghargaan.findUnique.data.id}/edit`);
|
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
|
||||||
}
|
}
|
||||||
}}
|
variant="light"
|
||||||
disabled={!statePenghargaan.findUnique.data}
|
radius="md"
|
||||||
color={"green"}
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* 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 penghargaan ini?'
|
text="Apakah Anda yakin ingin menghapus penghargaan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,83 +1,109 @@
|
|||||||
'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,
|
||||||
|
Tooltip,
|
||||||
|
} 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() {
|
||||||
const statePenghargaan = useProxy(penghargaanState)
|
const statePenghargaan = useProxy(penghargaanState);
|
||||||
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 router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
statePenghargaan.create.form = {
|
statePenghargaan.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
juara: "",
|
juara: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
}
|
};
|
||||||
setPreviewImage(null)
|
setPreviewImage(null);
|
||||||
setFile(null)
|
setFile(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.error("Silahkan pilih file gambar terlebih dahulu")
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: file,
|
file,
|
||||||
name: file.name
|
name: file.name,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
const uploaded = res.data?.data
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal upload gambar")
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
}
|
}
|
||||||
|
|
||||||
statePenghargaan.create.form.imageId = uploaded.id
|
statePenghargaan.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
await statePenghargaan.create.create()
|
await statePenghargaan.create.create();
|
||||||
resetForm()
|
resetForm();
|
||||||
router.push("/admin/desa/penghargaan")
|
router.push('/admin/desa/penghargaan');
|
||||||
|
};
|
||||||
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Create Penghargaan</Title>
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Penghargaan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={statePenghargaan.create.form.name}
|
value={statePenghargaan.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
|
||||||
statePenghargaan.create.form.name = val.target.value;
|
label={<Text fz="sm" fw="bold">Nama Penghargaan</Text>}
|
||||||
}}
|
placeholder="Masukkan nama penghargaan"
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>}
|
required
|
||||||
placeholder="masukkan nama penghargaan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={statePenghargaan.create.form.juara}
|
value={statePenghargaan.create.form.juara}
|
||||||
onChange={(val) => {
|
onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
|
||||||
statePenghargaan.create.form.juara = val.target.value;
|
label={<Text fz="sm" fw="bold">Juara</Text>}
|
||||||
}}
|
placeholder="Masukkan juara"
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
|
required
|
||||||
placeholder="masukkan juara"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz="sm" fw="bold" mb={6}>Deskripsi</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={statePenghargaan.create.form.deskripsi}
|
value={statePenghargaan.create.form.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -85,26 +111,67 @@ function CreatePenghargaan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<FileInput
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
|
{/* Dropzone Upload */}
|
||||||
value={file}
|
<Box>
|
||||||
onChange={async (e) => {
|
<Text fz="sm" fw="bold" mb={6}>Gambar</Text>
|
||||||
if (!e) return;
|
<Dropzone
|
||||||
setFile(e);
|
onDrop={(files) => {
|
||||||
const base64 = await e.arrayBuffer().then((buf) =>
|
const selectedFile = files[0];
|
||||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
if (selectedFile) {
|
||||||
);
|
setFile(selectedFile);
|
||||||
setPreviewImage(base64);
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
{previewImage ? (
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
maxSize={5 * 1024 ** 2}
|
||||||
) : (
|
accept={{ 'image/*': [] }}
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
radius="md"
|
||||||
<IconImageInPicture />
|
p="xl"
|
||||||
</Center>
|
>
|
||||||
)}
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Button Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,21 +2,38 @@
|
|||||||
'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, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
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='Penghargaan'
|
title="Penghargaan"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama atau deskripsi..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,125 +44,114 @@ function Penghargaan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPenghargaan({ search }: { search: string }) {
|
function ListPenghargaan({ search }: { search: string }) {
|
||||||
const state = useProxy(penghargaanState)
|
const state = useProxy(penghargaanState);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = state.findMany;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = data || []
|
||||||
if (!data) return [];
|
|
||||||
return data.filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name?.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}, [data, search]);
|
|
||||||
|
|
||||||
// Handle loading state
|
// Loading state
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
return (
|
||||||
return (
|
<Box py={10}>
|
||||||
<Box py={10}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Group justify="space-between" mb="md">
|
||||||
<JudulList
|
<Title order={4}>List Penghargaan</Title>
|
||||||
title='List Penghargaan'
|
<Tooltip label="Tambah Penghargaan" withArrow>
|
||||||
href='/admin/desa/penghargaan/create'
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Table striped withTableBorder withRowBorders>
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/penghargaan/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '35%' }}>Nama</TableTh>
|
||||||
<TableTh>Deskripsi</TableTh>
|
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||||
<TableTh>Image</TableTh>
|
<TableTh style={{ width: '30%' }}>Aksi</TableTh>
|
||||||
<TableTh>Detail</TableTh>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
<TableTr>
|
{filteredData.length > 0 ? (
|
||||||
<TableTd colSpan={4}>
|
filteredData.map((item) => (
|
||||||
<Text ta="center">Tidak ada data</Text>
|
<TableTr key={item.id}>
|
||||||
</TableTd>
|
<TableTd>
|
||||||
</TableTr>
|
<Box w={200}>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text
|
||||||
|
truncate="end"
|
||||||
|
lineClamp={1}
|
||||||
|
fz="sm"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/penghargaan/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Tidak ada data penghargaan yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Box>
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
|
||||||
<TableTd>
|
|
||||||
<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>
|
|
||||||
<Image w={100} src={item.image?.link} alt="gambar" />
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Text>
|
|
||||||
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
|
|
||||||
<IconDeviceImac size={20} />
|
|
||||||
</Button>
|
|
||||||
</Text>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
))}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10, search);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } 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';
|
||||||
|
import { IconListDetails, IconCategory } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,16 +13,21 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "List Pengumuman",
|
label: "List Pengumuman",
|
||||||
value: "listpengumuman",
|
value: "listpengumuman",
|
||||||
href: "/admin/desa/pengumuman/list-pengumuman"
|
href: "/admin/desa/pengumuman/list-pengumuman",
|
||||||
|
icon: <IconListDetails size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat semua daftar pengumuman"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Pengumuman",
|
label: "Kategori Pengumuman",
|
||||||
value: "kategoripengumuman",
|
value: "kategoripengumuman",
|
||||||
href: "/admin/desa/pengumuman/kategori-pengumuman"
|
href: "/admin/desa/pengumuman/kategori-pengumuman",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori pengumuman"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -39,24 +45,59 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Pengumuman</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsLayanan;
|
export default LayoutTabsLayanan;
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Text,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
@@ -10,67 +20,108 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriPengumuman() {
|
function EditKategoriPengumuman() {
|
||||||
const editState = useProxy(stateDesaPengumuman.category)
|
const editState = useProxy(stateDesaPengumuman.category);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: editState.update.form.name || '',
|
name: editState.update.form.name || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
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 {
|
try {
|
||||||
editState.update.form = {
|
const data = await editState.update.load(id);
|
||||||
...editState.update.form,
|
if (data) {
|
||||||
name: formData.name,
|
setFormData({
|
||||||
};
|
name: data.name || '',
|
||||||
await editState.update.update();
|
});
|
||||||
toast.success('Kategori Pengumuman berhasil diperbarui!');
|
}
|
||||||
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating kategori Pengumuman:', error);
|
console.error('Error loading kategori Pengumuman:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
|
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 (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Edit Kategori Pengumuman</Title>
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Kategori Pengumuman
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label={
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Nama Kategori Pengumuman
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
placeholder="Masukkan nama kategori Pengumuman"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>}
|
setFormData({ ...formData, name: e.target.value })
|
||||||
placeholder="masukkan nama kategori Pengumuman"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriPengumuman() {
|
function CreateKategoriPengumuman() {
|
||||||
const createState = useProxy(stateDesaPengumuman.category)
|
const createState = useProxy(stateDesaPengumuman.category);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/pengumuman/kategori-pengumuman")
|
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Pengumuman
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Pengumuman</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Pengumuman</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Pengumuman</Text>}
|
||||||
placeholder='Masukkan nama kategori Pengumuman'
|
placeholder="Masukkan nama kategori pengumuman"
|
||||||
value={createState.create.form.name}
|
value={createState.create.form.name || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.name = e.target.value)}
|
||||||
createState.create.form.name = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
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 {
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
Box, Button, Center, Paper, Skeleton, Stack,
|
||||||
|
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
|
||||||
|
Text, Title, Tooltip, Pagination
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function KategoriPengumuman() {
|
function KategoriPengumuman() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Pengumuman'
|
title='Kategori Pengumuman'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama kategori...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -34,87 +35,121 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
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 { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(1, 10, search)
|
||||||
}, [])
|
}, [search])
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
|
load(page, 10, search)
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Stack>
|
||||||
<JudulList
|
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
||||||
title='List Kategori Pengumuman'
|
<Title order={4}>List Kategori Pengumuman</Title>
|
||||||
href='/admin/desa/pengumuman/kategori-pengumuman/create'
|
<Tooltip label="Tambah Kategori Pengumuman" withArrow>
|
||||||
/>
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>No</TableTh>
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '60%' }}>Nama</TableTh>
|
||||||
<TableTh>Edit</TableTh>
|
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||||
<TableTh>Hapus</TableTh>
|
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item, index) => (
|
{filteredData.length > 0 ? (
|
||||||
<TableTr key={item.id}>
|
filteredData.map((item, index) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
<Box w={100}>
|
<TableTd>
|
||||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||||
</Box>
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd>
|
||||||
<TableTd>{item.name}</TableTd>
|
<Text truncate lineClamp={1}>{item.name}</Text>
|
||||||
<TableTd>
|
</TableTd>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}>
|
<TableTd>
|
||||||
<IconEdit size={20} />
|
<Tooltip label="Edit Kategori Pengumuman" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</TableTd>
|
variant='light'
|
||||||
<TableTd>
|
color='green'
|
||||||
<Button
|
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
|
||||||
color='red'
|
>
|
||||||
disabled={listDataState.delete.loading}
|
<IconEdit size={20} />
|
||||||
onClick={() => {
|
</Button>
|
||||||
setSelectedId(item.id)
|
</Tooltip>
|
||||||
setModalHapus(true)
|
</TableTd>
|
||||||
}}>
|
<TableTd>
|
||||||
<IconTrash size={20} />
|
<Tooltip label="Hapus Kategori Pengumuman" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
|
variant='light'
|
||||||
|
color='red'
|
||||||
|
disabled={listDataState.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id)
|
||||||
|
setModalHapus(true)
|
||||||
|
}}>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">Tidak ada data kategori pengumuman yang cocok</Text>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
<Center mt="md">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage, 10, search)}
|
||||||
|
total={totalPages}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import colors from "@/con/colors";
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconArrowBack } from "@tabler/icons-react";
|
import { IconArrowBack } from "@tabler/icons-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
@@ -20,34 +22,34 @@ 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 EditPengumuman() {
|
function EditPengumuman() {
|
||||||
const editState = useProxy(stateDesaPengumuman);
|
const editState = useProxy(stateDesaPengumuman);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: editState.pengumuman.edit.form.judul || '',
|
judul: editState.pengumuman.edit.form.judul || "",
|
||||||
deskripsi: editState.pengumuman.edit.form.deskripsi || '',
|
deskripsi: editState.pengumuman.edit.form.deskripsi || "",
|
||||||
categoryPengumumanId: editState.pengumuman.edit.form.categoryPengumumanId || '',
|
categoryPengumumanId:
|
||||||
content: editState.pengumuman.edit.form.content || ''
|
editState.pengumuman.edit.form.categoryPengumumanId || "",
|
||||||
|
content: editState.pengumuman.edit.form.content || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load pengumuman by id saat pertama kali
|
// Load pengumuman by id saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editState.category.findMany.load()
|
editState.category.findMany.load();
|
||||||
const loadpengumuman = async () => {
|
const loadpengumuman = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await stateDesaPengumuman.pengumuman.edit.load(id); // akses langsung, bukan dari proxy
|
const data = await stateDesaPengumuman.pengumuman.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
judul: data.judul || '',
|
judul: data.judul || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
categoryPengumumanId: data.categoryPengumumanId || '',
|
categoryPengumumanId: data.categoryPengumumanId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -57,21 +59,18 @@ function EditPengumuman() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadpengumuman();
|
loadpengumuman();
|
||||||
}, [params?.id]); // ✅ hapus editState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// edit global state with form data
|
// update global state
|
||||||
editState.pengumuman.edit.form = {
|
editState.pengumuman.edit.form = {
|
||||||
...editState.pengumuman.edit.form,
|
...editState.pengumuman.edit.form,
|
||||||
judul: formData.judul,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
content: formData.content,
|
|
||||||
categoryPengumumanId: formData.categoryPengumumanId || ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await editState.pengumuman.edit.update();
|
await editState.pengumuman.edit.update();
|
||||||
toast.success("pengumuman berhasil diperbarui!");
|
toast.success("Pengumuman berhasil diperbarui!");
|
||||||
router.push("/admin/desa/pengumuman/list-pengumuman");
|
router.push("/admin/desa/pengumuman/list-pengumuman");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pengumuman:", error);
|
console.error("Error updating pengumuman:", error);
|
||||||
@@ -80,57 +79,97 @@ function EditPengumuman() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button
|
||||||
</Button>
|
variant="subtle"
|
||||||
</Box>
|
onClick={() => router.back()}
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
p="xs"
|
||||||
<Stack gap={"xs"}>
|
radius="md"
|
||||||
<Title order={3}>Edit pengumuman</Title>
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Pengumuman
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul Pengumuman"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
required
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi Singkat"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
setFormData({ ...formData, deskripsi: e.target.value })
|
||||||
placeholder="masukkan deskripsi"
|
}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<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
|
<Select
|
||||||
value={formData.categoryPengumumanId}
|
value={formData.categoryPengumumanId}
|
||||||
onChange={(val) => setFormData({ ...formData, categoryPengumumanId: val || "" })}
|
onChange={(val) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
setFormData({ ...formData, categoryPengumumanId: val || "" })
|
||||||
placeholder='Pilih kategori'
|
}
|
||||||
|
label="Kategori"
|
||||||
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
editState.category.findMany.data?.map((v) => ({
|
editState.category.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.name
|
label: v.name,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
required
|
required
|
||||||
error={!formData.categoryPengumumanId ? "Pilih kategori" : undefined}
|
error={
|
||||||
|
!formData.categoryPengumumanId ? "Pilih kategori" : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Edit pengumuman</Button>
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData({ ...formData, content: htmlContent })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
|
||||||
|
color: "#fff",
|
||||||
|
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,116 +1,163 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { useProxy } from 'valtio/utils';
|
import {
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
|
|
||||||
|
export default 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/list-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 height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = pengumumanState.pengumuman.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Pengumuman</Text>
|
</Button>
|
||||||
{pengumumanState.pengumuman.findUnique.data ? (
|
|
||||||
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
withBorder
|
||||||
<Box>
|
w={{ base: '100%', md: '60%' }}
|
||||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
bg={colors['white-1']}
|
||||||
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.CategoryPengumuman?.name}</Text>
|
p="lg"
|
||||||
</Box>
|
radius="md"
|
||||||
<Box>
|
shadow="sm"
|
||||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
>
|
||||||
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.judul}</Text>
|
<Stack gap="md">
|
||||||
</Box>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Box>
|
Detail Pengumuman
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
</Text>
|
||||||
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.deskripsi}</Text>
|
|
||||||
</Box>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Box>
|
<Stack gap="sm">
|
||||||
<Text fw={"bold"} fz={"lg"}>Konten</Text>
|
<Box>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: pengumumanState.pengumuman.findUnique.data?.content }} />
|
<Text fz="lg" fw="bold">
|
||||||
</Box>
|
Kategori
|
||||||
<Flex gap={"xs"} mt={10}>
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.CategoryPengumuman?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Judul
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.judul || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text fz="md" c="dimmed">
|
||||||
|
{data?.deskripsi || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">
|
||||||
|
Konten
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data?.content || '-',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Pengumuman" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pengumumanState.pengumuman.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(pengumumanState.pengumuman.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={pengumumanState.pengumuman.delete.loading || !pengumumanState.pengumuman.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Pengumuman" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (pengumumanState.pengumuman.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/desa/pengumuman/list-pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`);
|
router.push(
|
||||||
}
|
`/admin/desa/pengumuman/list-pengumuman/${data.id}/edit`
|
||||||
}}
|
)
|
||||||
disabled={!pengumumanState.pengumuman.findUnique.data}
|
}
|
||||||
color={"green"}
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* 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 pengumuman ini?'
|
text="Apakah anda yakin ingin menghapus pengumuman ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailPengumuman;
|
|
||||||
@@ -1,79 +1,110 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
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,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
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';
|
||||||
|
|
||||||
|
|
||||||
function CreatePengumuman() {
|
function CreatePengumuman() {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman)
|
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pengumumanState.category.findMany.load()
|
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/list-pengumuman")
|
router.push('/admin/desa/pengumuman/list-pengumuman');
|
||||||
}
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
pengumumanState.pengumuman.create.form = {
|
pengumumanState.pengumuman.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
content: "",
|
content: '',
|
||||||
categoryPengumumanId: "",
|
categoryPengumumanId: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
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'}>
|
return (
|
||||||
<Stack gap={"xs"}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Title order={4}>Create Pengumuman</Title>
|
{/* Header */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Pengumuman
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
value={pengumumanState.pengumuman.create.form.judul}
|
||||||
placeholder='Masukkan judul'
|
onChange={(val) => (pengumumanState.pengumuman.create.form.judul = val.target.value)}
|
||||||
onChange={(val) => {
|
label={<Text fz="sm" fw="bold">Judul</Text>}
|
||||||
pengumumanState.pengumuman.create.form.judul = val.target.value
|
placeholder="Masukkan judul pengumuman"
|
||||||
}}
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Kategori */}
|
||||||
<Select
|
<Select
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
label={<Text fz="sm" fw="bold">Kategori</Text>}
|
||||||
placeholder='Pilih kategori'
|
placeholder="Pilih kategori"
|
||||||
|
value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
|
||||||
|
}}
|
||||||
data={pengumumanState.category.findMany.data?.map((item) => ({
|
data={pengumumanState.category.findMany.data?.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}))}
|
}))}
|
||||||
onChange={(val) => {
|
|
||||||
const selected = pengumumanState.category.findMany.data?.find((item) => item.id === val);
|
|
||||||
if (selected) {
|
|
||||||
pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
searchable
|
searchable
|
||||||
nothingFoundMessage="Tidak ditemukan"
|
nothingFoundMessage="Tidak ditemukan"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi Singkat */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
|
value={pengumumanState.pengumuman.create.form.deskripsi}
|
||||||
placeholder='Masukkan deskripsi singkat'
|
onChange={(val) => (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
|
||||||
onChange={(val) => {
|
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
|
||||||
pengumumanState.pengumuman.create.form.deskripsi = val.target.value
|
placeholder="Masukkan deskripsi singkat"
|
||||||
}}
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Konten Editor */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={pengumumanState.pengumuman.create.form.content}
|
value={pengumumanState.pengumuman.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -82,8 +113,20 @@ function CreatePengumuman() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
{/* Tombol Submit */}
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { 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';
|
||||||
@@ -9,14 +28,13 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
import stateDesaPengumuman from '../../../_state/desa/pengumuman';
|
||||||
|
|
||||||
|
|
||||||
function Pengumuman() {
|
function Pengumuman() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='List Pengumuman'
|
title="Pengumuman Desa"
|
||||||
placeholder='pencarian'
|
placeholder="Cari judul atau kategori..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,86 +45,107 @@ 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 {
|
|
||||||
data,
|
const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = pengumumanState.pengumuman.findMany;
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (data || [])
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<Grid>
|
<Title order={4}>Daftar Pengumuman</Title>
|
||||||
<GridCol span={{ base: 12, md: 11 }}>
|
<Tooltip label="Tambah Pengumuman" withArrow>
|
||||||
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text>
|
<Button
|
||||||
</GridCol>
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
color="blue"
|
||||||
<Button onClick={() => router.push("/admin/desa/pengumuman/list-pengumuman/create")} bg={colors['blue-button']}>
|
variant="light"
|
||||||
<IconCircleDashedPlus size={25} />
|
onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
|
||||||
</Button>
|
>
|
||||||
</GridCol>
|
Tambah Baru
|
||||||
</Grid>
|
</Button>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
</Tooltip>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
</Group>
|
||||||
<TableThead>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTr>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
<TableTh w={250}>Judul</TableTh>
|
<TableThead>
|
||||||
<TableTh w={250}>Kategori</TableTh>
|
<TableTr>
|
||||||
<TableTh w={200}>Detail</TableTh>
|
<TableTh style={{ width: '40%' }}>Judul</TableTh>
|
||||||
|
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
||||||
</TableTr>
|
<TableTh style={{ width: '20%' }}>Detail</TableTh>
|
||||||
</TableThead>
|
</TableTr>
|
||||||
<TableTbody >
|
</TableThead>
|
||||||
{filteredData.map((item) => (
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd >
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.judul}</Text>
|
{item.judul}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd >{item.CategoryPengumuman?.name}</TableTd>
|
<TableTd>
|
||||||
<TableTd>
|
<Text fz="sm" c="dimmed">
|
||||||
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)}>
|
{item.CategoryPengumuman?.name || '-'}
|
||||||
<IconDeviceImacCog size={25} />
|
</Text>
|
||||||
</Button>
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={3}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada pengumuman yang cocok</Text>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Pengumuman;
|
export default Pengumuman;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||||
|
import { IconCategory, IconListCheck } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -12,17 +13,21 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "List Potensi",
|
label: "List Potensi",
|
||||||
value: "list_potensi",
|
value: "list_potensi",
|
||||||
href: "/admin/desa/potensi/list-potensi"
|
href: "/admin/desa/potensi/list-potensi",
|
||||||
|
icon: <IconListCheck size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat semua potensi desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Potensi",
|
label: "Kategori Potensi",
|
||||||
value: "kategori_potensi",
|
value: "kategori_potensi",
|
||||||
href: "/admin/desa/potensi/kategori-potensi"
|
href: "/admin/desa/potensi/kategori-potensi",
|
||||||
|
icon: <IconCategory size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola kategori potensi"
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -40,24 +45,59 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Potensi</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsPotensi;
|
export default LayoutTabsPotensi;
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
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 { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } 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';
|
||||||
@@ -10,67 +19,95 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditKategoriPotensi() {
|
function EditKategoriPotensi() {
|
||||||
const editState = useProxy(potensiDesaState.kategoriPotensi)
|
const editState = useProxy(potensiDesaState.kategoriPotensi);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nama: editState.update.form.nama || '',
|
nama: editState.update.form.nama || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadKategori = async () => {
|
const loadKategori = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
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 {
|
try {
|
||||||
editState.update.form = {
|
const data = await editState.update.load(id);
|
||||||
...editState.update.form,
|
if (data) {
|
||||||
nama: formData.nama,
|
setFormData({
|
||||||
};
|
nama: data.nama || '',
|
||||||
await editState.update.update();
|
});
|
||||||
toast.success('Kategori Potensi berhasil diperbarui!');
|
}
|
||||||
router.push('/admin/desa/potensi/kategori-potensi');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating kategori potensi:', error);
|
console.error('Error loading kategori potensi:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui kategori potensi');
|
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 (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Button>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Box>
|
</Button>
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Tooltip>
|
||||||
<Stack gap={"xs"}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Title order={3}>Edit Kategori Potensi</Title>
|
Edit Kategori Potensi
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Kategori Potensi"
|
||||||
|
placeholder="Masukkan nama kategori potensi"
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Potensi</Text>}
|
required
|
||||||
placeholder="masukkan nama kategori potensi"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={handleSubmit}>Simpan</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,50 +1,87 @@
|
|||||||
'use client'
|
'use client';
|
||||||
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 { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CreateKategoriPotensi() {
|
function CreateKategoriPotensi() {
|
||||||
const createState = useProxy(potensiDesaState.kategoriPotensi)
|
const createState = useProxy(potensiDesaState.kategoriPotensi);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.form = {
|
createState.create.form = {
|
||||||
nama: "",
|
nama: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
await createState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/desa/potensi/kategori-potensi")
|
router.push('/admin/desa/potensi/kategori-potensi');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan back button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Kategori Potensi
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
{/* Form utama */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Potensi</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Potensi</Text>}
|
label={<Text fw="bold" fz="sm">Nama Kategori Potensi</Text>}
|
||||||
placeholder='Masukkan nama kategori Potensi'
|
placeholder="Masukkan nama kategori potensi"
|
||||||
value={createState.create.form.nama}
|
value={createState.create.form.nama || ''}
|
||||||
onChange={(val) => {
|
onChange={(e) => (createState.create.form.nama = e.target.value)}
|
||||||
createState.create.form.nama = val.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Group>
|
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
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, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core';
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
|
|
||||||
function KategoriPotensi() {
|
function KategoriPotensi() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -19,7 +16,7 @@ function KategoriPotensi() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Potensi'
|
title='Kategori Potensi'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama kategori...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -34,87 +31,113 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
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 { data, page, totalPages, loading, load } = listDataState.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listDataState.findMany.load()
|
load(1, 10, search)
|
||||||
}, [])
|
}, [search])
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
listDataState.delete.delete(selectedId)
|
listDataState.delete.delete(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
setSelectedId(null)
|
||||||
|
load(page, 10, search)
|
||||||
listDataState.findMany.load()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.nama.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!listDataState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p="md">
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Stack>
|
||||||
<JudulList
|
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
||||||
title='List Kategori Potensi'
|
<Title order={4}>List Kategori Potensi</Title>
|
||||||
href='/admin/desa/potensi/kategori-potensi/create'
|
<Tooltip label="Tambah Kategori Potensi" withArrow>
|
||||||
/>
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>No</TableTh>
|
<TableTh style={{ width: '10%' }}>No</TableTh>
|
||||||
<TableTh>Nama</TableTh>
|
<TableTh style={{ width: '60%' }}>Nama</TableTh>
|
||||||
<TableTh>Edit</TableTh>
|
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||||
<TableTh>Hapus</TableTh>
|
<TableTh style={{ width: '15%' }}>Hapus</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item, index) => (
|
{filteredData.length > 0 ? (
|
||||||
<TableTr key={item.id}>
|
filteredData.map((item, index) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
<Box w={100}>
|
<TableTd>
|
||||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
||||||
</Box>
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd>
|
||||||
<TableTd>{item.nama}</TableTd>
|
<Text truncate lineClamp={1}>{item.nama}</Text>
|
||||||
<TableTd>
|
</TableTd>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
|
<TableTd>
|
||||||
<IconEdit size={20} />
|
<Button variant='light' color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
|
||||||
</Button>
|
<IconEdit size={20} />
|
||||||
</TableTd>
|
</Button>
|
||||||
<TableTd>
|
</TableTd>
|
||||||
<Button
|
<TableTd>
|
||||||
color='red'
|
<Button
|
||||||
disabled={listDataState.delete.loading}
|
variant='light'
|
||||||
onClick={() => {
|
color='red'
|
||||||
setSelectedId(item.id)
|
disabled={listDataState.delete.loading}
|
||||||
setModalHapus(true)
|
onClick={() => {
|
||||||
}}>
|
setSelectedId(item.id)
|
||||||
<IconTrash size={20} />
|
setModalHapus(true)
|
||||||
</Button>
|
}}>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data kategori potensi yang cocok</Text>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
<Center mt="md">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage, 10, search)}
|
||||||
|
total={totalPages}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
|
|||||||
@@ -5,7 +5,19 @@ 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, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
@@ -13,38 +25,36 @@ 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.potensiDesa)
|
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
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({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: "",
|
||||||
deskripsi: '',
|
deskripsi: "",
|
||||||
kategoriId: '',
|
kategoriId: "",
|
||||||
content: '',
|
content: "",
|
||||||
imageId: ''
|
imageId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiDesaState.kategoriPotensi.findMany.load()
|
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 potensiState.edit.load(id); // ambil data dari API
|
const data = await potensiState.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || '',
|
name: data.name || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
kategoriId: data.kategoriId || '',
|
kategoriId: data.kategoriId || "",
|
||||||
content: data.content || '',
|
content: data.content || "",
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
@@ -62,13 +72,9 @@ function EditPotensi() {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// Sinkronkan semua data dari formData ke state global
|
|
||||||
potensiState.edit.form = {
|
potensiState.edit.form = {
|
||||||
...potensiState.edit.form,
|
...potensiState.edit.form,
|
||||||
name: formData.name,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
kategoriId: formData.kategoriId,
|
|
||||||
content: formData.content,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -92,44 +98,52 @@ function EditPotensi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Button>
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
</Box>
|
</Button>
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Tooltip>
|
||||||
<Stack gap={"xs"}>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Title order={3}>Edit Potensi</Title>
|
Edit Potensi Desa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Judul Potensi"
|
||||||
|
placeholder="Masukkan judul"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
const val = e.target.value;
|
required
|
||||||
setFormData((prev) => ({ ...prev, name: val }));
|
|
||||||
potensiState.edit.form.name = val;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
|
||||||
placeholder="masukkan judul"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Deskripsi Singkat"
|
||||||
|
placeholder="Masukkan deskripsi"
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(e) => {
|
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
|
||||||
const val = e.target.value;
|
required
|
||||||
setFormData((prev) => ({ ...prev, deskripsi: val }));
|
|
||||||
potensiState.edit.form.deskripsi = val;
|
|
||||||
}}
|
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
|
|
||||||
placeholder="masukkan deskripsi"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.kategoriId}
|
value={formData.kategoriId}
|
||||||
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
|
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
label="Kategori"
|
||||||
placeholder='Pilih kategori'
|
placeholder="Pilih kategori"
|
||||||
data={
|
data={
|
||||||
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
|
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.nama
|
label: v.nama,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
@@ -137,77 +151,90 @@ function EditPotensi() {
|
|||||||
required
|
required
|
||||||
error={!formData.kategoriId ? "Pilih kategori" : undefined}
|
error={!formData.kategoriId ? "Pilih kategori" : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Potensi
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ "image/*": [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format gambar wajib
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
<div>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{
|
||||||
</div>
|
maxHeight: 220,
|
||||||
</Group>
|
objectFit: "contain",
|
||||||
</Dropzone>
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
|
}}
|
||||||
{/* Tampilkan preview kalau ada */}
|
/>
|
||||||
{previewImage && (
|
</Box>
|
||||||
<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>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => setFormData({ ...formData, content: htmlContent })}
|
||||||
setFormData((prev) => ({ ...prev, content: htmlContent }));
|
|
||||||
potensiState.edit.form.content = htmlContent;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Potensi</Button>
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
|
||||||
|
color: "#fff",
|
||||||
|
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditPotensi;
|
export default EditPotensi;
|
||||||
|
|||||||
@@ -1,122 +1,151 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import React from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
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';
|
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.potensiDesa)
|
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
potensiState.findUnique.load(params?.id as string)
|
potensiState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
potensiState.delete.byId(selectedId)
|
potensiState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/potensi")
|
router.push("/admin/desa/potensi");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!potensiState.findUnique.data) {
|
if (!potensiState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
{Array.from({ length: 10 }).map((_, k) => (
|
<Skeleton height={500} radius="md" />
|
||||||
<Skeleton key={k} h={40} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = potensiState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
|
</Button>
|
||||||
{potensiState.findUnique.data ? (
|
|
||||||
<Paper key={potensiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
withBorder
|
||||||
<Box>
|
w={{ base: "100%", md: "60%" }}
|
||||||
<Text fz={"lg"} fw={"bold"}>Judul</Text>
|
bg={colors['white-1']}
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.name}</Text>
|
p="lg"
|
||||||
</Box>
|
radius="md"
|
||||||
<Box>
|
shadow="sm"
|
||||||
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
|
>
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.kategori?.nama}</Text>
|
<Stack gap="md">
|
||||||
</Box>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Box>
|
Detail Potensi
|
||||||
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
|
</Text>
|
||||||
<Text fz={"lg"}>{potensiState.findUnique.data.deskripsi}</Text>
|
|
||||||
</Box>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Box>
|
<Stack gap="sm">
|
||||||
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
|
<Box>
|
||||||
<Image src={potensiState.findUnique.data.image?.link} alt="gambar" />
|
<Text fz="lg" fw="bold">Judul</Text>
|
||||||
</Box>
|
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||||
<Box>
|
</Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Konten</Text>
|
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: potensiState.findUnique.data.content }} />
|
<Box>
|
||||||
</Box>
|
<Text fz="lg" fw="bold">Kategori</Text>
|
||||||
<Box>
|
<Text fz="md" c="dimmed">{data.kategori?.nama || '-'}</Text>
|
||||||
<Flex gap={"xs"}>
|
</Box>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
<Box>
|
||||||
if (potensiState.findUnique.data) {
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
setSelectedId(potensiState.findUnique.data.id)
|
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
|
||||||
setModalHapus(true)
|
</Box>
|
||||||
}
|
|
||||||
}}
|
<Box>
|
||||||
disabled={potensiState.delete.loading || !potensiState.findUnique.data}
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
color="red"
|
{data.image?.link ? (
|
||||||
>
|
<Image
|
||||||
<IconX size={20} />
|
src={data.image.link}
|
||||||
</Button>
|
alt={data.name || 'Gambar Potensi'}
|
||||||
<Button
|
w={200}
|
||||||
onClick={() => {
|
h={200}
|
||||||
if (potensiState.findUnique.data) {
|
radius="md"
|
||||||
router.push(`/admin/desa/potensi/list-potensi/${potensiState.findUnique.data.id}/edit`)
|
fit="cover"
|
||||||
}
|
/>
|
||||||
}}
|
) : (
|
||||||
disabled={!potensiState.findUnique.data}
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
color="green"
|
)}
|
||||||
>
|
</Box>
|
||||||
<IconEdit size={20} />
|
|
||||||
</Button>
|
<Box>
|
||||||
</Flex>
|
<Text fz="lg" fw="bold">Konten</Text>
|
||||||
</Box>
|
<Text
|
||||||
</Stack>
|
fz="md"
|
||||||
</Paper>
|
c="dimmed"
|
||||||
) : null}
|
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<Tooltip label="Hapus Potensi" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(data.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Potensi" withArrow position="top">
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/desa/potensi/list-potensi/${data.id}/edit`)
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text="Apakah anda yakin ingin menghapus potensi ini?"
|
text="Apakah Anda yakin ingin menghapus potensi ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|||||||
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, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -12,8 +24,6 @@ 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 CreatePotensi() {
|
function CreatePotensi() {
|
||||||
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
const potensiState = useProxy(potensiDesaState.potensiDesa);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
@@ -21,8 +31,8 @@ function CreatePotensi() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiDesaState.kategoriPotensi.findMany.load()
|
potensiDesaState.kategoriPotensi.findMany.load();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
|
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
|
||||||
@@ -59,34 +69,50 @@ function CreatePotensi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
|
</Tooltip>
|
||||||
<Stack gap="xs">
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Title order={3}>Create Potensi</Title>
|
Tambah Potensi Desa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={potensiState.create.form.name}
|
value={potensiState.create.form.name}
|
||||||
onChange={(val) => (potensiState.create.form.name = val.target.value)}
|
onChange={(val) => (potensiState.create.form.name = val.target.value)}
|
||||||
label={<Text fz="sm" fw="bold">Judul</Text>}
|
label={<Text fz="sm" fw="bold">Judul</Text>}
|
||||||
placeholder="masukkan judul"
|
placeholder="Masukkan judul potensi"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={potensiState.create.form.deskripsi}
|
value={potensiState.create.form.deskripsi}
|
||||||
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
|
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
|
||||||
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
|
||||||
placeholder="masukkan deskripsi"
|
placeholder="Masukkan deskripsi singkat"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Kategori */}
|
||||||
<Select
|
<Select
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
label={<Text fz="sm" fw="bold">Kategori</Text>}
|
||||||
placeholder='Pilih kategori'
|
placeholder="Pilih kategori"
|
||||||
value={potensiState.create.form.kategoriId || ""}
|
value={potensiState.create.form.kategoriId || ""}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
potensiState.create.form.kategoriId = val ?? "";
|
potensiState.create.form.kategoriId = val ?? "";
|
||||||
@@ -97,65 +123,58 @@ function CreatePotensi() {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Potensi
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ 'image/*': [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
<div>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
</div>
|
/>
|
||||||
</Group>
|
</Box>
|
||||||
</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>
|
||||||
|
|
||||||
|
{/* Konten Editor */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="sm" fw="bold">Konten</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Konten Lengkap
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={potensiState.create.form.content}
|
value={potensiState.create.form.content}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -164,9 +183,21 @@ function CreatePotensi() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>
|
{/* Tombol Simpan */}
|
||||||
Simpan Potensi
|
<Group justify="right">
|
||||||
</Button>
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Simpan Potensi
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Potensi() {
|
function Potensi() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title='Potensi Desa'
|
||||||
placeholder='pencarian'
|
placeholder='Cari potensi atau kategori...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -30,8 +45,8 @@ function Potensi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPotensi({ search }: { search: string }) {
|
function ListPotensi({ search }: { search: string }) {
|
||||||
const potensiState = useProxy(potensiDesaState)
|
const potensiState = useProxy(potensiDesaState);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -42,117 +57,108 @@ function ListPotensi({ search }: { search: string }) {
|
|||||||
} = potensiState.potensiDesa.findMany;
|
} = potensiState.potensiDesa.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiState.kategoriPotensi.findMany.load()
|
potensiState.kategoriPotensi.findMany.load();
|
||||||
load(page, 10)
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (potensiState.potensiDesa.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
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) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton height={300} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</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 (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Stack>
|
<Group justify="space-between" mb="md">
|
||||||
<JudulList
|
<Title order={4}>Daftar Potensi Desa</Title>
|
||||||
title='List Potensi'
|
<Tooltip label="Tambah Potensi" withArrow>
|
||||||
href='/admin/desa/potensi/list-potensi/create'
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Box style={{ overflowX: "auto" }}>
|
color="blue"
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
variant="light"
|
||||||
<TableThead>
|
onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
|
||||||
<TableTr>
|
>
|
||||||
<TableTh>Judul</TableTh>
|
Tambah Baru
|
||||||
<TableTh>Kategori</TableTh>
|
</Button>
|
||||||
<TableTh>Deskripsi</TableTh>
|
</Tooltip>
|
||||||
<TableTh>Detail</TableTh>
|
</Group>
|
||||||
</TableTr>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
</TableThead>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
<TableTbody>
|
<TableThead>
|
||||||
{filteredData.map((item) => (
|
<TableTr>
|
||||||
|
<TableTh style={{ width: '20%' }}>Judul</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
|
||||||
|
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Detail</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={100}>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box></TableTd>
|
</Text>
|
||||||
<TableTd>{item.kategori?.nama}</TableTd>
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
|
||||||
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={300}>
|
<Box w={300}>
|
||||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text
|
||||||
|
truncate
|
||||||
|
fz="sm"
|
||||||
|
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}>
|
<Button
|
||||||
<IconDeviceImacCog size={25} />
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text color="dimmed">Tidak ada data potensi yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
load(newPage, 10);
|
load(newPage, 10);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Potensi;
|
export default Potensi;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } 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';
|
||||||
|
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -12,21 +13,28 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Profile Desa",
|
label: "Profile Desa",
|
||||||
value: "profiledesa",
|
value: "profiledesa",
|
||||||
href: "/admin/desa/profile/profile-desa"
|
href: "/admin/desa/profile/profile-desa",
|
||||||
|
icon: <IconUser size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Lihat dan kelola profil desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel",
|
label: "Profile Perbekel",
|
||||||
value: "profileperbekel",
|
value: "profileperbekel",
|
||||||
href: "/admin/desa/profile/profile-perbekel"
|
href: "/admin/desa/profile/profile-perbekel",
|
||||||
|
icon: <IconUsers size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Kelola data Perbekel"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
label: "Profile Perbekel Dari Masa Ke Masa",
|
||||||
value: "profile-perbekel-dari-masa-ke-masa",
|
value: "profile-perbekel-dari-masa-ke-masa",
|
||||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"
|
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
||||||
|
icon: <IconCalendar size={18} stroke={1.8} />,
|
||||||
|
tooltip: "Riwayat Perbekel dari masa ke masa"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
const currentTab = tabs.find(tab => tab.href === pathname)
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find(t => t.value === value)
|
||||||
@@ -44,24 +52,59 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Profile Desa</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
color={colors['blue-button']}
|
||||||
{tabs.map((e, i) => (
|
variant='pills'
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
value={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
p="sm"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
|
<TabsTab
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsPanel key={i} value={e.value}>
|
{tabs.map((tab, i) => (
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
<TabsPanel
|
||||||
<></>
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsDetail;
|
export default LayoutTabsDetail;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user