Compare commits

..

20 Commits

Author SHA1 Message Date
9f9a0fb451 Sinkronisasi UI & API Admin - User Submenu Info Sekolah 2025-08-29 15:20:46 +08:00
b6d6583e77 Sinkroniasasi Admin - User, Submenu Info Sekolah Paud 2025-08-29 01:31:05 +08:00
a8fd715822 Fix UI User Menu Ekonomi & Fix UI Submenu Profile, Desa Anti Korupsi 2025-08-28 11:44:03 +08:00
f9530c32eb Fix UI User Menu PPID & Kesehatan 2025-08-27 15:39:13 +08:00
f15ef5a275 Sinkronisasi UI & API Admin - User Submenu Gotong Royong, Menu Lingkungan 2025-08-27 11:57:02 +08:00
3a726a3334 Fix Menu Lingkungan Darmasaba User 2025-08-26 17:49:33 +08:00
b21e1f0c2e Add Debounched Search Menu Ekonomi, Inovasi, Keamanan 2025-08-25 21:47:45 +08:00
f63249327d Sinkronisasi UI & API Admin - User Menu Inovasi 2025-08-25 16:40:03 +08:00
bb8dab05ba Fix API Jumlah Penganggguran 2025-08-25 11:07:21 +08:00
3081e426bd Fix Seed Jumlah Pengangguran 2025-08-23 11:39:16 +08:00
8a275c2a32 Fix Tampilan Data Kesehatan Warga User 2025-08-22 16:30:35 +08:00
8469ebd2e1 Sinkronisasi UI & API Admin - User Submenu Demografi Pekerjaan, Menu Ekonomi 2025-08-22 01:10:18 +08:00
760ba4b6d2 Sinkronisasi Sinkronisasi UI & API Admin - User Submenu Program Kemiskinan 2025-08-22 00:26:58 +08:00
20d4c90e60 Sinkronisasi UI & API Admin - User Submenu Jumlah Pengangguran - Jumlah Penduduk Miskin 2025-08-21 17:15:06 +08:00
fafbb12a08 Sinkronisasi UI & API Admin - User Submenu Pendapatan Asli Desa 2025-08-21 14:50:23 +08:00
01aa0da5cc Fix admin menu Landing page 2025-08-21 10:16:05 +08:00
b580978f8e Fix eror edit admin lowongan kerja 2025-08-20 17:52:10 +08:00
1c01397c0d Sinkronisasi UI & API Admin - User Submenu lowongan kerja lokal, Menu Ekonomi 2025-08-20 17:17:04 +08:00
90a6605efd Sinkronisasi UI & API Admin - User Submenu Pasar Desa, Menu Ekonomi 2025-08-20 17:01:20 +08:00
c22d865283 Sinkronisasi UI & API Admin - User Submenu Tips Keamanan, Menu Keamanan 2025-08-20 14:35:08 +08:00
286 changed files with 18936 additions and 9470 deletions

View File

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

View File

@@ -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"
} }
] ]

View File

@@ -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 String // 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")
} }
@@ -1547,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
@@ -1641,14 +1645,22 @@ model KolaborasiInovasi {
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])
imageId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
} }
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 ========================================= //
model InfoTekno { model InfoTekno {
id String @id @default(cuid()) id String @id @default(cuid())

View File

@@ -1,57 +1,62 @@
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 fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.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 () => {
// =========== 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 +111,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({
@@ -160,23 +249,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 +289,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 +311,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 +331,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 +351,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 +376,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,6 +396,35 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("visi misi desa success ..."); console.log("visi misi desa success ...");
// =========== MENU PPID ===========
// =========== SUBMENU PROFILE PPID ===========
for (const c of profilePPID) {
await prisma.profilePPID.upsert({
where: { id: c.id },
update: {
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-update
},
create: {
id: c.id,
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-create
},
});
}
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
// =========== SUBMENU STRUKTUR PPID ===========
// =========== POSISI ORGANISASI PPID ===========
const flattenedPosisi = posisiOrganisasiPPID.flat(); const flattenedPosisi = posisiOrganisasiPPID.flat();
// ✅ Urutkan berdasarkan hierarki // ✅ Urutkan berdasarkan hierarki
@@ -341,9 +449,9 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
create: p, create: p,
}); });
} }
console.log("✅ Posisi organisasi berhasil"); console.log("posisi organisasi berhasil");
// 2. Seed Pegawai // =========== PEGAWAI PPID ===========
const flattenedPegawai = pegawaiPPID.flat(); const flattenedPegawai = pegawaiPPID.flat();
for (const p of flattenedPegawai) { for (const p of flattenedPegawai) {
await prisma.pegawaiPPID.upsert({ await prisma.pegawaiPPID.upsert({
@@ -352,7 +460,70 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
create: p, create: p,
}); });
} }
console.log("✅ Pegawai berhasil"); console.log("pegawai berhasil");
// =========== SUBMENU VISI MISI PPID ===========
for (const v of visiMisiPPID) {
await prisma.visiMisiPPID.upsert({
where: {
id: v.id,
},
update: {
misi: v.misi,
visi: v.visi,
},
create: {
id: v.id,
misi: v.misi,
visi: v.visi,
},
});
}
console.log("visi misi PPID success ...");
// =========== SUBMENU DASAR HUKUM PPID ===========
for (const v of dasarHukumPPID) {
await prisma.dasarHukumPPID.upsert({
where: {
id: v.id,
},
update: {
judul: v.judul,
content: v.content,
},
create: {
id: v.id,
judul: v.judul,
content: v.content,
},
});
}
console.log("dasar hukum PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) {
// Convert string date to Date object
const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({
where: {
id: v.id,
},
update: {
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
create: {
id: v.id,
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
});
}
console.log("daftar informasi publik PPID success ...");
for (const l of pelayananPerizinanBerusaha) { for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({ await prisma.pelayananPerizinanBerusaha.upsert({
@@ -486,48 +657,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: {
@@ -576,24 +705,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: {
@@ -681,9 +792,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,
@@ -693,7 +807,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,

View 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='&copy; <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>
);
}

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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 {

View 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;

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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;
} }

View File

@@ -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;
} }
}, },
}, },

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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}`);
} }

View File

@@ -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: {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
} }
}, },
}, },
@@ -304,13 +338,53 @@ const lembagaPendidikan = proxy({
}; };
}> }>
> | 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) {
lembagaPendidikan.findMany.data = Array.isArray(res.data.data) ? res.data.data : [];
lembagaPendidikan.findMany.total = typeof res.data.total === 'number' ? res.data.total : 0;
lembagaPendidikan.findMany.totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
console.log('Successfully loaded lembaga data:', {
count: lembagaPendidikan.findMany.data.length,
total: lembagaPendidikan.findMany.total,
totalPages: lembagaPendidikan.findMany.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 +628,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 +907,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 +968,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 +1103,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})`
); );
} }

View File

@@ -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;

View File

@@ -143,8 +143,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
dataKey="pekerjaan" dataKey="pekerjaan"
type="stacked" type="stacked"
series={[ series={[
{ name: 'lakiLaki', color: 'red.6', label: 'Laki - Laki' }, { name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: 'orange.6', label: 'Perempuan' }, { name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]} ]}
/> />
</Box> </Box>

View File

@@ -35,7 +35,7 @@ function EditJumlahPendudukMiskin() {
// Set the ID before submitting // Set the ID before submitting
stateJPM.update.id = id; stateJPM.update.id = id;
await stateJPM.update.submit(); await stateJPM.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-miskin-2024-2025') router.push('/admin/ekonomi/jumlah-penduduk-miskin')
} }
return ( return (
<Box> <Box>

View File

@@ -32,7 +32,7 @@ function CreateJumlahPendudukMiskin() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-miskin-2024-2025"); router.push("/admin/ekonomi/jumlah-penduduk-miskin");
} }
return ( return (
<Box> <Box>

View File

@@ -91,7 +91,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Jumlah Penduduk Miskin' title='List Jumlah Penduduk Miskin'
href='/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/create' href='/admin/ekonomi/jumlah-penduduk-miskin/create'
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -108,7 +108,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<TableTd>{item.year}</TableTd> <TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd> <TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/${item.id}`)}> <Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -8,29 +8,36 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() { function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title> <HeaderSearch
<ListGrafikBerdasarkanPendidikan /> title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanPendidikan search={search}/>
</Stack> </Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanPendidikan() { function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
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 [search, setSearch] = useState("");
const handleDelete = async () => { const handleDelete = async () => {
@@ -56,11 +63,11 @@ function ListGrafikBerdasarkanPendidikan() {
const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0); const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0); const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
setDonutData([ setDonutData([
{ name: 'SD', value: SD, color: colors['blue-button'], key: 'SD' }, { name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' },
{ name: 'SMP', value: SMP, color: '#10A85AFF', key: 'SMP' }, { name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' },
{ name: 'SMA', value: SMA, color: '#C07B13FF', key: 'SMA' }, { name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' },
{ name: 'D3', value: D3, color: '#1094A8FF', key: 'D3' }, { name: 'D3', value: D3, color: '#DB524D', key: 'D3' },
{ name: 'S1', value: S1, color: '#A83610FF', key: 'S1' }, { name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
]); ]);
} }
}, [stategrafik.findMany.data]) }, [stategrafik.findMany.data])
@@ -88,13 +95,9 @@ function ListGrafikBerdasarkanPendidikan() {
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}> <Paper bg={colors['white-1']} p={"md"}>
<JudulListTab <JudulList
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
title='List Grafik Pengangguran Berdasarkan Pendidikan' title='List Grafik Pengangguran Berdasarkan Pendidikan'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create' href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>

View File

@@ -8,29 +8,37 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanUsiaKerjaYangMenganggur() { function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title> <HeaderSearch
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur /> title='Detail Data Pengangguran'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} />
</Stack> </Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur() { function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
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 [search, setSearch] = useState("");
const handleDelete = async () => { const handleDelete = async () => {
@@ -85,13 +93,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur() {
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}> <Paper bg={colors['white-1']} p={"md"}>
<JudulListTab <JudulList
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
title='List Pengangguran Berdasarkan Usia Kerja' title='List Pengangguran Berdasarkan Usia Kerja'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create' href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>

View File

@@ -66,19 +66,23 @@ function EditDetailDataPengangguran() {
const data = stateDetail.findUnique.data; const data = stateDetail.findUnique.data;
if (data) { if (data) {
// Convert year from Date to number
const yearValue = data.year instanceof Date ? data.year.getFullYear() : data.year;
// Set the ID for update // Set the ID for update
stateDetail.update.id = id; stateDetail.update.id = id;
// Isi state Valtio untuk update // Update Valtio state with converted year
stateDetail.update.form = { stateDetail.update.form = {
...data, ...data,
year: yearValue,
percentageChange: data.percentageChange || 0 // Ensure it's always a number percentageChange: data.percentageChange || 0 // Ensure it's always a number
}; };
// Isi local formData supaya input bisa dikontrol // Update local formData with converted year
setFormData({ setFormData({
month: data.month, month: data.month,
year: data.year, year: yearValue,
totalUnemployment: data.totalUnemployment, totalUnemployment: data.totalUnemployment,
educatedUnemployment: data.educatedUnemployment, educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment, uneducatedUnemployment: data.uneducatedUnemployment,

View File

@@ -25,7 +25,7 @@ function DetailJumlahPengangguran() {
stateDetail.delete.byId(selectedId) stateDetail.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran") router.push("/admin/ekonomi/jumlah-pengangguran")
} }
} }
@@ -63,7 +63,7 @@ function DetailJumlahPengangguran() {
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Tahun</Text> <Text fw={"bold"}>Tahun</Text>
<Text>{stateDetail.findUnique.data?.year}</Text> <Text>{stateDetail.findUnique.data?.year ? new Date(stateDetail.findUnique.data.year).getFullYear() : ''}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Bulan</Text> <Text fw={"bold"}>Bulan</Text>
@@ -86,7 +86,7 @@ function DetailJumlahPengangguran() {
color={"red"}> color={"red"}>
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green"> <Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Flex>
@@ -108,4 +108,3 @@ function DetailJumlahPengangguran() {
} }
export default DetailJumlahPengangguran; export default DetailJumlahPengangguran;

View File

@@ -90,7 +90,7 @@ function CreateJumlahPengangguran() {
/> />
<TextInput <TextInput
label="Tahun" label="Tahun"
type="number" type="date"
value={stateDetail.create.form.year} value={stateDetail.create.form.year}
onChange={(e) => onChange={(e) =>
(stateDetail.create.form.year = Number(e.currentTarget.value)) (stateDetail.create.form.year = Number(e.currentTarget.value))

View File

@@ -8,21 +8,29 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
import JudulListTab from '../../_com/judulListTab';
function DetailDataPengangguran() { function DetailDataPengangguran() {
const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Detail Data Pengangguran</Title> <HeaderSearch
<ListDetailDataPengangguran /> title='Detail Data Pengangguran'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDetailDataPengangguran search={search} />
</Stack> </Stack>
</Box> </Box>
); );
} }
function ListDetailDataPengangguran() { function ListDetailDataPengangguran({search}: {search: string}) {
type DetailDataPengangguran = { type DetailDataPengangguran = {
id: string; id: string;
@@ -37,7 +45,6 @@ function ListDetailDataPengangguran() {
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState("")
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true)
@@ -50,7 +57,7 @@ function ListDetailDataPengangguran() {
setChartData(stateDetail.findMany.data.map((item) => ({ setChartData(stateDetail.findMany.data.map((item) => ({
id: item.id, id: item.id,
month: item.month, month: item.month,
year: item.year, year: item.year instanceof Date ? item.year.getFullYear() : Number(item.year),
educatedUnemployment: Number(item.educatedUnemployment), educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment), uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange), percentageChange: Number(item.percentageChange),
@@ -78,13 +85,9 @@ function ListDetailDataPengangguran() {
<Box> <Box>
<Stack gap={"md"}> <Stack gap={"md"}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulListTab <JudulList
title='List Detail Data Pengangguran' title='List Detail Data Pengangguran'
href='/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/create' href='/admin/ekonomi/jumlah-pengangguran/create'
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -102,7 +105,7 @@ function ListDetailDataPengangguran() {
<TableTd>{item.educatedUnemployment}</TableTd> <TableTd>{item.educatedUnemployment}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd> <TableTd>{item.uneducatedUnemployment}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${item.id}`)}> <Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -55,17 +55,17 @@ function EditLowonganKerja() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
lowonganKerjaState.update.form = { // Set the ID for the update
...lowonganKerjaState.update.form, lowonganState.update.id = params?.id as string;
posisi: formData.posisi,
namaPerusahaan: formData.namaPerusahaan, // Update the form state
lokasi: formData.lokasi, lowonganState.update.form = {
tipePekerjaan: formData.tipePekerjaan, ...lowonganState.update.form,
gaji: formData.gaji, ...formData
deskripsi: formData.deskripsi, };
kualifikasi: formData.kualifikasi,
} // Call the update function
await lowonganState.update.update() await lowonganState.update.update();
toast.success("Lowongan kerja berhasil diperbarui!"); toast.success("Lowongan kerja berhasil diperbarui!");
router.push("/admin/ekonomi/lowongan-kerja-lokal"); router.push("/admin/ekonomi/lowongan-kerja-lokal");
} catch (error) { } catch (error) {
@@ -88,7 +88,7 @@ function EditLowonganKerja() {
<TextInput <TextInput
value={formData.posisi} value={formData.posisi}
onChange={(val) => { onChange={(val) => {
formData.posisi = val.target.value; setFormData(prev => ({ ...prev, posisi: val.target.value }));
}} }}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>} label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi' placeholder='Masukkan posisi'
@@ -96,7 +96,7 @@ function EditLowonganKerja() {
<TextInput <TextInput
value={formData.namaPerusahaan} value={formData.namaPerusahaan}
onChange={(val) => { onChange={(val) => {
formData.namaPerusahaan = val.target.value; setFormData(prev => ({ ...prev, namaPerusahaan: val.target.value }));
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>} label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan' placeholder='Masukkan nama perusahaan'
@@ -104,7 +104,7 @@ function EditLowonganKerja() {
<TextInput <TextInput
value={formData.lokasi} value={formData.lokasi}
onChange={(val) => { onChange={(val) => {
formData.lokasi = val.target.value; setFormData(prev => ({ ...prev, lokasi: val.target.value }));
}} }}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>} label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi' placeholder='Masukkan lokasi'
@@ -112,7 +112,7 @@ function EditLowonganKerja() {
<TextInput <TextInput
value={formData.tipePekerjaan} value={formData.tipePekerjaan}
onChange={(val) => { onChange={(val) => {
formData.tipePekerjaan = val.target.value; setFormData(prev => ({ ...prev, tipePekerjaan: val.target.value }));
}} }}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>} label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan' placeholder='Masukkan tipe pekerjaan'
@@ -120,7 +120,7 @@ function EditLowonganKerja() {
<TextInput <TextInput
value={formData.gaji} value={formData.gaji}
onChange={(val) => { onChange={(val) => {
formData.gaji = val.target.value; setFormData(prev => ({ ...prev, gaji: val.target.value }));
}} }}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>} label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji' placeholder='Masukkan gaji'
@@ -130,7 +130,7 @@ function EditLowonganKerja() {
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) => {
formData.deskripsi = val; setFormData(prev => ({ ...prev, deskripsi: val }));
}} }}
/> />
</Box> </Box>
@@ -139,7 +139,7 @@ function EditLowonganKerja() {
<EditEditor <EditEditor
value={formData.kualifikasi} value={formData.kualifikasi}
onChange={(val) => { onChange={(val) => {
formData.kualifikasi = val; setFormData(prev => ({ ...prev, kualifikasi: val }));
}} }}
/> />
</Box> </Box>

View File

@@ -1,6 +1,6 @@
'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 } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,20 +30,21 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
const lowonganState = useProxy(lowonganKerjaState) const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = lowonganState.findMany
useShallowEffect(() => { useShallowEffect(() => {
lowonganState.findMany.load(); load(page, 10, search)
}, []) }, [page, search])
const filteredData = (lowonganState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.posisi.toLowerCase().includes(keyword) ||
item.namaPerusahaan.toLowerCase().includes(keyword) ||
item.lokasi.toLowerCase().includes(keyword)
);
});
if (!lowonganState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -60,18 +61,24 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Bekerja Sebagai</TableTh> <TableTh>Pekerjaan</TableTh>
<TableTh>Nama Usaha</TableTh> <TableTh>Nama Perusahaan</TableTh>
<TableTh>Alamat Usaha</TableTh> <TableTh>Lokasi</TableTh>
<TableTh>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.posisi}</TableTd> <TableTd>
<TableTd>{item.namaPerusahaan}</TableTd> <Text fz={"md"}>{item.posisi}</Text>
<TableTd>{item.lokasi}</TableTd> </TableTd>
<TableTd>
<Text fz={"md"}>{item.namaPerusahaan}</Text>
</TableTd>
<TableTd>
<Text fz={"md"}>{item.lokasi}</Text>
</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}> <Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
@@ -82,6 +89,14 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,6 @@
'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 } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -13,31 +13,39 @@ import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function PasarDesa() { function PasarDesa() {
const [search, setSearch] = useState("") const [search2, setSearch2] = useState("")
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Produk' title='Kategori Produk'
placeholder='pencarian' placeholder='pencarian'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search2}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch2(e.currentTarget.value)}
/> />
<ListPasarDesa search={search} /> <ListPasarDesa search2={search2} />
</Box> </Box>
); );
} }
function ListPasarDesa({ search }: { search: string }) { function ListPasarDesa({ search2 }: { search2: string }) {
const statePasar = useProxy(pasarDesaState.kategoriProduk) const statePasar = useProxy(pasarDesaState.kategoriProduk)
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()
const {
data,
page,
totalPages,
loading,
load,
} = statePasar.findMany
useShallowEffect(() => { useShallowEffect(() => {
statePasar.findMany.load() load(page, 10, search2)
}, []) }, [page, search2])
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
@@ -47,14 +55,9 @@ function ListPasarDesa({ search }: { search: string }) {
} }
} }
const filteredData = (statePasar.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!statePasar.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -99,6 +102,14 @@ function ListPasarDesa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -1,6 +1,6 @@
'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 } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -30,21 +30,21 @@ function ListPasarDesa({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.pasarDesa) const statePasar = useProxy(pasarDesaState.pasarDesa)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = statePasar.findMany
useShallowEffect(() => { useShallowEffect(() => {
statePasar.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (statePasar.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.harga.toString().toLowerCase().includes(keyword) ||
item.rating.toString().toLowerCase().includes(keyword) ||
item.alamatUsaha.toLowerCase().includes(keyword)
);
});
if (!statePasar.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -86,6 +86,14 @@ function ListPasarDesa({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'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, Title } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -23,7 +23,7 @@ function ProgramKemiskinan() {
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListProgramKemiskinan search={search}/> <ListProgramKemiskinan search={search} />
</Box> </Box>
); );
} }
@@ -34,14 +34,22 @@ function ListProgramKemiskinan({ search }: { search: string }) {
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const {
data,
page,
totalPages,
loading,
load,
} = programState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true)
programState.findMany.load() load(page, 10, search)
}, []) }, [])
useEffect(() => { useEffect(() => {
if (programState.findMany.data) { if (data) {
const chartData = programState.findMany.data const chartData = data
.filter(item => item.statistik) .filter(item => item.statistik)
.map(item => ({ .map(item => ({
tahun: item.statistik?.tahun, tahun: item.statistik?.tahun,
@@ -52,18 +60,11 @@ function ListProgramKemiskinan({ search }: { search: string }) {
setLineChart(chartData); setLineChart(chartData);
} }
}, [programState.findMany.data]) }, [data])
const filteredData = (programState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.statistik?.tahun.toString().includes(keyword)
);
});
if (!programState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -112,7 +113,7 @@ function ListProgramKemiskinan({ search }: { search: string }) {
<Box > <Box >
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title> <Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}> {mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
<Box w={"100%"} style={{overflowX: 'auto'}}> <Box w={"100%"} style={{ overflowX: 'auto' }}>
<LineChart <LineChart
width={820} width={820}
height={300} height={300}
@@ -143,6 +144,14 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
</Box> </Box>
); );

View File

@@ -2,15 +2,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } 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';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
import CreateEditor from '../../../_com/createEditor';
function CreateSektorUnggulanDesa() { function CreateSektorUnggulanDesa() {
const stateGrafik= useProxy(grafikSektorUnggulan); const stateGrafik = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter()
@@ -54,15 +55,15 @@ function CreateSektorUnggulanDesa() {
stateGrafik.create.form.name = val.currentTarget.value; stateGrafik.create.form.name = val.currentTarget.value;
}} }}
/> />
<TextInput <Box>
label="Deskripsi Sektor Unggulan" <Text fw={"bold"} fz={"sm"}>Deskripsi Sektor Ungggulan</Text>
type="text" <CreateEditor
value={stateGrafik.create.form.description} value={stateGrafik.create.form.description}
placeholder="Masukkan deskripsi sektor unggulan" onChange={(val) => {
onChange={(val) => { stateGrafik.create.form.description = val;
stateGrafik.create.form.description = val.currentTarget.value; }}
}} />
/> </Box>
<TextInput <TextInput
label="Jumlah" label="Jumlah"
type="number" type="number"

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,7 +30,7 @@ function SektorUnggulanDesa() {
function ListSektorUnggulanDesa({ search }: { search: string }) { function ListSektorUnggulanDesa({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const state = useProxy(grafikSektorUnggulan); const state = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<{id: string; name: string; description: string | null; value: number | null}[]>([]); const [chartData, setChartData] = useState<{ id: string; name: string; description: string | null; value: number | null }[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)') const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)') const isMobile = useMediaQuery('(max-width: 768px)')
@@ -61,38 +61,41 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
}, [state.findMany.data]); }, [state.findMany.data]);
return ( return (
<Box py={10}> <Box>
<Paper bg={colors['white-1']} p={'md'}> <Stack gap={"xs"}>
<JudulList <Paper bg={colors['white-1']} p={'md'}>
title='List Sektor Unggulan Desa' <JudulList
href='/admin/ekonomi/sektor-unggulan-desa/create' title='List Sektor Unggulan Desa'
/> href='/admin/ekonomi/sektor-unggulan-desa/create'
<Table striped withTableBorder withRowBorders> />
<TableThead> <Table striped withTableBorder withRowBorders>
<TableTr> <TableThead>
<TableTh>Nama Sektor Unggulan</TableTh> <TableTr>
<TableTh>Deskripsi Sektor Unggulan</TableTh> <TableTh>Nama Sektor Unggulan</TableTh>
<TableTh>Detail</TableTh> <TableTh>Deskripsi Sektor Unggulan</TableTh>
</TableTr> <TableTh>Detail</TableTh>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.map((item) => (
</Paper> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate={"end"} fz={'sm'} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '' }}></Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */} {/* Chart */}
{!mounted && !chartData ? ( {!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
@@ -115,6 +118,7 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
</Paper> </Paper>
</Box> </Box>
)} )}
</Stack>
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,6 @@
'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, Pagination } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -29,19 +29,22 @@ function DesaDigitalSmartVillage() {
function ListDesaDigitalSmartVillage({ search }: { search: string }) { function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState) const state = useProxy(desaDigitalState)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => { useShallowEffect(() => {
state.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (state.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -68,18 +71,27 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd> <TableTd>{item.name}</TableTd>
<TableTd> <TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}> <Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,6 @@
'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, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -29,19 +29,22 @@ function InfoTeknologiTepatGuna() {
function ListInfoTeknologiTepatGuna({ search }: { search: string }) { function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState) const state = useProxy(infoTeknoState)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => { useShallowEffect(() => {
state.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (state.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -68,17 +71,25 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd> <TableTd>{item.name}</TableTd>
<TableTd> <TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}> <Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))}
</TableTbody> </TableTbody>
</Table> </Table>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</Paper> </Paper>
</Box> </Box>
); );

View File

@@ -0,0 +1,62 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Kolaborasi Inovasi</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsKolaborasi;

View File

@@ -1,155 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
import { useState } from 'react';
import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
stateCreate.create.form = {
name: "",
tahun: 0,
slug: "",
deskripsi: "",
kolaborator: "",
imageId: "",
}
setPreviewImage(null);
setFile(null);
}
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Simpan ID gambar ke form
stateCreate.create.form.imageId = uploaded.id;
// Submit data berita
await stateCreate.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Kolaborasi Inovasi</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
placeholder="masukkan nama kolaborasi inovasi"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<TextInput
type='number'
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
onChange={(val) => stateCreate.create.form.tahun = parseInt(val.target.value)}
/>
<TextInput
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Kolaborasi Inovasi</Text>}
placeholder='Masukkan deskripsi singkat kolaborasi inovasi'
/>
<TextInput
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
placeholder='Masukkan kolaborator'
/>
<Box>
<Stack gap={"xs"}>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const newImages = files.map((file) => ({
file,
preview: URL.createObjectURL(file),
label: '',
}));
setFile(newImages[0].file);
setPreviewImage(newImages[0].preview); // ← ini yang kurang
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
</Box>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
</Stack>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateProgramKreatifDesa;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import LayoutTabsKolaborasi from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsKolaborasi>
{children}
</LayoutTabsKolaborasi>
);
}
export default Layout;

View File

@@ -2,41 +2,34 @@
"use client"; "use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor"; import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi";
import colors from "@/con/colors"; import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { import {
Box, Box,
Button, Button,
Center,
FileInput,
Image,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } 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 kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi";
function EditKolaborasiInovasi() { function EditKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState); const kolaborasiState = useProxy(kolaborasiInovasiState);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || '', name: kolaborasiState.update.form.name || '',
deskripsi: kolaborasiState.update.form.deskripsi || '', deskripsi: kolaborasiState.update.form.deskripsi || '',
tahun: kolaborasiState.update.form.tahun || '', tahun: kolaborasiState.update.form.tahun || '',
slug: kolaborasiState.update.form.slug || '', slug: kolaborasiState.update.form.slug || '',
kolaborator: kolaborasiState.update.form.kolaborator || '', kolaborator: kolaborasiState.update.form.kolaborator || '',
imageId: kolaborasiState.update.form.imageId || ''
}); });
// Load berita by id saat pertama kali // Load berita by id saat pertama kali
@@ -54,13 +47,7 @@ function EditKolaborasiInovasi() {
tahun: data.tahun || '', tahun: data.tahun || '',
slug: data.slug || '', slug: data.slug || '',
kolaborator: data.kolaborator || '', kolaborator: data.kolaborator || '',
imageId: data.imageId || '',
}); });
if (data.image) {
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} }
} catch (error) { } catch (error) {
console.error("Error loading berita:", error); console.error("Error loading berita:", error);
@@ -82,22 +69,7 @@ function EditKolaborasiInovasi() {
tahun: Number(formData.tahun), tahun: Number(formData.tahun),
slug: formData.slug, slug: formData.slug,
kolaborator: formData.kolaborator, kolaborator: formData.kolaborator,
imageId: formData.imageId // Keep existing imageId if not changed
}; };
// Jika ada file baru, upload
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
kolaborasiState.update.form.imageId = uploaded.id;
}
await kolaborasiState.update.submit(); await kolaborasiState.update.submit();
toast.success("Berita berhasil diperbarui!"); toast.success("Berita berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi"); router.push("/admin/inovasi/kolaborasi-inovasi");
@@ -144,28 +116,6 @@ function EditKolaborasiInovasi() {
label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>} label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>}
placeholder="masukkan kolaborator" placeholder="masukkan kolaborator"
/> />
<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 fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor <EditEditor

View File

@@ -1,15 +1,15 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
function DetailKolaborasiInovasi() { function DetailKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState) const kolaborasiState = useProxy(kolaborasiInovasiState)
@@ -69,10 +69,6 @@ function DetailKolaborasiInovasi() {
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} /> <Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} />
</Box> </Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={kolaborasiState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Kolaborator</Text> <Text fw={"bold"} fz={"lg"}>Kolaborator</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text> <Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text>

View File

@@ -0,0 +1,113 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { YearPickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
tahun: 0,
slug: "",
deskripsi: "",
kolaborator: "",
}
}
// Generate slug from name
useEffect(() => {
const { name } = stateCreate.create.form;
if (name) {
const slug = name
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
stateCreate.create.form.slug = slug;
}
}, [stateCreate.create.form.name]);
const handleSubmit = async () => {
try {
// Submit data kolaborasi inovasi
await stateCreate.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi");
toast.success("Berhasil menambahkan kolaborasi inovasi");
} catch (error) {
console.error("Error creating kolaborasi inovasi:", error);
toast.error("Terjadi kesalahan saat menyimpan data");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Kolaborasi Inovasi</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
placeholder="masukkan nama kolaborasi inovasi"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<YearPickerInput
clearable
value={stateCreate.create.form.tahun ? new Date(stateCreate.create.form.tahun, 0, 1) : null}
label="Tahun"
placeholder="Pilih tahun"
onChange={(dateString: string) => {
const year = dateString ? new Date(dateString).getFullYear() : 0;
stateCreate.create.form.tahun = year;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(val) => {
stateCreate.create.form.deskripsi = val;
}}
/>
</Box>
<TextInput
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
placeholder='Masukkan kolaborator'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateProgramKreatifDesa;

View File

@@ -3,11 +3,11 @@
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 { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../../_com/judulList';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import kolaborasiInovasiState from '../../_state/inovasi/kolaborasi-inovasi'; import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function KolaborasiInovasi() { function KolaborasiInovasi() {
@@ -28,22 +28,21 @@ function KolaborasiInovasi() {
function ListKolaborasiInovasi({ search }: { search: string }) { function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState) const listState = useProxy(kolaborasiInovasiState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter(); const router = useRouter();
useEffect(() => { const {
load(page, 10) data,
}, [page]) loading,
page,
totalPages,
load,
} = listState.findMany
const filteredData = (data || []).filter(item => { useEffect(() => {
const keyword = search.toLowerCase(); load(page, 10, search)
return ( }, [page, search])
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) || const filteredData = data || []
item.slug.toLowerCase().includes(keyword) ||
item.kolaborator.toLowerCase().includes(keyword)
);
});
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -64,11 +63,11 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '2%', textAlign: 'center' }}>No</TableTh> <TableTh>No</TableTh>
<TableTh style={{ width: '15%' }}>Nama Kolaborasi Inovasi</TableTh> <TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Tahun</TableTh> <TableTh>Tahun</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi Singkat</TableTh> <TableTh>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
</Table> </Table>
@@ -89,21 +88,21 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '1%', textAlign: 'center' }}>No</TableTh> <TableTh>No</TableTh>
<TableTh style={{ width: '15%' }}>Nama Kolaborasi Inovasi</TableTh> <TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Tahun</TableTh> <TableTh>Tahun</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi Singkat</TableTh> <TableTh>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '1%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTd>{index + 1}</TableTd>
<TableTd style={{ width: '15%' }}>{item.name}</TableTd> <TableTd>{item.name}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{item.tahun}</TableTd> <TableTd>{item.tahun}</TableTd>
<TableTd style={{ width: '20%' }}>{item.slug}</TableTd> <TableTd>{item.slug}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}> <TableTd>
<Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}> <Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
@@ -116,17 +115,13 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => load(newPage)} // ini penting!
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
/> />
</Center> </Center>
</Box >
</Box>
); );
} }

View File

@@ -0,0 +1,148 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditFoto() {
const state = useProxy(mitraKolaborasi)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: state.update.form.name || '',
imageId: state.update.form.imageId || ''
});
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
setFormData({
name: data.name || '',
imageId: data.imageId || ''
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
toast.error('Gagal memuat data foto');
}
};
loadFoto();
}, [params?.id]);
const handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
name: formData.name,
imageId: formData.imageId
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
state.update.form.imageId = uploaded.id;
}
await state.update.update();
toast.success('Mitra berhasil diperbarui!');
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
} catch (error) {
console.error('Error updating mitra:', error);
toast.error('Terjadi kesalahan saat memperbarui mitra');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Mitra</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
placeholder='Masukkan nama mitra'
value={formData.name}
onChange={(e) =>
(formData.name = e.target.value)
}
/>
<Box>
<Text>Upload Foto</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditFoto;

View File

@@ -0,0 +1,136 @@
'use client'
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateFoto() {
const state = useProxy(mitraKolaborasi)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
state.create.form = {
name: "",
imageId: "",
};
setPreviewImage(null)
setFile(null)
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Mitra</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
placeholder='Masukkan nama mitra'
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.target.value;
}}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateFoto;

View File

@@ -0,0 +1,187 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function MitraKolaborasi() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Mitra Kolaborasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListMitraKolaborasi search={search} />
</Box>
);
}
function ListMitraKolaborasi({ search }: { search: string }) {
const listState = useProxy(mitraKolaborasi)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const handleHapus = () => {
if (selectedId) {
mitraKolaborasi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/kolaborasi-inovasi")
}
}
const {
data,
loading,
page,
totalPages,
load,
} = listState.findMany
useEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data mitra kolaborasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box style={{
width: 100,
height: 100,
position: 'relative',
overflow: 'hidden',
borderRadius: 4
}}>
<Image
src={item.image?.link || ''}
alt={item.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center'
}}
/>
</Box>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={mitraKolaborasi.delete.loading || !item}
color={"red"}
>
<IconX size={20} />
</Button>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
router.push(`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`);
}
}}
disabled={!item}
color={"green"}
>
<IconEdit size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus mitra kolaborasi ini?'
/>
</Box >
);
}
export default MitraKolaborasi;

View File

@@ -1,6 +1,6 @@
'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, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,19 +30,21 @@ function ListTipsKeamanan({ search }: { search: string }) {
const stateKeamanan = useProxy(tipsKeamananState) const stateKeamanan = useProxy(tipsKeamananState)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateKeamanan.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateKeamanan.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (stateKeamanan.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!stateKeamanan.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -67,9 +69,15 @@ function ListTipsKeamanan({ search }: { search: string }) {
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.judul}</TableTd>
<TableTd> <TableTd>
<Text fz={"xs"} dangerouslySetInnerHTML={{__html: item.deskripsi}} /> <Box w={150}>
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.judul}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={250}>
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{__html: item.deskripsi}} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}> <Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}>
@@ -81,6 +89,14 @@ function ListTipsKeamanan({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -51,7 +51,7 @@ function DetailFasilitasKesehatan() {
<GridCol span={12}> <GridCol span={12}>
<Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text> <Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text>
</GridCol> </GridCol>
<GridCol span={12}> {/* <GridCol span={12}>
<Flex gap={"xs"}> <Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}> <Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}>
Tambah Dokter Tambah Dokter
@@ -60,7 +60,7 @@ function DetailFasilitasKesehatan() {
Tambah Layanan Tambah Layanan
</Button> </Button>
</Flex> </Flex>
</GridCol> </GridCol> */}
</Grid> </Grid>
{stateFasilitasKesehatan.findUnique.data ? ( {stateFasilitasKesehatan.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}> <Paper bg={colors['BG-trans']} p={'md'}>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { ActionIcon, Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, 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 JudulList from '../../_com/judulList';
@@ -30,19 +30,22 @@ function APBDes() {
function ListAPBDes({ search }: { search: string }) { function ListAPBDes({ search }: { search: string }) {
const listState = useProxy(apbdes) const listState = useProxy(apbdes)
const router = useRouter(); const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => { const {
const keyword = search.toLowerCase(); data,
return ( page,
item.name.toLowerCase().includes(keyword) || totalPages,
item.jumlah.toLowerCase().includes(keyword) loading,
) load,
}); } = listState.findMany
if (!listState.findMany.data) { useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -88,7 +91,7 @@ function ListAPBDes({ search }: { search: string }) {
rel="noopener noreferrer" rel="noopener noreferrer"
variant='transparent' variant='transparent'
> >
<IconFile size={25} color={colors['blue-button']}/> <IconFile size={25} color={colors['blue-button']} />
</ActionIcon> </ActionIcon>
) : ( ) : (
<Text>Tidak ada dokumen tersedia</Text> <Text>Tidak ada dokumen tersedia</Text>
@@ -106,6 +109,14 @@ function ListAPBDes({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -1,60 +1,101 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconList, IconCategory } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "List Desa Anti Korupsi", label: "List Desa Anti Korupsi",
value: "listDesaAntiKorupsi", value: "listDesaAntiKorupsi",
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi" href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi",
icon: <IconList size={18} stroke={1.8} />,
tooltip: "Kelola daftar program desa anti korupsi",
}, },
{ {
label: "Kategori Desa Anti Korupsi", label: "Kategori Desa Anti Korupsi",
value: "kategoriDesaAntiKorupsi", value: "kategoriDesaAntiKorupsi",
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi" href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori desa anti korupsi",
}, },
]; ];
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}>Desa Anti Korupsi</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Desa Anti Korupsi
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
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>
<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>
); );
} }

View File

@@ -2,14 +2,14 @@
'use client' 'use client'
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
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';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriDesaAntiKorupsi() { export default function EditKategoriDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const id = params?.id as string; const id = params?.id as string;
@@ -18,16 +18,17 @@ function EditKategoriDesaAntiKorupsi() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
}); });
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const loadKategorikegiatan = async () => { const loadKategori = async () => {
if (!id) return; if (!id) return;
setIsLoading(true);
try { try {
const data = await stateKategori.edit.load(id); const data = await stateKategori.edit.load(id);
if (data) { if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id; stateKategori.edit.id = id;
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
@@ -36,63 +37,88 @@ function EditKategoriDesaAntiKorupsi() {
} catch (error) { } catch (error) {
console.error("Error loading kategori desa anti korupsi:", error); console.error("Error loading kategori desa anti korupsi:", error);
toast.error("Gagal memuat data kategori desa anti korupsi"); toast.error("Gagal memuat data kategori desa anti korupsi");
} finally {
setIsLoading(false);
} }
}; };
loadKategorikegiatan(); loadKategori();
}, [id]); }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { if (!formData.name.trim()) {
if (!formData.name.trim()) { return toast.error('Nama kategori tidak boleh kosong');
toast.error('Nama kategori desa anti korupsi tidak boleh kosong'); }
return;
}
try {
setIsLoading(true);
stateKategori.edit.form = { stateKategori.edit.form = {
name: formData.name.trim(), name: formData.name.trim(),
}; };
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) { if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback stateKategori.edit.id = id;
} }
const success = await stateKategori.edit.update(); await stateKategori.edit.update();
toast.success('Kategori berhasil diperbarui');
if (success) { router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
}
} catch (error) { } catch (error) {
console.error("Error updating kategori desa anti korupsi:", error); console.error("Error updating kategori desa anti korupsi:", error);
// toast akan ditampilkan dari fungsi update toast.error(error instanceof Error ? error.message : 'Gagal memperbarui kategori');
} finally {
setIsLoading(false);
} }
}; };
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Edit Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Kategori Desa Anti Korupsi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>} required
placeholder='Masukkan nama kategori desa anti korupsi' disabled={isLoading}
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
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>
</Box> </Box>
); );
} }
export default EditKategoriDesaAntiKorupsi;

View File

@@ -1,16 +1,16 @@
/* 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, 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 { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
function CreateKategoriDesaAntiKorupsi() { export default function CreateKategoriDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi) const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
useEffect(() => { useEffect(() => {
stateKategori.findMany.load(); stateKategori.findMany.load();
@@ -20,42 +20,64 @@ function CreateKategoriDesaAntiKorupsi() {
stateKategori.create.form = { stateKategori.create.form = {
name: "", name: "",
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!stateKategori.create.form.name) {
return alert('Nama kategori harus diisi');
}
await stateKategori.create.create(); await stateKategori.create.create();
resetForm(); resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi") router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box> <Group mb="md">
<Box mb={10}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Kategori Desa Anti Korupsi</Title> bg={colors['white-1']}
<TextInput p="lg"
value={stateKategori.create.form.name} radius="md"
onChange={(val) => { shadow="sm"
stateKategori.create.form.name = val.target.value; style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Kategori"
placeholder="Masukkan nama kategori"
value={stateKategori.create.form.name || ''}
onChange={(e) => (stateKategori.create.form.name = e.target.value)}
required
/>
<Group justify="right" mt="md">
<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)',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>} >
placeholder='Masukkan nama kategori desa anti korupsi' Simpan
/> </Button>
<Group> </Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> </Stack>
</Group> </Paper>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }
export default CreateKategoriDesaAntiKorupsi;

View File

@@ -1,13 +1,12 @@
/* 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 } 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 { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
@@ -50,88 +49,90 @@ function ListKategoriKegiatan({ search }: { search: string }) {
} }
} }
useEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, search)
}, [page]) }, [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)
);
})
}, [data, search]);
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kategori Kegiatan'
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Kategori Kegiatan' <Title order={4}>Daftar Kategori Kegiatan</Title>
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create' <Tooltip label="Tambah Kategori" withArrow>
/> <Button
<Box style={{ overflowY: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Kategori</TableTh> <TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh> <TableTh>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd> <TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}> <Text fw={500}>{item.name}</Text>
<IconEdit size={20} /> </TableTd>
</Button> <TableTd>
</TableTd> <Tooltip label="Edit" withArrow>
<TableTd> <Button
<Button color="red" onClick={() => { variant="light"
setSelectedId(item.id) color="blue"
setModalHapus(true) size="sm"
}}> onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
<IconX size={20} /> >
</Button> <IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={2}>
<Center py={20}>
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
@@ -141,11 +142,13 @@ function ListKategoriKegiatan({ search }: { search: string }) {
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>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}

View File

@@ -1,18 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import { useProxy } from 'valtio/utils'; import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, 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 { useProxy } from 'valtio/utils';
import colors from '@/con/colors'; import colors from '@/con/colors';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
interface FormDesaAntiKorupsi { interface FormDesaAntiKorupsi {
@@ -22,18 +20,20 @@ interface FormDesaAntiKorupsi {
fileId: string; fileId: string;
} }
function EditDesaAntiKorupsi() { export default function EditDesaAntiKorupsi() {
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi) const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null); const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const params = useParams() const [isLoading, setIsLoading] = useState(false);
const router = useRouter() const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({ const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
name: '', name: '',
deskripsi: '', deskripsi: '',
kategoriId: '', kategoriId: '',
fileId: '', fileId: '',
}) });
useEffect(() => { useEffect(() => {
const loadDesaAntiKorupsi = async () => { const loadDesaAntiKorupsi = async () => {
@@ -43,7 +43,6 @@ function EditDesaAntiKorupsi() {
try { try {
const data = await desaAntiKorupsiState.edit.load(id); const data = await desaAntiKorupsiState.edit.load(id);
if (data) { if (data) {
// ⬇️ FIX PENTING: tambahkan ini
desaAntiKorupsiState.edit.id = id; desaAntiKorupsiState.edit.id = id;
desaAntiKorupsiState.edit.form = { desaAntiKorupsiState.edit.form = {
@@ -61,169 +60,198 @@ function EditDesaAntiKorupsi() {
}); });
if (data?.file?.link) { if (data?.file?.link) {
setPreviewFile(data.file.link) setPreviewFile(data.file.link);
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading program penghijauan:", error); console.error('Error loading data:', error);
toast.error("Gagal memuat data program penghijauan"); toast.error('Gagal memuat data Desa Anti Korupsi');
} }
} };
loadDesaAntiKorupsi(); loadDesaAntiKorupsi();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formData.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!formData.kategoriId) {
return toast.warn('Pilih kategori dokumen');
}
setIsLoading(true);
try { try {
// Update global state with form data // Update global state with form data
desaAntiKorupsiState.edit.form = { desaAntiKorupsiState.edit.form = {
...desaAntiKorupsiState.edit.form, ...desaAntiKorupsiState.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId || '', kategoriId: formData.kategoriId || '',
fileId: formData.fileId // Keep existing imageId if not changed
}; };
// Jika ada file baru, upload // Upload new file if exists
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"); throw new Error('Gagal mengunggah dokumen');
} }
// Update imageId in global state
desaAntiKorupsiState.edit.form.fileId = uploaded.id; desaAntiKorupsiState.edit.form.fileId = uploaded.id;
} }
await desaAntiKorupsiState.edit.update(); await desaAntiKorupsiState.edit.update();
toast.success("desa anti korupsi berhasil diperbarui!"); toast.success('Data berhasil diperbarui');
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"); router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) { } catch (error) {
console.error("Error updating desa anti korupsi:", error); console.error('Error updating data:', error);
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi"); toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsLoading(false);
} }
}; };
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" color="blue" 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 w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> </Tooltip>
<Stack> <Title order={4} ml="sm" c="dark">
<Text fz={"xl"} fw={"bold"}>Edit List Desa Anti Korupsi</Text> Edit Desa Anti Korupsi
{desaAntiKorupsiState.findUnique.data ? ( </Title>
<Paper key={desaAntiKorupsiState.findUnique.data.id}> </Group>
<Stack gap={"xs"}>
<TextInput <Paper
value={formData.name} w={{ base: '100%', md: '50%' }}
onChange={(val) => { bg={colors['white-1']}
setFormData({ p="lg"
...formData, radius="md"
name: val.target.value shadow="sm"
}) style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Judul Dokumen"
placeholder="Masukkan judul dokumen"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<Select
label="Kategori"
placeholder="Pilih kategori"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || '' })}
data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
required
searchable
clearable
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Dokumen
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2}
accept={{
'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
}}
radius="md"
>
<Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
<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>
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="lg" inline>
Seret dokumen ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
</Text>
</div>
</Group>
</Dropzone>
{previewFile && (
<Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Pratinjau Dokumen
</Text>
<Box
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
height: '500px',
width: '100%',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} >
placeholder='Masukkan judul' <iframe
/> src={previewFile}
<Box> width="100%"
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> height="100%"
<EditEditor style={{ border: 'none' }}
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val
})
}}
/> />
</Box> </Box>
<Select </Box>
value={formData.kategoriId} )}
onChange={(val) => { </Box>
setFormData({
...formData,
kategoriId: val ?? ""
})
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori"
data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
/>
<Box>
<Text fz={"md"} fw={"bold"}>File Document</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.pdf', '.doc', '.docx'],
}}
>
<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>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <Group justify="right" mt="xl">
<Text size="xl" inline> <Button
Drag file ke sini atau klik untuk pilih file onClick={handleSubmit}
</Text> radius="md"
<Text size="sm" c="dimmed" inline mt={7}> size="md"
Maksimal 5MB dan harus format document loading={isLoading}
</Text> style={{
</div> background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
</Group> color: '#fff',
</Dropzone> boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
<Box> }}
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> >
{previewFile ? ( Simpan
<iframe </Button>
src={previewFile} </Group>
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
</Box>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
) : null}
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditDesaAntiKorupsi;

View File

@@ -2,15 +2,15 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
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 DetailKegiatanDesa() { export default function DetailKegiatanDesa() {
const detailState = useProxy(korupsiState.desaAntikorupsi) const detailState = useProxy(korupsiState.desaAntikorupsi)
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -34,89 +34,122 @@ function DetailKegiatanDesa() {
if (!detailState.findUnique.data) { if (!detailState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = detailState.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 List Desa Anti Korupsi</Text> </Button>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: "100%", md: "50%" }}
<Box> bg={colors['white-1']}
<Text fw={"bold"} fz={"lg"}>Judul</Text> p="lg"
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text> radius="md"
</Box> shadow="sm"
<Box> >
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Stack gap="md">
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} /> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
</Box> Detail Desa Anti Korupsi
<Box> </Text>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
</Box> <Stack gap="md">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text> <Text fz="lg" fw="bold">Judul</Text>
{detailState.findUnique.data?.file?.link ? ( <Text fz="md" c="dimmed">{data.name || '-'}</Text>
<iframe </Box>
src={detailState.findUnique.data.file.link}
width="100%" <Box>
height="500px" <Text fz="lg" fw="bold">Kategori</Text>
style={{ border: "1px solid #ccc", borderRadius: "8px" }} <Text fz="md" c="dimmed">{data.kategori?.name || '-'}</Text>
/> </Box>
) : (
<Text>Tidak ada dokumen tersedia</Text> <Box>
)} <Text fz="lg" fw="bold" mb="xs">Deskripsi</Text>
</Box> <Box
<Flex gap={"xs"} mt={10}> fz="md"
<Button c="dimmed"
onClick={() => { dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
if (detailState.findUnique.data) { style={{ lineHeight: 1.6 }}
setSelectedId(detailState.findUnique.data.id); />
setModalHapus(true); </Box>
}
<Box>
<Text fz="lg" fw="bold" mb="xs">Dokumen</Text>
{data.file?.link ? (
<Box
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
height: '500px',
width: '100%'
}} }}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
> >
<IconX size={20} /> <iframe
</Button> src={data.file.link}
width="100%"
height="100%"
style={{ border: 'none' }}
/>
</Box>
) : (
<Text fz="sm" c="dimmed">Tidak ada dokumen tersedia</Text>
)}
</Box>
<Group gap="sm" mt="md">
<Tooltip label="Hapus Data" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { setSelectedId(data.id);
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`); setModalHapus(true);
}
}} }}
disabled={!detailState.findUnique.data} variant="light"
color={"green"} radius="md"
size="md"
disabled={detailState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${data.id}/edit`)}
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 desa anti korupsi ini?' text="Apakah Anda yakin ingin menghapus data Desa Anti Korupsi ini?"
/> />
</Box> </Box>
); );
} }
export default DetailKegiatanDesa;

View File

@@ -1,10 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
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, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -13,12 +24,12 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateDesaAntiKorupsi() { export default function CreateDesaAntiKorupsi() {
const router = useRouter(); const router = useRouter();
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi) const stateKorupsi = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null); const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
stateKorupsi.findMany.load(); stateKorupsi.findMany.load();
@@ -27,140 +38,181 @@ function CreateDesaAntiKorupsi() {
const resetForm = () => { const resetForm = () => {
stateKorupsi.create.form = { stateKorupsi.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
kategoriId: "", kategoriId: '',
fileId: "", fileId: '',
}; };
setFile(null); setFile(null);
setPreviewFile(null); setPreviewFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file pdf terlebih dahulu"); return toast.warn('Pilih file dokumen terlebih dahulu');
}
if (!stateKorupsi.create.form.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!stateKorupsi.create.form.kategoriId) {
return toast.warn('Pilih kategori dokumen');
} }
const res = await ApiFetch.api.fileStorage.create.post({ setIsLoading(true);
file, try {
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 mengupload file"); throw new Error('Gagal mengunggah dokumen');
}
stateKorupsi.create.form.fileId = uploaded.id;
await stateKorupsi.create.create();
toast.success('Data berhasil disimpan');
resetForm();
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) {
console.error('Error:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
} finally {
setIsLoading(false);
} }
};
stateKorupsi.create.form.fileId = uploaded.id;
await stateKorupsi.create.create();
resetForm();
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
}
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Tambah Dokumen Desa Anti Korupsi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Kegiatan Desa</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>File Document</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Dokumen
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewFile(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{
'application/*': ['.pdf', '.doc', '.docx'],
}}
>
<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>
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag file ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format document
</Text>
</div>
</Group>
</Dropzone>
<Box>
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
{previewFile ? (
<iframe
src={previewFile}
width="100%"
height="500px"
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
/>
) : (
<Text>Tidak ada dokumen tersedia</Text>
)}
</Box>
</Box>
</Box>
<TextInput
value={stateKorupsi.create.form.name}
onChange={(val) => {
stateKorupsi.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateKorupsi.create.form.deskripsi}
onChange={(val) => {
stateKorupsi.create.form.deskripsi = val;
}} }}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2}
accept={{
'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
}}
radius="md"
>
<Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
<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>
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="lg" inline>
Seret dokumen ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
</Text>
</div>
</Group>
</Dropzone>
{previewFile && (
<Box mt="md" style={{ textAlign: 'center' }}>
<iframe
src={previewFile}
width="100%"
height="500px"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
maxWidth: '100%',
}}
/>
</Box>
)}
</Box>
<TextInput
label="Judul Dokumen"
placeholder="Masukkan judul dokumen"
value={stateKorupsi.create.form.name || ''}
onChange={(e) => (stateKorupsi.create.form.name = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateKorupsi.create.form.deskripsi || ''}
onChange={(val) => (stateKorupsi.create.form.deskripsi = val)}
/> />
</Box> </Box>
<Select <Select
value={stateKorupsi.create.form.kategoriId} label="Kategori"
onChange={(val) => {
stateKorupsi.create.form.kategoriId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={stateKorupsi.create.form.kategoriId || ''}
onChange={(val) => (stateKorupsi.create.form.kategoriId = val || '')}
data={ data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({ korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name, label: v.name,
})) || [] })) || []
} }
required
searchable
clearable
/> />
<Group> <Group justify="right" mt="xl">
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Button
onClick={handleSubmit}
radius="md"
size="md"
loading={isLoading}
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>
</Box> </Box>
); );
} }
export default CreateDesaAntiKorupsi;

View File

@@ -1,13 +1,12 @@
/* 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 { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
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 { 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 korupsiState from '../../../_state/landing-page/desa-anti-korupsi'; import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
function DesaAntiKorupsi() { function DesaAntiKorupsi() {
@@ -16,7 +15,7 @@ function DesaAntiKorupsi() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Desa Anti Korupsi' title='List Desa Anti Korupsi'
placeholder='pencarian' placeholder='Cari nama program 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,8 +26,8 @@ function DesaAntiKorupsi() {
} }
function ListDesaAntiKorupsi({ search }: { search: string }) { function ListDesaAntiKorupsi({ search }: { search: string }) {
const listState = useProxy(korupsiState.desaAntikorupsi)
const router = useRouter(); const router = useRouter();
const listState = useProxy(korupsiState.desaAntikorupsi);
const { const {
data, data,
@@ -38,114 +37,100 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
load, load,
} = listState.findMany; } = listState.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10); load(page, 10, search);
}, [page]); }, [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) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => b.createdAt - a.createdAt);
}, [data, search]);
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Desa Anti Korupsi'
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Program Desa Anti Korupsi</Title>
title='List Desa Anti Korupsi' <Tooltip label="Tambah Program Desa Anti Korupsi" withArrow>
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/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/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
<TableTr> >
<TableTh>Nama Desa Anti Korupsi</TableTh> Tambah Baru
<TableTh>Deskripsi Desa Anti Korupsi</TableTh> </Button>
<TableTh>Kategori Desa Anti Korupsi</TableTh> </Tooltip>
<TableTh>Detail</TableTh> </Group>
</TableTr> <Box style={{ overflowX: "auto" }}>
</TableThead> <Table highlightOnHover>
<TableTbody> <TableThead>
{filteredData.map((item) => ( <TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Box w={350}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <Text lineClamp={1} fw={500}>{item.name || '-'}</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text fz="sm" c="dimmed">
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> {item.kategori?.name || '-'}
</Box> </Text>
</TableTd> </TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${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 c="dimmed">Tidak ada data program 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 DesaAntiKorupsi; export default DesaAntiKorupsi;

View File

@@ -0,0 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Grafik Kepuasan Masyarakat",
value: "grafikkepuasannamasyarakat",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
{
label: "Responden",
value: "responden",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/responden"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Indeks Kepuasan Masyarakat</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsKepuasan;

View File

@@ -0,0 +1,256 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import colors from '@/con/colors';
import { PieChart, BarChart } from '@mantine/charts'; // ✅ Ganti recharts dengan Mantine
import {
Box,
Center,
Flex,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
interface ChartDataItem {
name: string;
value: number;
color: string;
label?: string;
}
function Page() {
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
useShallowEffect(() => {
if (!data && !loading) {
state.findMany.load();
return;
}
if (data) {
// Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
// Hitung total berdasarkan rating
const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
// Hitung total berdasarkan kelompok umur
const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
// Update gender chart data
setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]);
// Update rating chart data
setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' },
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
]);
// Update age group chart data
setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' },
]);
// Process data for bar chart (group by month)
const monthYearMap = new Map<string, number>();
data.forEach((item: any) => {
// Try both createdAt and tanggal fields
const dateValue = item.tanggal || item.createdAt;
if (!dateValue) return;
const parsedDate = new Date(dateValue);
if (isNaN(parsedDate.getTime())) return;
const month = parsedDate.getMonth() + 1;
const year = parsedDate.getFullYear();
const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
});
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => {
const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' });
return {
month: `${monthName} ${year}`,
count,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
};
})
.sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count }));
setBarChartData(barData);
}
}, [data]);
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Stack py={10}>
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan
</Text>
</Stack>
);
}
return (
<Stack gap="xs">
{/* Bar Chart - Data per Tanggal */}
<Paper bg={colors['white-1']} p="md" radius="md" mb="md">
<Title order={4} mb="md" ta="center">Jumlah Responden per Bulan</Title>
<Box h={300}>
<BarChart
h={300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Box>
</Paper>
<SimpleGrid cols={{ base: 1, md: 3 }}>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Box>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={250}
data={donutDataJenisKelamin}
/>
</Center>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
)}
</Stack>
</Paper>
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Pilihan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Box>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={250}
data={donutDataRating}
/>
</Center>
<Stack gap="sm" mt="md">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
)}
</Stack>
</Paper>
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Box>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={250}
data={donutDataKelompokUmur}
/>
</Center>
<Stack gap="sm" mt="md">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
)}
</Stack>
</Paper>
</SimpleGrid>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import LayoutTabsKepuasan from './_lib/layoutTab';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsKepuasan>
{children}
</LayoutTabsKepuasan>
);
}
export default Layout;

View File

@@ -0,0 +1,190 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Text, Select } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import { toast } from 'react-toastify';
interface FormResponden {
name: string;
tanggal: string;
jenisKelaminId: string;
ratingId: string;
kelompokUmurId: string;
}
function EditResponden() {
const router = useRouter()
const params = useParams() as { id: string }
const state = useProxy(indeksKepuasanState.responden)
const id = params.id
const [formData, setFormData] = useState<FormResponden>({
name: '',
tanggal: '',
jenisKelaminId: '',
ratingId: '',
kelompokUmurId: '',
})
useEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load();
indeksKepuasanState.pilihanRatingResponden.findMany.load();
indeksKepuasanState.kelompokUmurResponden.findMany.load();
const loadResponden = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
state.update.id = id;
state.update.form = {
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
};
setFormData({
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
});
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
}
}
loadResponden();
}, [params?.id]);
const handleSubmit = async () => {
state.update.id = id;
state.update.form = { ...formData }; // <-- sinkronisasi manual
await state.update.submit();
router.push('/admin/ppid/ikm-desa-darmasaba/responden')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Responden</Title>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.currentTarget.value
})
}}
/>
<TextInput
label="Tanggal"
type="date"
placeholder='Pilih tanggal'
value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
onChange={(e) => {
const selectedDate = e.currentTarget.value;
setFormData({
...formData,
tanggal: selectedDate,
});
}}
/>
<Select
key={"jenisKelamin"}
label={<Text fw="bold" fz="sm">Jenis Kelamin</Text>}
placeholder="Pilih jenis kelamin"
value={formData.jenisKelaminId}
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val || "" })}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null/undefined
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} // ✅ disable saat loading
clearable
searchable
required
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
/>
<Select
key={"rating"}
value={formData.ratingId}
onChange={(val) => setFormData({ ...formData, ratingId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Rating</Text>}
placeholder='Pilih rating'
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
clearable
searchable
required
error={!formData.ratingId ? "Pilih rating" : undefined}
/>
<Select
key={"kelompokUmur"}
value={formData.kelompokUmurId}
onChange={(val) => setFormData({ ...formData, kelompokUmurId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kelompok Umur</Text>}
placeholder='Pilih kelompok umur'
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
clearable
searchable
required
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditResponden;

View File

@@ -0,0 +1,116 @@
'use client'
import { ModalKonfirmasiHapus } from "@/app/admin/(dashboard)/_com/modalKonfirmasiHapus"
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"
import colors from "@/con/colors"
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from "@mantine/core"
import { useShallowEffect } from "@mantine/hooks"
import { IconArrowBack, IconEdit, IconX } from "@tabler/icons-react"
import { useRouter, useParams } from "next/navigation"
import { useState } from "react"
import { useProxy } from "valtio/utils"
export default function DetailResponden() {
const [modalHapus, setModalHapus] = useState(false)
const stateDetail = useProxy(indeksKepuasanState.responden)
const router = useRouter()
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateDetail.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ppid/ikm-desa-darmasaba/responden")
}
}
if (!stateDetail.findUnique.data) {
return (
<Stack>
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Responden</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Responden</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal</Text>
<Text fz={"lg"}>{
stateDetail.findUnique.data?.tanggal
? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
: '-'
}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jenis Kelamin</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.jenisKelamin?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.rating?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kelompok Umur</Text>
<Text fz={"lg"}>{stateDetail.findUnique.data?.kelompokUmur?.name}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`);
}
}}
disabled={!stateDetail.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus responden ini?"
/>
</Box>
)
}

View File

@@ -0,0 +1,148 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Select, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import { useShallowEffect } from '@mantine/hooks';
function RespondenCreate() {
const router = useRouter();
const stategrafikBerdasarkanResponden = useProxy(indeksKepuasanState.responden)
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafikBerdasarkanResponden.create.form = {
...stategrafikBerdasarkanResponden.create.form,
name: "",
tanggal: "",
jenisKelaminId: "",
ratingId: "",
kelompokUmurId: "",
}
}
useShallowEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load()
})
const handleSubmit = async () => {
try {
const id = await stategrafikBerdasarkanResponden.create.create();
if (typeof id !== 'undefined') {
const idStr = String(id);
await stategrafikBerdasarkanResponden.findUnique.load(idStr);
if (stategrafikBerdasarkanResponden.findUnique.data) {
setDonutData([stategrafikBerdasarkanResponden.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/responden");
} catch (error) {
console.error('Error submitting form:', error);
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
value={stategrafikBerdasarkanResponden.create.form.name}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
value={stategrafikBerdasarkanResponden.create.form.tanggal}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.tanggal = val.currentTarget.value;
}}
/>
<Select
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={stategrafikBerdasarkanResponden.create.form.jenisKelaminId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.jenisKelaminId = val ?? "";
}}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
/>
<Select
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={stategrafikBerdasarkanResponden.create.form.kelompokUmurId || ""}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.kelompokUmurId = val ?? "";
}}
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}
export default RespondenCreate;

View File

@@ -0,0 +1,144 @@
'use client';
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
function Responden() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Responden'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListResponden search={search} />
</Box>
);
}
interface ListRespondenProps {
search: string;
}
function ListResponden({ search }: ListRespondenProps) {
const state = useProxy(indeksKepuasanState.responden);
const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 10)
}, [page]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md">
<Stack>
<Title>Responden</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap="xs">
<Paper bg={colors['white-1']} p="md" h={{ base: 730, md: 650 }}>
<Title mb={10} order={3}>List Responden</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data responden</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.name}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID')
: '-'}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.jenisKelamin.name}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</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>
</Stack>
</Box>
);
}
export default Responden;

View File

@@ -1,6 +1,6 @@
'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 } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -42,18 +42,21 @@ function ListKategoriPrestasi({ search }: { search: string }) {
} }
} }
const {
data,
page,
totalPages,
loading,
load,
} = stateKategori.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateKategori.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (stateKategori.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!stateKategori.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -100,6 +103,14 @@ function ListKategoriPrestasi({ search }: { search: string }) {
</Table> </Table>
</Box> </Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -1,7 +1,7 @@
/* 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, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, 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';
@@ -30,20 +30,36 @@ function ListPrestasiDesa() {
function ListPrestasi({ search }: { search: string }) { function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa) const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter(); const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => { const{
const keyword = search.toLowerCase(); data,
return ( page,
item.name.toLowerCase().includes(keyword) || totalPages,
item.deskripsi.toLowerCase().includes(keyword) || loading,
item.kategori?.name?.toLowerCase().includes(keyword) load,
); } = listState.findMany
// Debug log
console.log('ListPrestasi state:', {
loading,
data: data?.length,
page,
totalPages,
search
}); });
if (!listState.findMany.data) { useEffect(() => {
console.log('Loading data...', { page, search });
load(page, 10, search).then(() => {
console.log('Data loaded:', listState.findMany.data);
}).catch(error => {
console.error('Error loading data:', error);
});
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -82,7 +98,7 @@ function ListPrestasi({ search }: { search: string }) {
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box> </Box>
</TableTd> </TableTd>
<TableTd>{item.kategori?.name}</TableTd> <TableTd>{item.kategori?.name || 'No Category'}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}> <Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}>
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={25} />
@@ -95,6 +111,14 @@ function ListPrestasi({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -1,67 +1,110 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconBulb, IconUsers, IconBrandFacebook } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "Program Inovasi", label: "Program Inovasi",
value: "program-inovasi", value: "program-inovasi",
href: "/admin/landing-page/profile/program-inovasi" href: "/admin/landing-page/profile/program-inovasi",
icon: <IconBulb size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola program inovasi desa",
}, },
{ {
label: "Pejabat Desa", label: "Pejabat Desa",
value: "pejabat-desa", value: "pejabat-desa",
href: "/admin/landing-page/profile/pejabat-desa" href: "/admin/landing-page/profile/pejabat-desa",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola data pejabat desa",
}, },
{ {
label: "Media Sosial", label: "Media Sosial",
value: "media-sosial", value: "media-sosial",
href: "/admin/landing-page/profile/media-sosial" href: "/admin/landing-page/profile/media-sosial",
icon: <IconBrandFacebook size={18} stroke={1.8} />,
tooltip: "Atur tautan media sosial 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}>Profile</Title> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> Profil Desa
<TabsList p={"xs"} bg={"#BBC8E7FF"}> </Title>
{tabs.map((e, i) => ( <Tabs
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
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>
<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 LayoutTabs; export default LayoutTabs;

View File

@@ -1,9 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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';
@@ -12,17 +23,17 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditMediaSosial() { function EditMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
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: stateMediaSosial.update.form.name || "", name: stateMediaSosial.update.form.name || '',
iconUrl: stateMediaSosial.update.form.iconUrl || "", iconUrl: stateMediaSosial.update.form.iconUrl || '',
imageId: stateMediaSosial.update.form.imageId || "" imageId: stateMediaSosial.update.form.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const id = params?.id as string; const id = params?.id as string;
@@ -34,136 +45,147 @@ function EditMediaSosial() {
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || "", name: data.name || '',
iconUrl: data.iconUrl || "", iconUrl: data.iconUrl || '',
imageId: data.imageId || "", imageId: data.imageId || '',
}); });
// Tampilkan preview gambar if (data.image?.link) setPreviewImage(data.image.link);
if (data.image?.link) {
setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading program inovasi:", error); console.error('Error loading media sosial:', error);
toast.error( toast.error(
error instanceof Error ? error.message : "Gagal mengambil data program inovasi" error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
); );
} }
} };
loadMediaSosial(); loadMediaSosial();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateMediaSosial.update.form = { stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
...stateMediaSosial.update.form,
name: formData.name,
iconUrl: formData.iconUrl,
imageId: formData.imageId ?? "",
}
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
stateMediaSosial.update.form.imageId = uploaded.id; stateMediaSosial.update.form.imageId = uploaded.id;
} }
await stateMediaSosial.update.update(); await stateMediaSosial.update.update();
toast.success("Media Sosial berhasil diperbarui!"); toast.success('Media sosial berhasil diperbarui!');
router.push("/admin/landing-page/profile/media-sosial"); router.push('/admin/landing-page/profile/media-sosial');
} catch (error) { } catch (error) {
console.error("Error updating media sosial:", error); console.error('Error updating media sosial:', error);
toast.error("Terjadi kesalahan saat memperbarui media sosial"); toast.error('Terjadi kesalahan saat memperbarui media sosial');
} }
}; };
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Edit Media Sosial
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Media Sosial</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Media Sosial
<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={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
</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>
<TextInput <TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>} required
placeholder='Masukkan nama media sosial'
/> />
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={formData.iconUrl} value={formData.iconUrl}
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })} onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Icon URL / No Telephone</Text>} required
placeholder='Masukkan icon url'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>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>

View File

@@ -2,103 +2,132 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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 DetailMediaSosial() { function DetailMediaSosial() {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
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(() => {
stateMediaSosial.findUnique.load(params?.id as string) stateMediaSosial.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateMediaSosial.delete.byId(selectedId) stateMediaSosial.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/landing-page/profile/media-sosial") router.push("/admin/landing-page/profile/media-sosial");
} }
} };
if (!stateMediaSosial.findUnique.data) { if (!stateMediaSosial.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = stateMediaSosial.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 Media Sosial</Text> </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Paper
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Media Sosial
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Media Sosial / Nama Kontak</Text> <Text fz="lg" fw="bold">Nama Media Sosial / Kontak</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Icon URL / No Telephone</Text> <Text fz="lg" fw="bold">Icon / Nomor Telepon</Text>
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text> <Text fz="md" c="dimmed">{data.iconUrl || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz="lg" fw="bold">Gambar</Text>
<Box w={100} h={100}> {data.image?.link ? (
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" /> <Image
</Box> src={data.image.link}
alt={data.name || 'Gambar Media Sosial'}
w={120}
h={120}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box> </Box>
<Box>
<Flex gap={"xs"}> <Group gap="sm">
<Tooltip label="Hapus Media Sosial" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateMediaSosial.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateMediaSosial.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={!stateMediaSosial.findUnique.data} variant="light"
color="red"> radius="md"
<IconX size={20} /> size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Media Sosial" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateMediaSosial.findUnique.data) { onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${data.id}/edit`)}
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateMediaSosial.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </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 media sosial ini?" text="Apakah Anda yakin ingin menghapus media sosial ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,19 @@
/* 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 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';
@@ -11,9 +22,9 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import profileLandingPageState from '../../../../_state/landing-page/profile'; import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateMediaSosial() { export default function CreateMediaSosial() {
const router = useRouter(); const router = useRouter();
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial) const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
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);
@@ -23,27 +34,28 @@ function CreateMediaSosial() {
const resetForm = () => { const resetForm = () => {
stateMediaSosial.create.form = { stateMediaSosial.create.form = {
name: "", name: '',
imageId: "", imageId: '',
iconUrl: "", iconUrl: '',
}; };
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');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
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 mengupload file"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
stateMediaSosial.create.form.imageId = uploaded.id; stateMediaSosial.create.form.imageId = uploaded.id;
@@ -51,98 +63,108 @@ function CreateMediaSosial() {
await stateMediaSosial.create.create(); await stateMediaSosial.create.create();
resetForm(); resetForm();
router.push("/admin/landing-page/profile/media-sosial") router.push('/admin/landing-page/profile/media-sosial');
} };
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Tambah Media Sosial
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Media Sosial</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Media Sosial
<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>
<TextInput <TextInput
label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak"
value={stateMediaSosial.create.form.name || ''} value={stateMediaSosial.create.form.name || ''}
onChange={(val) => { onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
stateMediaSosial.create.form.name = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
placeholder='Masukkan nama media sosial / nama kontak'
/> />
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon"
value={stateMediaSosial.create.form.iconUrl || ''} value={stateMediaSosial.create.form.iconUrl || ''}
onChange={(val) => { onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
stateMediaSosial.create.form.iconUrl = val.target.value; required
}}
label={<Text fw={"bold"} fz={"sm"}>Link Media Sosial / No Telephone</Text>}
placeholder='Masukkan link media sosial / no telephone'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>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>
</Box> </Box>
); );
} }
export default CreateMediaSosial;

View File

@@ -1,13 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
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 { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, 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 profileLandingPageState from '../../../_state/landing-page/profile'; import profileLandingPageState from '../../../_state/landing-page/profile';
function MediaSosial() { function MediaSosial() {
@@ -16,7 +15,7 @@ function MediaSosial() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Media Sosial' title='Media Sosial'
placeholder='pencarian' placeholder='Cari nama media sosial atau kontak...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -38,95 +37,83 @@ function ListMediaSosial({ search }: { search: string }) {
load, load,
} = stateMediaSosial.findMany; } = stateMediaSosial.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, search)
}, [page]) }, [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.iconUrl?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={550} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
<TableTh>Image</TableTh>
<TableTh>Icon URL / No Telephone</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Media Sosial' <Title order={4}>Daftar Media Sosial</Title>
href='/admin/landing-page/profile/media-sosial/create' <Tooltip label="Tambah Media Sosial" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
<Box style={{ overflowY: "auto" }}> Tambah Baru
<Table striped withTableBorder withRowBorders> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh> <TableTh>Nama Media Sosial / Kontak</TableTh>
<TableTh>Image</TableTh> <TableTh>Gambar</TableTh>
<TableTh>Icon URL / No Telephone</TableTh> <TableTh>Icon / No. Telepon</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={50} h={50}> <Text fw={500}>{item.name}</Text>
<Image src={item.image?.link} alt={item.name} /> </TableTd>
</Box> <TableTd>
</TableTd> <Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
<TableTd> {item.image?.link ? (
<Box w={250}> <Image src={item.image.link} alt={item.name} fit="cover" />
<a style={{color: "black"}} href={item.iconUrl} target="_blank" rel="noopener noreferrer"> ) : (
<Text truncate fz={'sm'}>{item.iconUrl}</Text> <Box bg={colors['blue-button']} w="100%" h="100%" />
</a> )}
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}> <Text truncate fz="sm" color="dimmed">
<IconDeviceImac size={20} /> {item.iconUrl || item.noTelp || '-'}
</Button> </Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${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 media sosial yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
@@ -136,11 +123,13 @@ function ListMediaSosial({ search }: { search: string }) {
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>

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -144,124 +144,134 @@ function EditPejabatDesa() {
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Box> <Group mb="md">
<Button variant="subtle" onClick={handleBack}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pejabat Desa
</Title>
</Group>
<Box> <Paper
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}> w={{ base: "100%", md: "50%" }}
<Stack gap="xs"> bg={colors['white-1']}
<Title order={3}>Edit Profile Pejabat Desa</Title> p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Profile Pejabat Desa</Title>
{/* Nama Field */} {/* Nama Field */}
<TextInput <TextInput
label={<Text fw="bold">Nama Perbekel</Text>} label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel" placeholder="Masukkan nama perbekel"
value={allState.edit.form.name} value={allState.edit.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)} onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.edit.form.name && "Nama wajib diisi"} error={!allState.edit.form.name && "Nama wajib diisi"}
/> />
{/* Posisi Field */} {/* Posisi Field */}
<TextInput <TextInput
label={<Text fw="bold">Posisi</Text>} label={<Text fw="bold">Posisi</Text>}
placeholder="Masukkan posisi" placeholder="Masukkan posisi"
value={allState.edit.form.position} value={allState.edit.form.position}
onChange={(e) => handleFieldChange('position', e.currentTarget.value)} onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
error={!allState.edit.form.position && "Posisi wajib diisi"} error={!allState.edit.form.position && "Posisi wajib diisi"}
/> />
{/* File Upload */} {/* File Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Dropzone
<Box> onDrop={(files) => handleFileChange(files[0])}
<Dropzone onReject={() => toast.error('File tidak valid.')}
onDrop={(files) => handleFileChange(files[0])} maxSize={5 * 1024 ** 2} // Maks 5MB
onReject={() => toast.error('File tidak valid.')} accept={{ 'image/*': [] }}
maxSize={5 * 1024 ** 2} // Maks 5MB >
accept={{ 'image/*': [] }} <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
> <Dropzone.Accept>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<Dropzone.Accept> </Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Accept> <IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<Dropzone.Reject> </Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Reject> <IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar Maksimal 5MB dan harus format gambar
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */} {/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm">
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview"
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
maxHeight: '200px', maxHeight: '200px',
objectFit: 'contain', objectFit: 'contain',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #ddd', border: '1px solid #ddd',
}} }}
/> />
</Box> </Box>
)}
</Box>
</Box>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)} )}
</Box> </Box>
</Box>
{/* Submit Button */} {/* Preview Gambar */}
<Group> <Box>
<Button <Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
bg={colors['blue-button']} {previewImage ? (
onClick={handleSubmit} <Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
loading={isSubmitting || allState.edit.loading} ) : (
disabled={!allState.edit.form.name} <Center w={200} h={200} bg="gray.2">
> <Stack align="center" gap="xs">
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} <IconImageInPicture size={48} color="gray" />
</Button> <Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
<Button {/* Submit Button */}
variant="outline" <Group>
onClick={handleBack} <Button
disabled={isSubmitting || allState.edit.loading} bg={colors['blue-button']}
> onClick={handleSubmit}
Batal loading={isSubmitting || allState.edit.loading}
</Button> disabled={!allState.edit.form.name}
</Group> >
</Stack> {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Paper> </Button>
</Box>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.edit.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,24 +1,26 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { Box, Button, Center, Divider, Grid, GridCol, Image, 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 profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
function Page() { function Page() {
const router = useRouter() const router = useRouter();
const allList = useProxy(profileLandingPageState.pejabatDesa) const allList = useProxy(profileLandingPageState.pejabatDesa);
useShallowEffect(() => { useShallowEffect(() => {
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed allList.findUnique.load("edit");
}, []) }, []);
if (!allList.findUnique.data) { if (!allList.findUnique.data) {
return <Stack> return (
<Skeleton radius={10} h={800} /> <Stack align="center" justify="center" py="xl">
</Stack> <Skeleton radius="md" height={800} />
</Stack>
);
} }
const dataArray = Array.isArray(allList.findUnique.data) const dataArray = Array.isArray(allList.findUnique.data)
@@ -26,79 +28,82 @@ function Page() {
: [allList.findUnique.data]; : [allList.findUnique.data];
return ( return (
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap={"xs"}> <Stack gap="md">
<Grid> <Grid align="center">
<GridCol span={{ base: 12, md: 11 }}> <GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Pejabat Desa</Title> <Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}> <Tooltip label="Edit Profil Pejabat" withArrow>
<IconEdit size={16} /> <Button
</Button> c="blue"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}
>
Edit
</Button>
</Tooltip>
</GridCol> </GridCol>
</Grid> </Grid>
{dataArray.map((item) => ( {dataArray.map((item) => (
<Box key={item.id} > <Paper key={item.id} p="xl" bg={colors['BG-trans']} radius="md" shadow="xs">
<Paper p={"xl"} bg={colors['BG-trans']}> <Box px={{ base: "sm", md: 100 }}>
<Box px={{ base: "md", md: 100 }}> <Grid>
<Grid> <GridCol span={12}>
<GridCol span={{ base: 12, md: 12 }}> <Center>
<Center> <Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' /> </Center>
</Center> </GridCol>
</GridCol> <GridCol span={12}>
<GridCol span={{ base: 12, md: 12 }}> <Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text> Profil Pimpinan Badan Publik Desa Darmasaba
</GridCol> </Text>
</Grid> </GridCol>
</Box> </Grid>
<Divider my={"md"} color={colors['blue-button']} /> </Box>
{/* biodata perbekel */} <Divider my="md" color={colors['blue-button']} />
<Box px={{ base: 0, md: 50 }} pb={30}> <Box px={{ base: 0, md: 50 }} pb="xl">
<Box pb={20} px={{ base: 0, md: 50 }}> <Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg">
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}> <Stack gap={0}>
<Stack gap={0}> <Center>
<Center> <Image
<Image pt={{ base: 0, md: 60 }}
pt={{ base: 0, md: 90 }} src={item.image?.link || "/perbekel.png"}
src={item.image?.link || "/perbekel.png"} w={{ base: 250, md: 350 }}
w={{ base: 250, md: 350 }} alt="Foto Profil Pejabat"
alt='Foto Profil PPID' radius="md"
onError={(e) => { onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
e.currentTarget.src = "/perbekel.png"; />
}} </Center>
/> <Paper
</Center> bg={colors['blue-button']}
<Paper py="md"
bg={colors['blue-button']} px="sm"
py={20} radius="md"
className="glass3" className="glass3"
px={{ base: 10, md: 10 }} style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
> <Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}> {item.name}
{item.name} </Text>
</Text>
</Paper>
</Stack>
</Paper> </Paper>
</Box> </Stack>
<Box pt={10}> </Paper>
<Box> <Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text> <Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text> <Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
</Box> {item.position}
</Box> </Text>
</Box> </Box>
</Paper> </Box>
</Box> </Paper>
))} ))}
</Stack> </Stack>
</Paper> </Paper>
) );
} }
export default Page;
export default Page;

View File

@@ -3,7 +3,18 @@
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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';
@@ -86,92 +97,113 @@ function EditProgramInovasi() {
}; };
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Edit Program Inovasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Program Inovasi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Program Inovasi
<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={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
</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>
<TextInput <TextInput
label="Nama Program Inovasi"
placeholder="Masukkan nama program inovasi"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>} required
placeholder='Masukkan nama produk'
/> />
<TextInput <TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi program inovasi"
value={formData.description} value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setFormData({ ...formData, description: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>} required
placeholder='Masukkan deskripsi'
/> />
<TextInput <TextInput
label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)"
value={formData.link} value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })} onChange={(e) => setFormData({ ...formData, link: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
placeholder='Masukkan link'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>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>

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile'; import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
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';
@@ -31,82 +31,116 @@ function DetailProgramInovasi() {
if (!stateProgramInovasi.findUnique.data) { if (!stateProgramInovasi.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={12}>
<Skeleton h={500} /> <Skeleton height={520} radius="md" />
</Stack> </Stack>
) )
} }
const data = stateProgramInovasi.findUnique.data
return ( return (
<Box> <Box px={{ base: 'md', md: 'xl' }} py="lg">
<Box mb={10}> <Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
<Button variant="subtle" onClick={() => router.back()}> Kembali
<IconArrowBack color={colors['blue-button']} size={25} /> </Button>
</Button>
</Box> <Paper
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> w={{ base: "100%", md: "60%" }}
<Stack> bg={colors['white-1']}
<Text fz={"xl"} fw={"bold"}>Detail Program Inovasi</Text> p="lg"
<Paper bg={colors['BG-trans']} p={'md'}> radius="md"
<Stack gap={"xs"}> shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Inovasi
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Inovasi</Text> <Text fz="lg" fw="bold">Nama Program Inovasi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.description}</Text> <Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }}>{data.description || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link</Text> <Text fz="lg" fw="bold">Link</Text>
<a {data.link ? (
href={stateProgramInovasi.findUnique.data?.link || "#"} <a
target="_blank" href={data.link}
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"} style={{
</a> color: colors['blue-button'],
</Box> textDecoration: 'underline',
<Box> wordBreak: 'break-word',
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={stateProgramInovasi.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateProgramInovasi.findUnique.data) {
setSelectedId(stateProgramInovasi.findUnique.data.id);
setModalHapus(true);
}
}} }}
disabled={!stateProgramInovasi.findUnique.data} >
color="red"> {data.link}
<IconX size={20} /> </a>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Program"
radius="md"
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Program Inovasi" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Program Inovasi" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateProgramInovasi.findUnique.data) { onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${data.id}/edit`)}
router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!stateProgramInovasi.findUnique.data} >
color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </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 program inovasi ini?" text="Apakah Anda yakin ingin menghapus program inovasi ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
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';
@@ -13,7 +25,7 @@ import profileLandingPageState from '../../../../_state/landing-page/profile';
function CreateProgramInovasi() { function CreateProgramInovasi() {
const router = useRouter(); const router = useRouter();
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
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);
@@ -31,20 +43,21 @@ function CreateProgramInovasi() {
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");
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
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 mengupload file"); return toast.error("Gagal mengunggah gambar, silakan coba lagi");
} }
stateProgramInovasi.create.form.imageId = uploaded.id; stateProgramInovasi.create.form.imageId = uploaded.id;
@@ -55,99 +68,116 @@ function CreateProgramInovasi() {
router.push("/admin/landing-page/profile/program-inovasi") router.push("/admin/landing-page/profile/program-inovasi")
} }
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Tambah Program Inovasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Program Inovasi</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Program Inovasi
<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={{ 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>
<TextInput
value={stateProgramInovasi.create.form.name || ''}
onChange={(val) => {
stateProgramInovasi.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Program Inovasi</Text>}
placeholder='Masukkan nama program inovasi'
/>
<TextInput
value={stateProgramInovasi.create.form.description || ''}
onChange={(val) => {
stateProgramInovasi.create.form.description = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput <TextInput
value={stateProgramInovasi.create.form.link || ''} label="Nama Program Inovasi"
onChange={(val) => { placeholder="Masukkan nama program inovasi"
stateProgramInovasi.create.form.link = val.target.value; value={stateProgramInovasi.create.form.name}
}} onChange={(e) => (stateProgramInovasi.create.form.name = e.target.value)}
label={<Text fw={"bold"} fz={"sm"}>Link</Text>} required
placeholder='Masukkan link'
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateProgramInovasi.create.form.description || ''}
onChange={(htmlContent: string) => {
stateProgramInovasi.create.form.description = htmlContent;
}}
/>
</Box>
<TextInput
label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)"
value={stateProgramInovasi.create.form.link || ''}
onChange={(e) => (stateProgramInovasi.create.form.link = e.target.value)}
/>
<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>

View File

@@ -1,22 +1,22 @@
/* 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 { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, 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 profileLandingPageState from '../../../_state/landing-page/profile'; import profileLandingPageState from '../../../_state/landing-page/profile';
function ProgramInovasi() { function ProgramInovasi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box px="md" py="lg">
<HeaderSearch <HeaderSearch
title='Program Inovasi' title="Program Inovasi"
placeholder='pencarian' placeholder="Cari program inovasi..."
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,117 +27,118 @@ function ProgramInovasi() {
} }
function ListProgramInovasi({ search }: { search: string }) { function ListProgramInovasi({ search }: { search: string }) {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi) const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
data,
page,
totalPages,
loading,
load,
} = stateProgramInovasi.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10); load(page, 10, search);
}, [page]); }, [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.description?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={20}>
<Skeleton height={550} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={15}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<JudulList <Box mb="md" display="flex"
title='List Program Inovasi' style={{ justifyContent: 'space-between', alignItems: 'center' }}
href='/admin/landing-page/profile/program-inovasi/create' >
/> <Title order={4}>Daftar Program Inovasi</Title>
<Box style={{ overflowY: "auto" }}> <Tooltip label="Tambah Program Inovasi" withArrow>
<Table striped withTableBorder withRowBorders> <Button
color="blue"
leftSection={<IconPlus size={18} />}
variant="light"
radius="md"
onClick={() => router.push('/admin/landing-page/profile/program-inovasi/create')}
>
Tambah Program
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Program</TableTh> <TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh> <TableTh>Link</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length === 0 ? (
<TableTr key={item.id}> <TableTr>
<TableTd>{item.name}</TableTd> <TableTd colSpan={4}>
<TableTd w={200}>{item.description}</TableTd> <Center py={20}>
<TableTd> <Text color="dimmed">Belum ada data program inovasi</Text>
<Box w={250}> </Center>
<a style={{ color: "black" }} href={item.link} target="_blank" rel="noopener noreferrer">
<Text truncate fz={'sm'}>{item.link}</Text>
</a>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500}>{item.name}</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lineClamp={2}>
{item.description}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Tooltip label="Buka tautan program" position="top" withArrow>
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
>
<Text truncate fz="sm">{item.link}</Text>
</a>
</Tooltip>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
radius="md"
onClick={() =>
router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)
}
>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
{filteredData.length > 0 && (
<Center mt="md">
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
color="blue"
/>
</Center>
)}
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,14 +1,14 @@
/* 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 { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, 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 sdgsDesa from '../../_state/landing-page/sdgs-desa';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
function SdgsDesa() { function SdgsDesa() {
@@ -39,20 +39,11 @@ function ListSdgsDesa({ search }: { search: string }) {
load, load,
} = listState.findMany; } = listState.findMany;
useEffect(() => { useShallowEffect(() => {
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.jumlah?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {

View File

@@ -42,18 +42,10 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search)
}, [page]) }, [page, search])
const filteredData = (data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.jumlah.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,

View File

@@ -1,63 +1,93 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconTrash, IconRecycle } from '@tabler/icons-react';
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) { function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "List Pengelolaan Sampah Bank Sampah",
value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "List Pengelolaan Sampah Bank Sampah",
router.push(tab.href) value: "listpengelolaansampahbanksampah",
} href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
setActiveTab(value) icon: <IconTrash size={18} stroke={1.8} />,
tooltip: "Kelola data pengelolaan sampah bank sampah",
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
icon: <IconRecycle size={18} stroke={1.8} />,
tooltip: "Kelola data bank sampah terdekat",
},
];
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 tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
} }
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="md">
<Title order={3}>Layanan Online Desa</Title> <Title order={3} mb="sm">Pengelolaan Sampah Bank Sampah</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> value={activeTab}
{tabs.map((e, i) => ( onChange={handleTabChange}
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
))} radius="md"
</TabsList> >
{tabs.map((e, i) => ( <TabsList>
<TabsPanel key={i} value={e.value}> {tabs.map((tab) => (
{/* Konten dummy, bisa diganti tergantung routing */} <Tooltip
<></> key={tab.value}
</TabsPanel> label={tab.tooltip}
))} position="top"
</Tabs> withArrow
{children} transitionProps={{ transition: 'pop', duration: 300 }}
</Stack> >
); <TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
padding: '10px 20px',
height: 'auto',
minHeight: 44,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsPanel
value={activeTab || ''}
pt="lg"
style={{
minHeight: '60vh',
}}
>
{children}
</TabsPanel>
</Tabs>
</Stack>
);
} }
export default LayoutTabsPengelolaanSampahBankSampah; export default LayoutTabsPengelolaanSampahBankSampah;

View File

@@ -1,8 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
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 dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -64,63 +64,97 @@ function EditKeteranganBankSampahTerdekat() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (!formData.name.trim()) {
return toast.error('Nama bank sampah harus diisi');
}
if (!formData.alamat.trim()) {
return toast.error('Alamat harus diisi');
}
if (!formData.namaTempatMaps.trim()) {
return toast.error('Nama tempat di Maps harus diisi');
}
if (!markerPosition) {
return toast.error('Silakan pilih lokasi di peta');
}
keteranganState.edit.form = { keteranganState.edit.form = {
...keteranganState.edit.form, ...keteranganState.edit.form,
name: formData.name.trim(), name: formData.name.trim(),
alamat: formData.alamat.trim(), alamat: formData.alamat.trim(),
namaTempatMaps: formData.namaTempatMaps.trim(), namaTempatMaps: formData.namaTempatMaps.trim(),
lat: formData.lat, lat: markerPosition.lat,
lng: formData.lng, lng: markerPosition.lng,
} };
await keteranganState.edit.update(); await keteranganState.edit.update();
toast.success('Data bank sampah berhasil diperbarui');
keteranganState.findUnique.data = null; keteranganState.findUnique.data = null;
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) { } catch (error) {
console.error("Error updating pengelolaan sampah:", error); console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah"); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah');
} }
} }
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Edit Bank Sampah Terdekat
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Keterangan Bank Sampah Terdekat</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
value={formData.name} value={formData.name}
onChange={(val) => setFormData({ ...formData, name: val.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>} required
placeholder='Masukkan nama Bank Sampah Terdekat'
/> />
<TextInput <TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
value={formData.alamat} value={formData.alamat}
onChange={(val) => setFormData({ ...formData, alamat: val.target.value })} onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fw="bold" fz="sm">Alamat</Text>} required
placeholder='Masukkan alamat Bank Sampah'
/> />
<TextInput <TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
value={formData.namaTempatMaps} value={formData.namaTempatMaps}
onChange={(val) => setFormData({ ...formData, namaTempatMaps: val.target.value })} onChange={(e) => setFormData({ ...formData, namaTempatMaps: e.target.value })}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>} required
placeholder='Masukkan nama tempat maps Bank Sampah'
/> />
<Box> <Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text> <Text fw="bold" fz="sm" mb={6}>
<Box style={{ height: 300, width: '100%' }}> Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMapEdit <LeafletMapEdit
key={markerPosition?.lat ?? 'default'} key={markerPosition?.lat ?? 'default'}
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }} initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
onChange={(pos) => { onChange={(pos) => {
setMarkerPosition(pos); setMarkerPosition(pos);
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
lat: pos.lat, lat: pos.lat,
lng: pos.lng, lng: pos.lng,
@@ -128,9 +162,26 @@ function EditKeteranganBankSampahTerdekat() {
}} }}
/> />
</Box> </Box>
{markerPosition && (
<Text fz="xs" mt={4} c="green">
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
</Text>
)}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right" mt="md">
<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>

View File

@@ -3,132 +3,148 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Anchor, Box, Button, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowLeft, IconEdit, IconTrash } 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 { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import dynamic from 'next/dynamic';
import dynamic from 'next/dynamic'
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), { const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), {
ssr: false ssr: false
}) });
function DetailKeteranganBankSampahTerdekat() { function DetailKeteranganBankSampahTerdekat() {
const router = useRouter(); const router = useRouter();
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const params = useParams() const params = useParams();
useEffect(() => { useEffect(() => {
keteranganState.findUnique.load(params?.id as string) keteranganState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
keteranganState.delete.byId(selectedId) keteranganState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat") router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} }
} };
if (!keteranganState.findUnique.data) { if (!keteranganState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack p="md">
<Skeleton h={500} /> <Skeleton h={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="light"
leftSection={<IconArrowLeft size={20} />}
onClick={() => router.back()}
radius="xl"
color="blue"
>
Kembali
</Button> </Button>
</Box> </Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> <Paper
<Stack> w={{ base: "100%", md: "60%" }}
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text> p="xl"
radius="lg"
withBorder
shadow="md"
style={{ background: colors['white-1'] }}
>
<Stack gap="lg">
<Title order={2} c="dark">
Detail Bank Sampah Terdekat
</Title>
<Paper bg={colors['BG-trans']} p={'md'}> <Paper p="lg" radius="md" withBorder >
<Stack gap={"xs"}> <Stack gap="md">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text> <Text fz="sm" c="dimmed">Nama Bank Sampah</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.name}</Text> <Text fz="lg" fw={600}>{keteranganState.findUnique.data?.name}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text> <Text fz="sm" c="dimmed">Alamat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.alamat}</Text> <Text fz="lg">{keteranganState.findUnique.data?.alamat}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text> <Text fz="sm" c="dimmed">Nama Tempat di Maps</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.namaTempatMaps}</Text> <Text fz="lg">{keteranganState.findUnique.data?.namaTempatMaps}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Peta Lokasi</Text> <Text fz="sm" c="dimmed" mb={6}>Peta Lokasi</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? ( {keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<Box <Box style={{ height: "300px", borderRadius: "12px", overflow: "hidden" }}>
style={{
height: "300px",
}}
>
<LeafletMap <LeafletMap
defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }} defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }}
readOnly readOnly
/> />
</Box> </Box>
) : ( ) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> <Text c="dimmed" fz="sm">Belum ada koordinat</Text>
)} )}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text> <Text fz="sm" c="dimmed" mb={6}>Petunjuk Arah</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? ( {keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<a <Anchor
href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`} href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }} underline="always"
c="blue"
> >
Buka Petunjuk Arah di Google Maps Buka di Google Maps
</a> </Anchor>
) : ( ) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> <Text c="dimmed" fz="sm">Belum ada koordinat</Text>
)} )}
</Box> </Box>
<Box> <Flex gap="sm" mt="md">
<Flex gap={"xs"}> <Button
<Button onClick={() => {
onClick={() => { if (keteranganState.findUnique.data) {
if (keteranganState.findUnique.data) { setSelectedId(keteranganState.findUnique.data.id);
setSelectedId(keteranganState.findUnique.data.id); setModalHapus(true);
setModalHapus(true); }
} }}
}} disabled={!keteranganState.findUnique.data}
disabled={!keteranganState.findUnique.data} leftSection={<IconTrash size={18} />}
color="red" color="red"
> radius="md"
<IconX size={20} /> variant='light'
</Button> >
<Button Hapus
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)} </Button>
color="green" <Button
> onClick={() =>
<IconEdit size={20} /> router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)
</Button> }
</Flex> leftSection={<IconEdit size={18} />}
</Box> color="green"
radius="md"
variant='light'
>
Edit
</Button>
</Flex>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -138,10 +154,9 @@ function DetailKeteranganBankSampahTerdekat() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus keterangan bank sampah terdekat ini?" text="Apakah Anda yakin ingin menghapus data bank sampah ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -1,9 +1,10 @@
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
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 { toast } from 'react-toastify';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -28,58 +29,107 @@ function CreateKeteranganBankSampahTerdekat() {
setMarkerPosition(null) setMarkerPosition(null)
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (markerPosition) { try {
keteranganState.create.form.lat = markerPosition.lat if (!keteranganState.create.form.name) {
keteranganState.create.form.lng = markerPosition.lng return toast.error('Nama bank sampah harus diisi');
}
if (markerPosition) {
keteranganState.create.form.lat = markerPosition.lat;
keteranganState.create.form.lng = markerPosition.lng;
} else {
return toast.error('Silakan pilih lokasi di peta');
}
await keteranganState.create.create();
toast.success('Data bank sampah berhasil ditambahkan');
resetForm();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) {
console.error('Error creating bank sampah:', error);
toast.error('Gagal menambahkan data bank sampah');
} }
await keteranganState.create.create()
resetForm()
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
} }
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" color="blue" 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>
<Title order={4} ml="sm" c="dark">
Tambah Bank Sampah Terdekat
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Keterangan Bank Sampah Terdekat</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
value={keteranganState.create.form.name} value={keteranganState.create.form.name}
onChange={(val) => keteranganState.create.form.name = val.target.value} onChange={(e) => (keteranganState.create.form.name = e.target.value)}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>} required
placeholder='Masukkan nama Bank Sampah Terdekat'
/> />
<TextInput <TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
value={keteranganState.create.form.alamat} value={keteranganState.create.form.alamat}
onChange={(val) => keteranganState.create.form.alamat = val.target.value} onChange={(e) => (keteranganState.create.form.alamat = e.target.value)}
label={<Text fw="bold" fz="sm">Alamat</Text>} required
placeholder='Masukkan alamat Bank Sampah'
/> />
<TextInput <TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
value={keteranganState.create.form.namaTempatMaps} value={keteranganState.create.form.namaTempatMaps}
onChange={(val) => keteranganState.create.form.namaTempatMaps = val.target.value} onChange={(e) => (keteranganState.create.form.namaTempatMaps = e.target.value)}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>} required
placeholder='Masukkan nama tempat maps Bank Sampah'
/> />
<Box> <Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text> <Text fw="bold" fz="sm" mb={6}>
<Box style={{ height: 300, width: '100%' }}> Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMap <LeafletMap
onSelect={(pos) => setMarkerPosition(pos)} onSelect={(pos) => setMarkerPosition(pos)}
defaultCenter={{ lat: -8.65, lng: 115.2 }} defaultCenter={{ lat: -8.65, lng: 115.2 }}
/> />
</Box> </Box>
{markerPosition && (
<Text fz="xs" mt={4} c="green">
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
</Text>
)}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right" mt="md">
<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>

View File

@@ -1,14 +1,12 @@
/* 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 } 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } 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 { useState } from 'react';
import { useEffect, useState } from 'react';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
function KeteranganBankSampahTerdekat() { function KeteranganBankSampahTerdekat() {
@@ -17,71 +15,124 @@ function KeteranganBankSampahTerdekat() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Keterangan Bank Sampah Terdekat' title='Keterangan Bank Sampah Terdekat'
placeholder='pencarian' placeholder='Cari nama bank sampah...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKeteranganBankSampahTerdekat search={search}/> <ListKeteranganBankSampahTerdekat search={search} />
</Box> </Box>
); );
} }
function ListKeteranganBankSampahTerdekat({ search }: { search: string }) { function ListKeteranganBankSampahTerdekat({ search }: { search: string }) {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const router = useRouter(); const router = useRouter();
useEffect(() => { const {
keteranganState.findMany.load() data,
}, []) page,
totalPages,
loading,
load,
} = keteranganState.findMany;
const filteredData = (keteranganState.findMany.data || []).filter(item => { useShallowEffect(() => {
const keyword = search.toLowerCase(); load(page, 10, search);
return ( }, [page, search]);
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaTempatMaps.toLowerCase().includes(keyword)
);
});
if (!keteranganState.findMany.data) { const filteredData = 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 bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Keterangan Bank Sampah Terdekat' <Title order={4}>Daftar Bank Sampah Terdekat</Title>
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create' <Tooltip label="Tambah Bank Sampah" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Bank Sampah Terdekat</TableTh> onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create')}
<TableTh>Alamat</TableTh> >
<TableTh>Nama Tempat Maps</TableTh> Tambah Baru
<TableTh>Detail</TableTh> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Tempat di Maps</TableTh>
<TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd>{item.alamat}</TableTd> <TableTd>
<TableTd>{item.namaTempatMaps}</TableTd> <Text fw={500}>{item.name}</Text>
<TableTd> </TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}> <TableTd>
<IconDeviceImac size={20} /> <Box w={200}>
</Button> <Text lineClamp={2} truncate="end" fz="sm">
</TableTd> {item.alamat || '-'}
</TableTr> </Text>
))} </Box>
</TableTbody> </TableTd>
</Table> <TableTd>
<Text fz="sm">
{item.namaTempatMaps || '-'}
</Text>
</TableTd>
<TableTd>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}
>
<IconDeviceImac size={20} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4} align="center" py="xl">
<Text c="dimmed">Tidak ada data bank sampah terdekat</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
</Box> </Box>
); );

View File

@@ -3,7 +3,7 @@
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
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';
@@ -65,47 +65,85 @@ function EditProgramKreatifDesa() {
...stateSampah.update.form, ...stateSampah.update.form,
name: formData.name.trim(), name: formData.name.trim(),
icon: formData.icon.trim(), icon: formData.icon.trim(),
} };
await stateSampah.update.submit(); await stateSampah.update.submit();
toast.success('Data pengelolaan sampah berhasil diperbarui!');
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
} catch (error) { } catch (error) {
console.error("Error updating pengelolaan sampah:", error); console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah");
} }
} }
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" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button
</Button> variant="subtle"
</Box> onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pengelolaan Sampah Bank Sampah
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={3}>Edit List Pengelolaan Sampah Bank Sampah</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Pengelolaan Sampah"
placeholder="Masukkan nama pengelolaan sampah"
value={formData.name} value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama List Pengelolaan Sampah Bank Sampah</Text>} onChange={(e) => {
placeholder="masukkan nama list pengelolaan sampah bank sampah" const value = e.target.value;
onChange={(val) => { setFormData(prev => ({
setFormData({ ...prev,
...formData, name: value
name: val.target.value }));
}) stateSampah.update.form.name = value;
}} }}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon List Pengelolaan Sampah Bank Sampah</Text> <Text fw="bold" fz="sm" mb={6}>
Pilih Ikon
</Text>
<SelectIconProgramEdit <SelectIconProgramEdit
value={formData.icon as IconKey} value={formData.icon as IconKey}
onChange={(value) => { onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value })); setFormData(prev => ({ ...prev, icon: value }));
stateSampah.update.form.icon = value; stateSampah.update.form.icon = value;
}} }}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Group justify="right" mt="md">
<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>

View File

@@ -1,8 +1,8 @@
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon'; import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
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';
@@ -11,43 +11,83 @@ import { useProxy } from 'valtio/utils';
function CreatePengelolaanSampahBankSampah() { function CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah) const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateCreate.create.form = { stateCreate.create.form = {
name: "", name: "",
icon: "", icon: "",
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); try {
resetForm(); await stateCreate.create.create();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah") resetForm();
} router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
return ( } catch (error) {
<Box> console.error('Error creating pengelolaan sampah:', error);
<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={3}>Create List Pengelolaan Sampah Bank Sampah</Title> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
<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 Pengelolaan Sampah Bank Sampah
</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 fz={"sm"} fw={"bold"}>Nama Pengelolaan Sampah Bank Sampah</Text>} label="Nama Pengelolaan Sampah"
placeholder="masukkan nama pengelolaan sampah bank sampah" placeholder="Masukkan nama pengelolaan sampah"
onChange={(val) => stateCreate.create.form.name = val.target.value} value={stateCreate.create.form.name || ''}
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Pengelolaan Sampah Bank Sampah</Text> <Text fw="bold" fz="sm" mb={6}>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} /> Pilih Ikon
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<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>

View File

@@ -1,26 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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 } 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 { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled, IconX } from '@tabler/icons-react'; import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconPlus, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
import React from 'react'; import React from 'react';
function PengelolaanSampahBankSampah() { function PengelolaanSampahBankSampah() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Pengelolaan Sampah Bank Sampah' title='List Pengelolaan Sampah Bank Sampah'
placeholder='pencarian' placeholder='Cari pengelolaan sampah...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -36,9 +34,18 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateList.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
@@ -48,15 +55,9 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
} }
} }
const filteredData = (stateList.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
|| item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<{ size: number; style?: React.CSSProperties }>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,
kompetisi: IconTrophy, kompetisi: IconTrophy,
wisata: IconTent, wisata: IconTent,
@@ -68,7 +69,7 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
trash: IconTrashFilled, trash: IconTrashFilled,
}; };
if (!stateList.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -78,49 +79,107 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pengelolaan Sampah Bank Sampah' <Title order={4}>Daftar Pengelolaan Sampah Bank Sampah</Title>
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create' <Tooltip label="Tambah Pengelolaan Sampah" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Pengelolaan Sampah</TableTh> onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create')}
<TableTh>Icon</TableTh> >
<TableTh>Edit</TableTh> Tambah Baru
<TableTh>Delete</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Group>
<TableTbody>
{filteredData.map((item) => ( <Box style={{ overflowX: 'auto' }}>
<TableTr key={item.id}> <Table highlightOnHover>
<TableTd>{item.name}</TableTd> <TableThead>
<TableTd style={{ width: '10%' }}> <TableTr>
{iconMap[item.icon] && ( <TableTh>Nama Pengelolaan Sampah</TableTh>
<Box title={item.icon}> <TableTh>Icon</TableTh>
{React.createElement(iconMap[item.icon], { size: 24 })} <TableTh>Edit</TableTh>
</Box> <TableTh>Delete</TableTh>
)}
</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500}>{item.name}</Text>
</TableTd>
<TableTd>
{iconMap[item.icon] ? (
<Tooltip label={item.icon} withArrow>
<Box>
{React.createElement(iconMap[item.icon], {
size: 24,
style: { color: colors['blue-button'] }
})}
</Box>
</Tooltip>
) : (
<Text c="dimmed" fz="sm">-</Text>
)}
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrashFilled size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3} align="center" py="xl">
<Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -126,7 +126,9 @@ function ListProgramPenghijauan({ search }: { search: string }) {
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd> <TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.judul }}></TableTd> <TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
<Text lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.judul }}/>
</TableTd>
<TableTd style={{ width: '10%' }}> <TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && ( {iconMap[item.icon] && (
<Box title={item.icon}> <Box title={item.icon}>

View File

@@ -0,0 +1,144 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditProgramKreatifDesa() {
const state = useProxy(beasiswaDesaState.keunggulanProgram)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
state.update.id = id;
state.update.form = {
judul: data.judul,
deskripsi: data.deskripsi,
};
setFormData({
judul: data.judul,
deskripsi: data.deskripsi,
});
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
judul: formData.judul.trim(),
deskripsi: formData.deskripsi.trim(),
};
await state.update.update();
toast.success('Data keunggulan program berhasil diperbarui!');
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
} catch (error) {
console.error("Error updating keunggulan program:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data keunggulan program");
}
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" 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">
Edit Keunggulan Program
</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
label="Judul"
placeholder="Masukkan judul"
value={formData.judul}
onChange={(e) => {
const value = e.target.value;
setFormData(prev => ({
...prev,
judul: value
}));
state.update.form.judul = value;
}}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={state.update.form.deskripsi}
onChange={(htmlContent) => {
state.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group justify="right" mt="md">
<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>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,101 @@
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKeunggulanProgram() {
const stateCreate = useProxy(beasiswaDesaState.keunggulanProgram);
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
judul: "",
deskripsi: "",
};
};
const handleSubmit = async () => {
try {
await stateCreate.create.create();
resetForm();
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
} catch (error) {
console.error('Error creating keunggulan program:', error);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
<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 Keunggulan Program
</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
label="Judul"
placeholder="Masukkan judul"
value={stateCreate.create.form.judul || ''}
onChange={(e) => (stateCreate.create.form.judul = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => {
stateCreate.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group justify="right" mt="md">
<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>
</Paper>
</Box>
);
}
export default CreateKeunggulanProgram;

View File

@@ -1,71 +1,168 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } 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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrashFilled } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function BeasiswaDesa() { function KeunggulanProgram() {
const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Keunggulan Program' title='List Keunggulan Program'
placeholder='pencarian' placeholder='Cari keunggulan program...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListBeasiswaDesa/> <ListKeunggulanProgram search={search} />
</Box> </Box>
); );
} }
function ListBeasiswaDesa() { function ListKeunggulanProgram({ search }: { search: string }) {
const router = useRouter(); const stateList = useProxy(beasiswaDesaState.keunggulanProgram)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const handleHapus = () => {
if (selectedId) {
stateList.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Title order={4}>List Beasiswa Desa</Title> <Title order={4}>Daftar Keunggulan Program</Title>
<Box style={{overflowX: 'auto'}}> <Tooltip label="Tambah Keunggulan Program" withArrow>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}> <Button
<TableThead> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/pendidikan/beasiswa-desa/keunggulan-program/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Keunggulan Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500}>{item.judul}</Text>
</TableTd>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500} dangerouslySetInnerHTML={{ __html: item.deskripsi }}></Text>
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/keunggulan-program/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrashFilled size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr> <TableTr>
<TableTh>Nomor</TableTh> <TableTd colSpan={3} align="center" py="xl">
<TableTh>Nama Lengkap</TableTh> <Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>1</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Email</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/beasiswa-desa/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
</TableTbody> )}
</Table> </TableTbody>
</Box> </Table>
</Stack> </Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengelolaan sampah bank sampah ini?'
/>
</Box> </Box>
) );
} }
export default BeasiswaDesa; export default KeunggulanProgram;

Some files were not shown because too many files have changed in this diff Show More