Compare commits

...

34 Commits

Author SHA1 Message Date
8817b937b1 API Auth 2025-09-04 11:46:08 +08:00
2adf60f9eb Fix UI Admin menu desa 2025-09-03 15:30:02 +08:00
fa9601e126 Fix UI Admin Menu Pendidikam, Add Menu User & Role 2025-09-02 18:08:53 +08:00
7ae83788b4 Fix UI Admin Menu Landing Page & PPID 2025-09-01 16:14:28 +08:00
22ec8d942d Sinkronisasi UI & Admin - Submenu Perpustakaan Digital 2025-08-30 12:22:32 +08:00
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
49067f0218 Add Detail Semua Polsek, Submenu Polsek Terdekat, Menu Keamanan 2025-08-19 17:40:59 +08:00
d79425d529 Sinkronisasi UI & API Menu Keamanan, Admin - User Submenu Keamanan Lingkungan & Polse Terdekat 2025-08-19 11:12:39 +08:00
4491d23bea Sinkronisasi UI & API Admin - User Submenu Penanganan Darurat, User Submenu Kontak Darurat & User Submenu Info Wabah / Penyakit 2025-08-18 20:56:18 +08:00
1e154ced86 Sinkronisasi UI & API Admin - User Submenu Program Kesehatan 2025-08-18 17:14:33 +08:00
bcc51aec12 Sinkronisasi UI & API Admin - User Submenu Data Kesehatan Warga 2025-08-18 15:01:39 +08:00
8d15563f15 Sinkronisasi UI & API Admin - User Submenu Data Kesehatan Warga
-Dibagian Tanggal Gak Auto Ngambil Tanggal Yang Udah Dipakai
-Dibagian fasilitas kesehatan : data dokter dan tarif rencananya mau pakai select
2025-08-15 14:07:56 +08:00
d7a592c635 Fix UI & API Admin Menu Kesehatan, Submenu Data Kesehatan Warga Bagian ChartBar 2025-08-14 20:47:07 +08:00
5e137ba658 Fix Admin Submenu Posyandu, Menu Kesehatan, dan Sinkronisasi UI & API Admin - User Submenu Posyandu 2025-08-14 11:48:57 +08:00
c99416c7f8 Fix FileInput dengan Dropzone 2025-08-14 10:24:03 +08:00
600 changed files with 44561 additions and 22212 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -57,6 +57,8 @@
"form-data": "^4.0.2", "form-data": "^4.0.2",
"framer-motion": "^12.23.5", "framer-motion": "^12.23.5",
"get-port": "^7.1.0", "get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
"jotai": "^2.12.3", "jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -64,7 +66,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"motion": "^12.4.1", "motion": "^12.4.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next": "15.1.6", "next": "^15.5.2",
"next-view-transitions": "^0.3.4", "next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",

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

@@ -1,6 +0,0 @@
[
{
"id": "cmdpm429r0000vnndkcwslt0h",
"name": "warga"
}
]

View File

@@ -0,0 +1,29 @@
[
{
"id": "1",
"name": "ADMIN DESA",
"description": "Administrator Desa",
"permissions": ["manage_users", "manage_content", "view_reports"],
"isActive": true,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
},
{
"id": "2",
"name": "ADMIN KESEHATAN",
"description": "Administrator Bidang Kesehatan",
"permissions": ["manage_health_data", "view_reports"],
"isActive": true,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
},
{
"id": "3",
"name": "ADMIN SEKOLAH",
"description": "Administrator Sekolah",
"permissions": ["manage_school_data", "view_reports"],
"isActive": true,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
}
]

View File

@@ -0,0 +1,32 @@
[
{
"id": "1",
"nama": "Admin Desa",
"nomor": "089647037426",
"roleId": "1",
"isActive": true,
"lastLogin": "2025-08-31T10:00:00.000Z",
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
},
{
"id": "2",
"nama": "Admin Kesehatan",
"nomor": "082339004198",
"roleId": "2",
"isActive": true,
"lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
},
{
"id": "3",
"nama": "Admin Sekolah",
"nomor": "085237157222",
"roleId": "3",
"isActive": true,
"lastLogin": null,
"createdAt": "2025-09-01T00:00:00.000Z",
"updatedAt": "2025-09-01T00:00:00.000Z"
}
]

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 DateTime // misal: 2025-05-01 tanggal DateTime @db.Date // misal: 2025-05-01
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id]) jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
jenisKelaminId String jenisKelaminId String
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id]) rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
@@ -292,6 +293,9 @@ model PosisiOrganisasiPPID {
pegawai PegawaiPPID[] pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String? parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id]) parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent") children PosisiOrganisasiPPID[] @relation("Parent")
} }
@@ -911,26 +915,56 @@ model PendaftaranJadwalKegiatan {
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= // // ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
model DataKematian_Kelahiran { model DataKematian_Kelahiran {
id String @id @default(cuid()) id String @id @default(cuid())
tahun String kematian Kematian @relation(fields: [kematianId], references: [id])
kematianKasar String kematianId String
kematianBayi String kelahiran Kelahiran @relation(fields: [kelahiranId], references: [id])
kelahiranKasar String kelahiranId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
}
model Kelahiran {
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataKematian_Kelahiran DataKematian_Kelahiran[]
}
model Kematian {
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
penyebab String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataKematian_Kelahiran DataKematian_Kelahiran[]
} }
// ========================================= GRAFIK KEPUASAN ========================================= // // ========================================= GRAFIK KEPUASAN ========================================= //
model GrafikKepuasan { model GrafikKepuasan {
id String @id @default(cuid()) id String @id @default(cuid())
label String nama String
jumlah String tanggal DateTime
createdAt DateTime @default(now()) jenisKelamin String
updatedAt DateTime @updatedAt alamat String
deletedAt DateTime @default(now()) penyakit String
isActive Boolean @default(true) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
// ========================================= ARTIKEL KESEHATAN ========================================= // // ========================================= ARTIKEL KESEHATAN ========================================= //
@@ -1027,16 +1061,17 @@ model DoctorSign {
// ========================================= POSYANDU ========================================= // // ========================================= POSYANDU ========================================= //
model Posyandu { model Posyandu {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
nomor String nomor String
deskripsi String deskripsi String
image FileStorage @relation(fields: [imageId], references: [id]) jadwalPelayanan String
imageId String image FileStorage @relation(fields: [imageId], references: [id])
createdAt DateTime @default(now()) imageId String
updatedAt DateTime @updatedAt createdAt DateTime @default(now())
deletedAt DateTime @default(now()) updatedAt DateTime @updatedAt
isActive Boolean @default(true) deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
// ========================================= PUSKESMAS ========================================= // // ========================================= PUSKESMAS ========================================= //
@@ -1516,7 +1551,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran { model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20) month String @db.VarChar(20)
year Int year DateTime
totalUnemployment Int totalUnemployment Int
educatedUnemployment Int educatedUnemployment Int
uneducatedUnemployment Int uneducatedUnemployment Int
@@ -1604,18 +1639,27 @@ model ProgramKreatif {
// ========================================= KOLABORASI INOVASI ========================================= // // ========================================= KOLABORASI INOVASI ========================================= //
model KolaborasiInovasi { model KolaborasiInovasi {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
tahun Int tahun Int
slug String @db.Text //deskripsi singkat slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang deskripsi String @db.Text //deskripsi panjang
kolaborator String kolaborator String
image FileStorage @relation(fields: [imageId], references: [id]) createdAt DateTime @default(now())
imageId String updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) deletedAt DateTime @default(now())
updatedAt DateTime @updatedAt isActive Boolean @default(true)
deletedAt DateTime @default(now()) }
isActive Boolean @default(true)
model MitraKolaborasi {
id String @id @default(cuid())
name String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
} }
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= // // ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
@@ -2059,26 +2103,66 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[] DataPerpustakaan DataPerpustakaan[]
} }
// ========================================= USER ========================================= //
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
nama String username String
email String @unique nomor String @unique
password String role Role @relation(fields: [roleId], references: [id])
role Role @relation(fields: [roleId], references: [id]) roleId String @default("1")
roleId String instansi String?
isActive Boolean @default(true) UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
createdAt DateTime @default(now()) isActive Boolean @default(true)
updatedAt DateTime @updatedAt lastLogin DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
} }
model Role { model Role {
id String @id @default(cuid())
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
description String?
permissions Json // Menyimpan permission dalam format JSON
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
users User[]
@@map("roles")
}
model KodeOtp {
id String @id @default(cuid()) id String @id @default(cuid())
name String isActive Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) nomor String
isActive Boolean @default(true) otp Int
User User[] }
// Tabel untuk menyimpan permission
model Permission {
id String @id @default(cuid())
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("permissions")
}
model UserSession {
id String @id @default(cuid())
token String
expires DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
User User @relation(fields: [userId], references: [id])
userId String @unique
} }
// ========================================= DATA PENDIDIKAN ========================================= // // ========================================= DATA PENDIDIKAN ========================================= //

View File

@@ -1,57 +1,119 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json"; import profilePejabatDesa from "./data/landing-page/profile/profile.json";
import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
import programInovasi from "./data/landing-page/profile/programInovasi.json"; import programInovasi from "./data/landing-page/profile/programInovasi.json";
import mediaSosial from "./data/landing-page/profile/mediaSosial.json"; import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
import desaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/desaantiKorpusi.json";
import kategoriDesaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json";
import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json"; import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json";
import apbdes from "./data/landing-page/apbdes/apbdes.json"; import apbdes from "./data/landing-page/apbdes/apbdes.json";
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json"; import kategoriPrestasiDesa from "./data/landing-page/prestasi-desa/kategori-prestasi.json";
import categoryPengumuman from "./data/category-pengumuman.json"; import prestasiDesa from "./data/landing-page/prestasi-desa/prestasi-desa.json";
import kategoriBerita from "./data/kategori-berita.json"; import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json";
import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
import layanan from "./data/list-layanan.json";
import potensi from "./data/list-potensi.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json"; import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json"; import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json"
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json"; import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json"; import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import categoryPengumuman from "./data/category-pengumuman.json";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json"; import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json"; import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import lambangDesa from "./data/desa/profile/lambang_desa.json"; import lambangDesa from "./data/desa/profile/lambang_desa.json";
import maskotDesa from "./data/desa/profile/maskot_desa.json"; import maskotDesa from "./data/desa/profile/maskot_desa.json";
import profilPerbekel from "./data/desa/profile/profil_perbekel.json"; import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json"; import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json"; import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json"; import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json"; import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json"; import kategoriBerita from "./data/kategori-berita.json";
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json"; import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json"; import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json"; import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json"; import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json";
import jenisInformasiDiminta from "./data/list-jenisInfromasi.json";
import potensi from "./data/list-potensi.json";
import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json"; import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json"; import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json"; import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json"; import roles from "./data/user/roles.json";
import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json"; import users from "./data/user/users.json";
import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
(async () => { (async () => {
// =========== USER & ROLE ===========
// In your seed.ts
// =========== ROLES ===========
console.log("🔄 Seeding roles...");
for (const r of roles) {
await prisma.role.upsert({
where: { id: r.id },
update: {
name: r.name,
description: r.description,
permissions: r.permissions,
isActive: r.isActive,
},
create: {
id: r.id,
name: r.name,
description: r.description,
permissions: r.permissions,
isActive: r.isActive,
},
});
}
console.log("✅ Roles seeded");
// =========== USERS ===========
console.log("🔄 Seeding users...");
for (const u of users) {
// First verify the role exists
const roleExists = await prisma.role.findUnique({
where: { id: u.roleId }
});
if (!roleExists) {
console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
continue;
}
await prisma.user.upsert({
where: { id: u.id },
update: {
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
},
create: {
id: u.id,
username: u.nama,
nomor: u.nomor,
roleId: u.roleId,
isActive: u.isActive,
},
});
}
console.log("✅ Users seeded");
// =========== LANDING PAGE =========== // =========== LANDING PAGE ===========
// =========== PROFILE =========== // =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA ===========
for (const p of profilePejabatDesa) { for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({ await prisma.pejabatDesa.upsert({
where: { id: p.id }, where: { id: p.id },
@@ -106,6 +168,90 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
} }
console.log("media sosial success ..."); console.log("media sosial success ...");
// =========== SUBMENU DESA ANTI KORUPSI ===========
// =========== KATEGORI DESA ANTI KORUPSI ===========
for (const k of kategoriDesaAntiKorupsi) {
await prisma.kategoriDesaAntiKorupsi.upsert({
where: { id: k.id },
update: {
name: k.name,
},
create: {
id: k.id,
name: k.name,
},
});
}
console.log("kategori desa anti korupsi success ...");
// =========== DESA ANTI KORUPSI ===========
for (const p of desaAntiKorupsi) {
await prisma.desaAntiKorupsi.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
});
}
console.log("desa anti korupsi success ...");
// =========== KATEGORI DESA ANTI KORUPSI ===========
for (const p of kategoriDesaAntiKorupsi) {
await prisma.kategoriDesaAntiKorupsi.upsert({
where: { id: p.id },
update: {
name: p.name,
},
create: {
id: p.id,
name: p.name,
},
});
}
console.log("desa anti korupsi success ...");
// =========== KATEGORI PRESTASI DESA===========
for (const c of kategoriPrestasiDesa) {
await prisma.kategoriPrestasiDesa.upsert({
where: { id: c.id },
update: {
name: c.name,
},
create: {
id: c.id,
name: c.name,
},
});
}
console.log("kategori prestasi desa success ...");
// =========== PRESTASI DESA===========
for (const p of prestasiDesa) {
await prisma.prestasiDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
});
}
console.log("prestasi desa success ...");
// =========== PENGHARGAAN =========== // =========== PENGHARGAAN ===========
for (const p of penghargaan) { for (const p of penghargaan) {
await prisma.penghargaan.upsert({ await prisma.penghargaan.upsert({
@@ -125,8 +271,8 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
} }
console.log("penghargaan success ..."); console.log("penghargaan success ...");
// =========== LAYANAN DESA =========== // =========== LAYANAN DESA ===========
for (const p of pelayananSuratKeterangan) { for (const p of pelayananSuratKeterangan) {
await prisma.pelayananSuratKeterangan.upsert({ await prisma.pelayananSuratKeterangan.upsert({
where: { id: p.id }, where: { id: p.id },
update: { update: {
@@ -160,23 +306,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
} }
console.log("pelayanan surat keterangan success ..."); console.log("pelayanan surat keterangan success ...");
// =========== LAYANAN ===========
for (const l of layanan) {
await prisma.layanan.upsert({
where: {
name: l.name,
},
update: {
name: l.name,
},
create: {
name: l.name,
},
});
}
console.log("layanan success ...");
// =========== SDGSDesa =========== // =========== SDGSDesa ===========
for (const l of sdgsDesa) { for (const l of sdgsDesa) {
await prisma.sDGSDesa.upsert({ await prisma.sDGSDesa.upsert({
@@ -217,6 +346,9 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("sdgs desa success ..."); console.log("sdgs desa success ...");
// =========== MENU DESA ===========
// =========== SUBMENU PROFILE ===========
// =========== SEJARAH DESA ===========
for (const l of sejarahDesa) { for (const l of sejarahDesa) {
await prisma.sejarahDesa.upsert({ await prisma.sejarahDesa.upsert({
where: { where: {
@@ -236,6 +368,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("sejarah desa success ..."); console.log("sejarah desa success ...");
// =========== MASKOT DESA ===========
for (const l of maskotDesa) { for (const l of maskotDesa) {
await prisma.maskotDesa.upsert({ await prisma.maskotDesa.upsert({
where: { where: {
@@ -255,6 +388,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("maskot desa success ..."); console.log("maskot desa success ...");
// =========== LAMBANG DESA ===========
for (const l of lambangDesa) { for (const l of lambangDesa) {
await prisma.lambangDesa.upsert({ await prisma.lambangDesa.upsert({
where: { where: {
@@ -274,6 +408,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("lambang desa success ..."); console.log("lambang desa success ...");
// =========== PROFIL PERBEKEL ===========
for (const c of profilPerbekel) { for (const c of profilPerbekel) {
await prisma.profilPerbekel.upsert({ await prisma.profilPerbekel.upsert({
where: { id: c.id }, where: { id: c.id },
@@ -298,6 +433,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
"✅ profilePerbekel seeded without imageId (editable later via UI)" "✅ profilePerbekel seeded without imageId (editable later via UI)"
); );
// =========== VISI MISI DESA ===========
for (const l of visiMisiDesa) { for (const l of visiMisiDesa) {
await prisma.visiMisiDesa.upsert({ await prisma.visiMisiDesa.upsert({
where: { where: {
@@ -317,63 +453,134 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("visi misi desa success ..."); console.log("visi misi desa success ...");
// Flatten the nested array structure for posisiOrganisasiPPID // =========== MENU PPID ===========
const flattenedPosisiOrganisasiPPID = posisiOrganisasiPPID.flat(); // =========== SUBMENU PROFILE PPID ===========
for (const c of profilePPID) {
await prisma.profilePPID.upsert({
where: { id: c.id },
update: {
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-update
},
create: {
id: c.id,
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-create
},
});
}
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
// =========== SUBMENU STRUKTUR PPID ===========
// =========== POSISI ORGANISASI PPID ===========
const flattenedPosisi = posisiOrganisasiPPID.flat();
// ✅ Urutkan berdasarkan hierarki
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
for (const p of sortedPosisi) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
if (p.parentId) {
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
if (!parentExists) {
console.warn(
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
);
continue;
}
}
for (const p of flattenedPosisiOrganisasiPPID) {
await prisma.posisiOrganisasiPPID.upsert({ await prisma.posisiOrganisasiPPID.upsert({
where: { where: { id: p.id },
id: p.id, update: p,
}, create: p,
update: {
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
parentId: p.parentId,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
parentId: p.parentId,
},
}); });
} }
console.log("posisi organisasi success ..."); console.log("posisi organisasi berhasil");
// Flatten the nested array structure for pegawaiPPID // =========== PEGAWAI PPID ===========
const flattenedPegawaiPPID = pegawaiPPID.flat(); const flattenedPegawai = pegawaiPPID.flat();
for (const p of flattenedPegawai) {
for (const p of flattenedPegawaiPPID) {
await prisma.pegawaiPPID.upsert({ await prisma.pegawaiPPID.upsert({
where: { id: p.id },
update: p,
create: p,
});
}
console.log("pegawai berhasil");
// =========== SUBMENU VISI MISI PPID ===========
for (const v of visiMisiPPID) {
await prisma.visiMisiPPID.upsert({
where: { where: {
id: p.id, id: v.id,
}, },
update: { update: {
namaLengkap: p.namaLengkap, misi: v.misi,
tanggalMasuk: new Date(p.tanggalMasuk), visi: v.visi,
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
}, },
create: { create: {
id: p.id, id: v.id,
namaLengkap: p.namaLengkap, misi: v.misi,
tanggalMasuk: new Date(p.tanggalMasuk), visi: v.visi,
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
}, },
}); });
} }
console.log("pegawai success ..."); console.log("visi misi PPID success ...");
// =========== SUBMENU DASAR HUKUM PPID ===========
for (const v of dasarHukumPPID) {
await prisma.dasarHukumPPID.upsert({
where: {
id: v.id,
},
update: {
judul: v.judul,
content: v.content,
},
create: {
id: v.id,
judul: v.judul,
content: v.content,
},
});
}
console.log("dasar hukum PPID success ...");
// =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID ===========
for (const v of daftarInformasiPublik) {
// Convert string date to Date object
const tanggal = new Date(v.tanggal);
await prisma.daftarInformasiPublik.upsert({
where: {
id: v.id,
},
update: {
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
create: {
id: v.id,
jenisInformasi: v.jenisInformasi,
deskripsi: v.deskripsi,
tanggal: tanggal,
},
});
}
console.log("daftar informasi publik PPID success ...");
for (const l of pelayananPerizinanBerusaha) { for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({ await prisma.pelayananPerizinanBerusaha.upsert({
@@ -507,48 +714,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
} }
console.log("cara memperoleh salinan informasi success ..."); console.log("cara memperoleh salinan informasi success ...");
for (const c of profilePPID) {
await prisma.profilePPID.upsert({
where: { id: c.id },
update: {
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-update
},
create: {
id: c.id,
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-create
},
});
}
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
for (const v of visiMisiPPID) {
await prisma.visiMisiPPID.upsert({
where: {
id: v.id,
},
update: {
misi: v.misi,
visi: v.visi,
},
create: {
id: v.id,
misi: v.misi,
visi: v.visi,
},
});
}
console.log("visi misi PPID success ...");
for (const j of jenisKelamin) { for (const j of jenisKelamin) {
await prisma.jenisKelaminResponden.upsert({ await prisma.jenisKelaminResponden.upsert({
where: { where: {
@@ -597,24 +762,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
} }
console.log("umur responden success ..."); console.log("umur responden success ...");
for (const v of dasarHukumPPID) {
await prisma.dasarHukumPPID.upsert({
where: {
id: v.id,
},
update: {
judul: v.judul,
content: v.content,
},
create: {
id: v.id,
judul: v.judul,
content: v.content,
},
});
}
console.log("dasar hukum PPID success ...");
for (const k of kategoriProduk) { for (const k of kategoriProduk) {
await prisma.kategoriProduk.upsert({ await prisma.kategoriProduk.upsert({
where: { where: {
@@ -702,9 +849,12 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("hubungan organisasi success ..."); console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) { for (const d of detailDataPengangguran) {
// Convert the year to a Date object (using January 1st of the year as the date)
const yearAsDate = new Date(d.year, 0, 1);
await prisma.detailDataPengangguran.upsert({ await prisma.detailDataPengangguran.upsert({
where: { where: {
month_year: { month: d.month, year: d.year }, month_year: { month: d.month, year: yearAsDate },
}, },
update: { update: {
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
@@ -714,7 +864,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
}, },
create: { create: {
month: d.month, month: d.month,
year: d.year, year: yearAsDate,
totalUnemployment: d.totalUnemployment, totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment, educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment, uneducatedUnemployment: d.uneducatedUnemployment,

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

@@ -74,18 +74,18 @@ const berita = proxy({
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "", search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => { load: async (page = 1, limit = 10, search = "", kategori = "") => {
berita.findMany.loading = true; // ✅ Akses langsung via nama path berita.findMany.loading = true; // ✅ Akses langsung via nama path
berita.findMany.page = page; berita.findMany.page = page;
berita.findMany.search = search; berita.findMany.search = search;
try { try {
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
if (kategori) query.kategori = kategori; if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query }); const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? []; berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1; berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -368,11 +368,37 @@ const kategoriBerita = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBerita.findMany.data = res.data?.data ?? []; kategoriBerita.findMany.page = page;
kategoriBerita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.kategoriberita[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBerita.findMany.data = res.data.data ?? [];
kategoriBerita.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
kategoriBerita.findMany.data = [];
kategoriBerita.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori berita paginated:", err);
kategoriBerita.findMany.data = [];
kategoriBerita.findMany.totalPages = 1;
} finally {
kategoriBerita.findMany.loading = false;
} }
}, },
}, },

View File

@@ -30,7 +30,6 @@ const templateTelunjukSaktiDesaForm = z.object({
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
}); });
const templatePelayananPerizinanBerusaha = z.object({ const templatePelayananPerizinanBerusaha = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"), name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
@@ -72,7 +71,6 @@ const pelayananPendudukNonPermanenForm = {
deskripsi: "", deskripsi: "",
}; };
const suratKeterangan = proxy({ const suratKeterangan = proxy({
create: { create: {
form: { ...suratKeteranganForm }, form: { ...suratKeteranganForm },
@@ -113,16 +111,21 @@ const suratKeterangan = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
suratKeterangan.findMany.loading = true; // Use the full path to access the property load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
suratKeterangan.findMany.loading = true; // Use the full path to access the property
suratKeterangan.findMany.page = page; suratKeterangan.findMany.page = page;
suratKeterangan.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[ const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
suratKeterangan.findMany.data = res.data.data || []; suratKeterangan.findMany.data = res.data.data || [];
suratKeterangan.findMany.total = res.data.total || 0; suratKeterangan.findMany.total = res.data.total || 0;
@@ -341,28 +344,34 @@ const pelayananTelunjukSaktiDesa = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
pelayananTelunjukSaktiDesa.findMany.loading = true; // Use the full path to access the property
pelayananTelunjukSaktiDesa.findMany.page = page; pelayananTelunjukSaktiDesa.findMany.page = page;
pelayananTelunjukSaktiDesa.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[ const res = await ApiFetch.api.desa.layanan.pelayanantelunjuksaktidesa[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pelayananTelunjukSaktiDesa.findMany.data = res.data.data || []; pelayananTelunjukSaktiDesa.findMany.data = res.data.data || [];
pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0; pelayananTelunjukSaktiDesa.findMany.total = res.data.total || 0;
pelayananTelunjukSaktiDesa.findMany.totalPages = res.data.totalPages || 1; pelayananTelunjukSaktiDesa.findMany.totalPages =
res.data.totalPages || 1;
} else { } else {
console.error("Failed to load telunjuk sakti desa:", res.data?.message); console.error("Failed to load surat keterangan:", res.data?.message);
pelayananTelunjukSaktiDesa.findMany.data = []; pelayananTelunjukSaktiDesa.findMany.data = [];
pelayananTelunjukSaktiDesa.findMany.total = 0; suratKeterangan.findMany.total = 0;
pelayananTelunjukSaktiDesa.findMany.totalPages = 1; suratKeterangan.findMany.totalPages = 1;
} }
} catch (error) { } catch (error) {
console.error("Error loading telunjuk sakti desa:", error); console.error("Error loading surat keterangan:", error);
pelayananTelunjukSaktiDesa.findMany.data = []; pelayananTelunjukSaktiDesa.findMany.data = [];
pelayananTelunjukSaktiDesa.findMany.total = 0; pelayananTelunjukSaktiDesa.findMany.total = 0;
pelayananTelunjukSaktiDesa.findMany.totalPages = 1; pelayananTelunjukSaktiDesa.findMany.totalPages = 1;
@@ -410,7 +419,9 @@ const pelayananTelunjukSaktiDesa = proxy({
); );
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
toast.success(result.message || "Telunjuk Sakti Desa berhasil dihapus"); toast.success(
result.message || "Telunjuk Sakti Desa berhasil dihapus"
);
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
} else { } else {
toast.error(result.message || "Gagal menghapus telunjuk sakti desa"); toast.error(result.message || "Gagal menghapus telunjuk sakti desa");
@@ -501,7 +512,9 @@ const pelayananTelunjukSaktiDesa = proxy({
} }
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
toast.success(result.message || "Telunjuk Sakti Desa berhasil diupdate"); toast.success(
result.message || "Telunjuk Sakti Desa berhasil diupdate"
);
await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list await pelayananTelunjukSaktiDesa.findMany.load(); // refresh list
return true; return true;
} else { } else {
@@ -522,7 +535,7 @@ const pelayananTelunjukSaktiDesa = proxy({
} }
}, },
}, },
}) });
const pelayananPerizinanBerusaha = proxy({ const pelayananPerizinanBerusaha = proxy({
findById: { findById: {
@@ -596,9 +609,7 @@ const pelayananPerizinanBerusaha = proxy({
} catch (error) { } catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error); console.error("Error fetching pelayanan perizinan berusaha:", error);
toast.error( toast.error(
error instanceof Error error instanceof Error ? error.message : "Gagal memuat data"
? error.message
: "Gagal memuat data"
); );
return null; return null;
} }
@@ -713,9 +724,7 @@ const pelayananPendudukNonPermanen = proxy({
} catch (error) { } catch (error) {
console.error("Error fetching pelayanan penduduk non permanen:", error); console.error("Error fetching pelayanan penduduk non permanen:", error);
toast.error( toast.error(
error instanceof Error error instanceof Error ? error.message : "Gagal memuat data"
? error.message
: "Gagal memuat data"
); );
return null; return null;
} }

View File

@@ -56,16 +56,21 @@ const penghargaanState = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
penghargaanState.findMany.loading = true; // Use the full path to access the property load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
penghargaanState.findMany.loading = true; // Use the full path to access the property
penghargaanState.findMany.page = page; penghargaanState.findMany.page = page;
penghargaanState.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.penghargaan[ const res = await ApiFetch.api.desa.penghargaan[
"find-many" "find-many"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
penghargaanState.findMany.data = res.data.data || []; penghargaanState.findMany.data = res.data.data || [];
penghargaanState.findMany.total = res.data.total || 0; penghargaanState.findMany.total = res.data.total || 0;

View File

@@ -55,11 +55,39 @@ const category = proxy({
pengumumans: number; pengumumans: number;
}; };
})[], })[],
page: 1,
totalPages: 1,
total: 0,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get(); load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
if (res.status === 200) { category.findMany.loading = true; // Use the full path to access the property
category.findMany.data = res.data?.data ?? []; category.findMany.page = page;
category.findMany.search = search;
try {
const res = await ApiFetch.api.desa.kategoripengumuman[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
category.findMany.data = res.data.data || [];
category.findMany.total = res.data.total || 0;
category.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load potensi desa:", res.data?.message);
category.findMany.data = [];
category.findMany.total = 0;
category.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading potensi desa:", error);
category.findMany.data = [];
category.findMany.total = 0;
category.findMany.totalPages = 1;
} finally {
category.findMany.loading = false;
} }
}, },
}, },

View File

@@ -56,9 +56,11 @@ const potensiDesa = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
potensiDesa.findMany.loading = true; // Use the full path to access the property potensiDesa.findMany.loading = true; // Use the full path to access the property
potensiDesa.findMany.page = page; potensiDesa.findMany.page = page;
potensiDesa.findMany.search = search;
try { try {
const res = await ApiFetch.api.desa.potensi[ const res = await ApiFetch.api.desa.potensi[
"find-many" "find-many"
@@ -298,11 +300,34 @@ const kategoriPotensi = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { kategoriPotensi.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriPotensi.findMany.data = res.data?.data ?? []; kategoriPotensi.findMany.page = page;
kategoriPotensi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.kategoripotensi["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriPotensi.findMany.data = res.data.data ?? [];
kategoriPotensi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriPotensi.findMany.data = [];
kategoriPotensi.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori potensi paginated:", err);
kategoriPotensi.findMany.data = [];
kategoriPotensi.findMany.totalPages = 1;
} finally {
kategoriPotensi.findMany.loading = false;
} }
}, },
}, },

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 keamananLingkunganState = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.KeamananLingkunganGetPayload<{ | Prisma.KeamananLingkunganGetPayload<{
include: { image: true }; include: {
image: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.keamananlingkungan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
keamananLingkunganState.findMany.data = res.data?.data ?? []; keamananLingkunganState.findMany.loading = true; // ✅ Akses langsung via nama path
keamananLingkunganState.findMany.page = page;
keamananLingkunganState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.keamananlingkungan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
keamananLingkunganState.findMany.data = res.data.data ?? [];
keamananLingkunganState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
keamananLingkunganState.findMany.data = [];
keamananLingkunganState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch keamanan lingkungan paginated:", err);
keamananLingkunganState.findMany.data = [];
keamananLingkunganState.findMany.totalPages = 1;
} finally {
keamananLingkunganState.findMany.loading = false;
} }
}, },
}, },

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";
@@ -63,13 +64,41 @@ const polsekTerdekatState = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.PolsekTerdekatGetPayload<{ | Prisma.PolsekTerdekatGetPayload<{
include: { layananPolsek: true }; include: {
layananPolsek: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get(); totalPages: 1,
if (res.status === 200) { loading: false,
polsekTerdekatState.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
polsekTerdekatState.findMany.loading = true; // ✅ Akses langsung via nama path
polsekTerdekatState.findMany.page = page;
polsekTerdekatState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get(
{ query }
);
if (res.status === 200 && res.data?.success) {
polsekTerdekatState.findMany.data = res.data.data ?? [];
polsekTerdekatState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
polsekTerdekatState.findMany.data = [];
polsekTerdekatState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch polsek terdekat paginated:", err);
polsekTerdekatState.findMany.data = [];
polsekTerdekatState.findMany.totalPages = 1;
} finally {
polsekTerdekatState.findMany.loading = false;
} }
}, },
}, },
@@ -237,6 +266,29 @@ const polsekTerdekatState = proxy({
polsekTerdekatState.edit.form = { ...defaultForm }; polsekTerdekatState.edit.form = { ...defaultForm };
}, },
}, },
findFirst: {
data: null as Prisma.PolsekTerdekatGetPayload<{
include: {
layananPolsek: true;
};
}> | null,
loading: false,
load: async () => { // Changed to arrow function
polsekTerdekatState.findFirst.loading = true;
try {
const res = await ApiFetch.api.keamanan.polsekterdekat["find-first"].get();
if (res.status === 200 && res.data?.success) {
polsekTerdekatState.findFirst.data = res.data.data || null;
} else {
polsekTerdekatState.findFirst.data = null;
}
} catch (err) {
console.error("Gagal fetch polsek terdekat terbaru:", err);
} finally {
polsekTerdekatState.findFirst.loading = false;
}
}
}
}); });
export default polsekTerdekatState; export default polsekTerdekatState;

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

@@ -5,6 +5,7 @@ import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
//fasilitas kesehatan aja
// Validasi form // Validasi form
const templateForm = z.object({ const templateForm = z.object({
name: z.string().min(1, "Nama harus diisi"), name: z.string().min(1, "Nama harus diisi"),
@@ -61,7 +62,7 @@ const defaultForm = {
}, },
}; };
const fasilitasKesehatanState = proxy({ const fasilitasKesehatan = proxy({
create: { create: {
form: { ...defaultForm }, form: { ...defaultForm },
loading: false, loading: false,
@@ -86,7 +87,7 @@ const fasilitasKesehatanState = proxy({
if (res.status === 200) { if (res.status === 200) {
toast.success("Berhasil menambahkan fasilitas kesehatan"); toast.success("Berhasil menambahkan fasilitas kesehatan");
this.resetForm(); this.resetForm();
await fasilitasKesehatanState.findMany.load(); await fasilitasKesehatan.findMany.load();
return res.data; return res.data;
} }
} catch (err: any) { } catch (err: any) {
@@ -102,7 +103,6 @@ const fasilitasKesehatanState = proxy({
this.form = { ...defaultForm }; this.form = { ...defaultForm };
}, },
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.FasilitasKesehatanGetPayload<{ | Prisma.FasilitasKesehatanGetPayload<{
@@ -156,7 +156,7 @@ const fasilitasKesehatanState = proxy({
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`); const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
fasilitasKesehatanState.findUnique.data = data.data ?? null; fasilitasKesehatan.findUnique.data = data.data ?? null;
} else { } else {
toast.error("Gagal load data fasilitas kesehatan"); toast.error("Gagal load data fasilitas kesehatan");
} }
@@ -176,8 +176,8 @@ const fasilitasKesehatanState = proxy({
const result = await res.json(); const result = await res.json();
const data = result.data; const data = result.data;
fasilitasKesehatanState.edit.id = data.id; fasilitasKesehatan.edit.id = data.id;
fasilitasKesehatanState.edit.form = { fasilitasKesehatan.edit.form = {
name: data.name, name: data.name,
informasiUmum: { informasiUmum: {
fasilitas: data.informasiumum.fasilitas, fasilitas: data.informasiumum.fasilitas,
@@ -205,7 +205,7 @@ const fasilitasKesehatanState = proxy({
}; };
}, },
async submit() { async submit() {
const cek = templateForm.safeParse(fasilitasKesehatanState.edit.form); const cek = templateForm.safeParse(fasilitasKesehatan.edit.form);
if (!cek.success) { if (!cek.success) {
const errMsg = cek.error.issues const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`) .map((v) => `${v.path.join(".")}: ${v.message}`)
@@ -215,42 +215,38 @@ const fasilitasKesehatanState = proxy({
} }
try { try {
fasilitasKesehatanState.edit.loading = true; fasilitasKesehatan.edit.loading = true;
const payload = { const payload = {
name: fasilitasKesehatanState.edit.form.name, name: fasilitasKesehatan.edit.form.name,
informasiUmum: { informasiUmum: {
fasilitas: fasilitas: fasilitasKesehatan.edit.form.informasiUmum.fasilitas,
fasilitasKesehatanState.edit.form.informasiUmum.fasilitas, alamat: fasilitasKesehatan.edit.form.informasiUmum.alamat,
alamat: fasilitasKesehatanState.edit.form.informasiUmum.alamat,
jamOperasional: jamOperasional:
fasilitasKesehatanState.edit.form.informasiUmum.jamOperasional, fasilitasKesehatan.edit.form.informasiUmum.jamOperasional,
}, },
layananUnggulan: { layananUnggulan: {
content: fasilitasKesehatanState.edit.form.layananUnggulan.content, content: fasilitasKesehatan.edit.form.layananUnggulan.content,
}, },
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.name, name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
specialist: specialist:
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.specialist, fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
jadwal: jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.jadwal,
}, },
fasilitasPendukung: { fasilitasPendukung: {
content: content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
fasilitasKesehatanState.edit.form.fasilitasPendukung.content,
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
fasilitasKesehatanState.edit.form.prosedurPendaftaran.content,
}, },
tarifDanLayanan: { tarifDanLayanan: {
layanan: fasilitasKesehatanState.edit.form.tarifDanLayanan.layanan, layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
tarif: fasilitasKesehatanState.edit.form.tarifDanLayanan.tarif, tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
}, },
}; };
const res = await fetch( const res = await fetch(
`/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatanState.edit.id}`, `/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatan.edit.id}`,
{ {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -264,7 +260,7 @@ const fasilitasKesehatanState = proxy({
} }
toast.success("Berhasil update fasilitas kesehatan"); toast.success("Berhasil update fasilitas kesehatan");
await fasilitasKesehatanState.findMany.load(); await fasilitasKesehatan.findMany.load();
return true; return true;
} catch (err) { } catch (err) {
toast.error( toast.error(
@@ -272,37 +268,297 @@ const fasilitasKesehatanState = proxy({
); );
return false; return false;
} finally { } finally {
fasilitasKesehatanState.edit.loading = false; fasilitasKesehatan.edit.loading = false;
} }
}, },
resetForm() { resetForm() {
fasilitasKesehatanState.edit.id = ""; fasilitasKesehatan.edit.id = "";
fasilitasKesehatanState.edit.form = { ...defaultForm }; fasilitasKesehatan.edit.form = { ...defaultForm };
}, },
}, },
delete: { delete: {
loading: false, loading: false,
async byId(id: string){ async byId(id: string) {
try { try {
fasilitasKesehatanState.delete.loading = true; fasilitasKesehatan.delete.loading = true;
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/del/${id}`, { const res = await fetch(
method: "DELETE", `/api/kesehatan/fasilitas-kesehatan/del/${id}`,
}); {
method: "DELETE",
}
);
const result = await res.json(); const result = await res.json();
if (res.ok && result.success) { if (res.ok && result.success) {
toast.success("Fasilitas kesehatan berhasil dihapus"); toast.success("Fasilitas kesehatan berhasil dihapus");
await fasilitasKesehatanState.findMany.load(); await fasilitasKesehatan.findMany.load();
} else { } else {
toast.error(result.message || "Gagal menghapus"); toast.error(result.message || "Gagal menghapus");
} }
} catch { } catch {
toast.error("Terjadi kesalahan saat menghapus"); toast.error("Terjadi kesalahan saat menghapus");
} finally { } finally {
fasilitasKesehatanState.delete.loading = false; fasilitasKesehatan.delete.loading = false;
} }
} },
}, },
}); });
//dokter & tenaga medis
const templateDokterForm = z.object({
name: z.string().min(1, "Nama tidak boleh kosong"),
specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
});
const defaultDokterForm = {
name: "",
specialist: "",
jadwal: "",
};
const dokter = proxy({
create: {
create: {
form: defaultDokterForm,
loading: false,
async create() {
const cek = templateDokterForm.safeParse(dokter.create.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
dokter.create.create.loading = true;
const res = await ApiFetch.api.kesehatan.doktertenagamedis[
"create"
].post(dokter.create.create.form);
if (res.status === 200) {
const id = res.data?.data;
if (id) {
toast.success("Success create");
dokter.create.create.form = { ...defaultDokterForm };
dokter.findMany.load();
return id;
}
}
toast.error("failed create");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
dokter.create.create.loading = false;
}
},
},
},
findMany: {
data: null as
| Prisma.DokterdanTenagaMedisGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
dokter.findMany.loading = true; // ✅ Akses langsung via nama path
dokter.findMany.page = page;
dokter.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.doktertenagamedis[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
dokter.findMany.data = res.data.data ?? [];
dokter.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dokter.findMany.data = [];
dokter.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch dokter tenaga medis paginated:", err);
dokter.findMany.data = [];
dokter.findMany.totalPages = 1;
} finally {
dokter.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.DokterdanTenagaMedisGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`);
if (res.ok) {
const data = await res.json();
dokter.findUnique.data = data.data ?? null;
} else {
console.error(
"Failed to fetch dokter dan tenaga medis",
res.statusText
);
dokter.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching dokter dan tenaga medis", error);
dokter.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultDokterForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
specialist: data.specialist,
jadwal: data.jadwal,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading dokter dan tenaga medis:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
name: this.form.name,
specialist: this.form.specialist,
jadwal: this.form.jadwal,
};
const cek = templateDokterForm.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/doktertenagamedis/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await dokter.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data dokter dan tenaga medis");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
dokter.delete.loading = true;
const response = await fetch(
`/api/kesehatan/doktertenagamedis/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Dokter dan tenaga medis berhasil dihapus"
);
await dokter.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus dokter dan tenaga medis"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus dokter dan tenaga medis");
} finally {
dokter.delete.loading = false;
}
},
},
});
const fasilitasKesehatanState = proxy({
fasilitasKesehatan,
dokter
});
export default fasilitasKesehatanState; export default fasilitasKesehatanState;

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";
@@ -5,20 +6,19 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateGrafikKepuasan = z.object({ const templateGrafikKepuasan = z.object({
label: z.string().min(2, "Label harus diisi"), nama: z.string().min(2, "Nama harus diisi"),
jumlah: z.string().min(1, "Jumlah harus diisi"), tanggal: z.string().min(1, "Tanggal harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
penyakit: z.string().min(1, "Penyakit harus diisi"),
}); });
type GrafikKepuasan = Prisma.GrafikKepuasanGetPayload<{ const defaultForm = {
select: { nama: "",
label: true; tanggal: "",
jumlah: true; jenisKelamin: "",
}; alamat: "",
}>; penyakit: "",
const defaultForm: GrafikKepuasan = {
label: "",
jumlah: ""
}; };
const grafikkepuasan = proxy({ const grafikkepuasan = proxy({
@@ -36,16 +36,15 @@ const grafikkepuasan = proxy({
} }
try { try {
grafikkepuasan.create.loading = true; grafikkepuasan.create.loading = true;
const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(grafikkepuasan.create.form); const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(
grafikkepuasan.create.form
);
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Success create");
grafikkepuasan.create.form = { grafikkepuasan.create.form = { ...defaultForm };
label: "",
jumlah: "",
};
grafikkepuasan.findMany.load(); grafikkepuasan.findMany.load();
return id; return id;
} }
@@ -62,21 +61,49 @@ const grafikkepuasan = proxy({
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.GrafikKepuasanGetPayload<{ omit: { isActive: true } }>[] | Prisma.GrafikKepuasanGetPayload<{
omit: {
isActive: true;
};
}>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.kesehatan.grafikkepuasan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
grafikkepuasan.findMany.data = res.data?.data ?? []; grafikkepuasan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikkepuasan.findMany.page = page;
grafikkepuasan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.grafikkepuasan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
grafikkepuasan.findMany.data = res.data.data ?? [];
grafikkepuasan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikkepuasan.findMany.data = [];
grafikkepuasan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
grafikkepuasan.findMany.data = [];
grafikkepuasan.findMany.totalPages = 1;
} finally {
grafikkepuasan.findMany.loading = false;
} }
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.GrafikKepuasanGetPayload<{ data: null as Prisma.GrafikKepuasanGetPayload<{
omit: { isActive: true } omit: { isActive: true };
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`); const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`);
@@ -95,88 +122,137 @@ const grafikkepuasan = proxy({
}, },
update: { update: {
id: "", id: "",
form: {...defaultForm}, form: { ...defaultForm },
loading: false, loading: false,
async byId() { async load(id: string) {
}, if (!id) {
async submit() { toast.warn("ID tidak valid");
const id = this.id; return null;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateGrafikKepuasan.safeParse(grafikkepuasan.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
this.loading = true;
const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
} }
toast.success("Berhasil update data!"); try {
const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
// ✅ Optional: refresh list kalau kamu langsung ke halaman list if (!response.ok) {
await grafikkepuasan.findMany.load(); throw new Error(`HTTP error! status: ${response.status}`);
}
return result.data; const result = await response.json();
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data grafik kepuasan");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
grafikkepuasan.delete.loading = true;
const response = await fetch(`/api/kesehatan/grafikkepuasan/del/${id}`, { if (result?.success) {
method: "DELETE", const data = result.data;
headers: { this.id = data.id;
"Content-Type": "application/json", this.form = {
}, nama: data.nama,
}); tanggal: data.tanggal,
jenisKelamin: data.jenisKelamin,
const result = await response.json(); alamat: data.alamat,
penyakit: data.penyakit,
if (response.ok && result?.success) { };
toast.success( return data; // Return the loaded data
result.message || "Grafik kepuasan berhasil dihapus" } else {
); throw new Error(result?.message || "Gagal memuat data");
await grafikkepuasan.findMany.load(); // refresh list }
} else { } catch (error) {
console.error("Error loading grafik kepuasan:", error);
toast.error( toast.error(
result?.message || "Gagal menghapus grafik kepuasan" error instanceof Error ? error.message : "Gagal memuat data"
); );
return null;
} }
} catch (error) { },
console.error("Gagal delete:", error); async submit() {
toast.error("Terjadi kesalahan saat menghapus grafik kepuasan"); const id = this.id;
} finally { if (!id) {
grafikkepuasan.delete.loading = false; toast.warn("ID tidak valid");
} return null;
} }
}
const formData = {
nama: this.form.nama,
tanggal: this.form.tanggal,
jenisKelamin: this.form.jenisKelamin,
alamat: this.form.alamat,
penyakit: this.form.penyakit,
};
const cek = templateGrafikKepuasan.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await grafikkepuasan.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data grafik kepuasan");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
grafikkepuasan.delete.loading = true;
const response = await fetch(
`/api/kesehatan/grafikkepuasan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik kepuasan berhasil dihapus");
await grafikkepuasan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik kepuasan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik kepuasan");
} finally {
grafikkepuasan.delete.loading = false;
}
},
},
}); });
export default grafikkepuasan; export default grafikkepuasan;

View File

@@ -1,10 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templatePersentase = z.object({ //persentase kelahiran kematian
const templatePersentaseKelahiran = z.object({
tahun: z.string().min(4, "Tahun harus diisi"), tahun: z.string().min(4, "Tahun harus diisi"),
kematianKasar: z.string().min(1, "Kematian kasar harus diisi"), kematianKasar: z.string().min(1, "Kematian kasar harus diisi"),
kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"), kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"),
@@ -13,18 +16,14 @@ const templatePersentase = z.object({
type Persentase = Prisma.DataKematian_KelahiranGetPayload<{ type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
select: { select: {
tahun: true; kematianId: true;
kematianKasar: true; kelahiranId: true;
kelahiranKasar: true;
kematianBayi: true;
}; };
}>; }>;
const defaultForm: Persentase = { const defaultForm: Persentase = {
tahun: "", kematianId: "",
kematianKasar: "", kelahiranId: "",
kelahiranKasar: "",
kematianBayi: "",
}; };
const persentasekelahiran = proxy({ const persentasekelahiran = proxy({
@@ -32,7 +31,9 @@ const persentasekelahiran = proxy({
form: defaultForm, form: defaultForm,
loading: false, loading: false,
async create() { async create() {
const cek = templatePersentase.safeParse(persentasekelahiran.create.form); const cek = templatePersentaseKelahiran.safeParse(
persentasekelahiran.create.form
);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -47,7 +48,7 @@ const persentasekelahiran = proxy({
].post(persentasekelahiran.create.form); ].post(persentasekelahiran.create.form);
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Success create");
persentasekelahiran.create.form = { ...defaultForm }; persentasekelahiran.create.form = { ...defaultForm };
@@ -69,21 +70,51 @@ const persentasekelahiran = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.DataKematian_KelahiranGetPayload<{ | Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true }; include: {
kematian: true;
kelahiran: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.kesehatan.persentasekelahiran[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
persentasekelahiran.findMany.data = res.data?.data ?? []; persentasekelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
persentasekelahiran.findMany.page = page;
persentasekelahiran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
persentasekelahiran.findMany.data = res.data.data ?? [];
persentasekelahiran.findMany.totalPages = res.data.totalPages ?? 1;
} else {
persentasekelahiran.findMany.data = [];
persentasekelahiran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
persentasekelahiran.findMany.data = [];
persentasekelahiran.findMany.totalPages = 1;
} finally {
persentasekelahiran.findMany.loading = false;
} }
}, },
}, },
findUnique: { findUnique: {
data: null as Prisma.DataKematian_KelahiranGetPayload<{ data: null as Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true }; include: {
kematian: true;
kelahiran: true;
};
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
@@ -114,13 +145,11 @@ const persentasekelahiran = proxy({
} }
const formData = { const formData = {
tahun: this.form.tahun, kematianId: this.form.kematianId,
kematianKasar: this.form.kematianKasar, kelahiranId: this.form.kelahiranId,
kelahiranKasar: this.form.kelahiranKasar,
kematianBayi: this.form.kematianBayi,
}; };
const cek = templatePersentase.safeParse(formData); const cek = templatePersentaseKelahiran.safeParse(formData);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -197,4 +226,521 @@ const persentasekelahiran = proxy({
}, },
}); });
export default persentasekelahiran; // data kelahiran
const templateKelahiran = z.object({
nama: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(4, "Tahun harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
});
const defaultKelahiran = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
};
const kelahiran = proxy({
create: {
form: { ...defaultKelahiran }, // ✅ ini kunci fix-nya
loading: false,
async create() {
const cek = templateKelahiran.safeParse(kelahiran.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kelahiran.create.loading = true;
const res = await ApiFetch.api.kesehatan.kelahiran["create"].post(
kelahiran.create.form
);
if (res.status === 200) {
kelahiran.findMany.load();
return toast.success("Kelahiran berhasil disimpan!");
}
return toast.error("Gagal menyimpan kelahiran");
} catch (error) {
console.log((error as Error).message);
} finally {
kelahiran.create.loading = false;
}
},
resetForm() {
kelahiran.create.form = { ...defaultKelahiran };
},
},
findMany: {
data: null as
| Prisma.KelahiranGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
kelahiran.findMany.page = page;
kelahiran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.kelahiran["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kelahiran.findMany.data = res.data.data ?? [];
kelahiran.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kelahiran.findMany.data = [];
kelahiran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kelahiran paginated:", err);
kelahiran.findMany.data = [];
kelahiran.findMany.totalPages = 1;
} finally {
kelahiran.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KelahiranGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kelahiran/${id}`);
if (res.ok) {
const data = await res.json();
kelahiran.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch kelahiran:", res.statusText);
kelahiran.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching kelahiran:", error);
kelahiran.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kelahiran.delete.loading = true;
const response = await fetch(`/api/kesehatan/kelahiran/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kelahiran berhasil dihapus");
await kelahiran.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kelahiran");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kelahiran");
} finally {
kelahiran.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKelahiran },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kelahiran/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
tanggal: data.tanggal,
jenisKelamin: data.jenisKelamin,
alamat: data.alamat,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading data kelahiran:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKelahiran.safeParse(kelahiran.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kelahiran.edit.loading = true;
const response = await fetch(`/api/kesehatan/kelahiran/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
tanggal: this.form.tanggal,
jenisKelamin: this.form.jenisKelamin,
alamat: this.form.alamat,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data kelahiran");
await kelahiran.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data kelahiran");
}
} catch (error) {
console.error("Error updating data kelahiran:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kelahiran"
);
return false;
} finally {
kelahiran.edit.loading = false;
}
},
reset() {
kelahiran.edit.id = "";
kelahiran.edit.form = { ...defaultKelahiran };
},
},
});
// data kematian
const templateKematian = z.object({
nama: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(4, "Tahun harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
alamat: z.string().min(1, "Alamat harus diisi"),
penyebab: z.string().min(1, "Penyebab harus diisi"),
});
const defaultKematian = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
penyebab: "",
};
const kematian = proxy({
create: {
form: { ...defaultKematian }, // ✅ ini kunci fix-nya
loading: false,
async create() {
const cek = templateKematian.safeParse(kematian.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kematian.create.loading = true;
const res = await ApiFetch.api.kesehatan.kematian["create"].post(
kematian.create.form
);
if (res.status === 200) {
kematian.findMany.load();
return toast.success("Kematian berhasil disimpan!");
}
return toast.error("Gagal menyimpan kematian");
} catch (error) {
console.log((error as Error).message);
} finally {
kematian.create.loading = false;
}
},
resetForm() {
kematian.create.form = { ...defaultKematian };
},
},
findMany: {
data: null as
| Prisma.KematianGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kematian.findMany.loading = true; // ✅ Akses langsung via nama path
kematian.findMany.page = page;
kematian.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.kematian["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kematian.findMany.data = res.data.data ?? [];
kematian.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kematian.findMany.data = [];
kematian.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kematian paginated:", err);
kematian.findMany.data = [];
kematian.findMany.totalPages = 1;
} finally {
kematian.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KematianGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kematian/${id}`);
if (res.ok) {
const data = await res.json();
kematian.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch kematian:", res.statusText);
kematian.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching kematian:", error);
kematian.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kematian.delete.loading = true;
const response = await fetch(`/api/kesehatan/kematian/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kematian berhasil dihapus");
await kematian.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kematian");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kematian");
} finally {
kematian.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKematian },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kematian/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
tanggal: data.tanggal,
jenisKelamin: data.jenisKelamin,
alamat: data.alamat,
penyebab: data.penyebab,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading data kematian:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKematian.safeParse(kematian.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kematian.edit.loading = true;
const response = await fetch(`/api/kesehatan/kematian/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
tanggal: this.form.tanggal,
jenisKelamin: this.form.jenisKelamin,
alamat: this.form.alamat,
penyebab: this.form.penyebab,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data kematian");
await kematian.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data kematian");
}
} catch (error) {
console.error("Error updating data kematian:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kematian"
);
return false;
} finally {
kematian.edit.loading = false;
}
},
reset() {
kematian.edit.id = "";
kematian.edit.form = { ...defaultKematian };
},
},
});
const persentaseKelahiranKematian = proxy({
persentasekelahiran,
kelahiran,
kematian
});
export default persentaseKelahiranKematian;

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";
@@ -20,17 +21,41 @@ const defaultForm = {
const infoWabahPenyakit = proxy({ const infoWabahPenyakit = proxy({
findMany: { findMany: {
data: [] as Prisma.InfoWabahPenyakitGetPayload<{ data: null as
include: { | Prisma.InfoWabahPenyakitGetPayload<{
image: true; include: {
}; image: true;
}>[], };
async load() { }>[]
const res = await ApiFetch.api.kesehatan.infowabahpenyakit[ | null,
"find-many" page: 1,
].get(); totalPages: 1,
if (res.status === 200) { loading: false,
infoWabahPenyakit.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
infoWabahPenyakit.findMany.loading = true; // ✅ Akses langsung via nama path
infoWabahPenyakit.findMany.page = page;
infoWabahPenyakit.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.infowabahpenyakit["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
infoWabahPenyakit.findMany.data = res.data.data ?? [];
infoWabahPenyakit.findMany.totalPages = res.data.totalPages ?? 1;
} else {
infoWabahPenyakit.findMany.data = [];
infoWabahPenyakit.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch info wabah penyakit paginated:", err);
infoWabahPenyakit.findMany.data = [];
infoWabahPenyakit.findMany.totalPages = 1;
} finally {
infoWabahPenyakit.findMany.loading = false;
} }
}, },
}, },

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";
@@ -5,204 +6,241 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"), name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(), imageId: z.string().nonempty(),
})
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
}
const kontakDarurat = proxy({
findMany: {
data: [] as Prisma.KontakDaruratGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.kontakdarurat[
"find-many"
].get();
if (res.status === 200) {
kontakDarurat.findMany.data = res.data?.data ?? [];
}
},
},
create:{
form: {...defaultForm},
loading: false,
async create() {
const cek = templateForm.safeParse(kontakDarurat.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDarurat.create.loading = true;
const res = await ApiFetch.api.kesehatan.kontakdarurat[
"create"
].post(kontakDarurat.create.form);
if (res.status === 200) {
kontakDarurat.findMany.load();
return toast.success("Kontak Darurat berhasil disimpan!");
}
return toast.error("Gagal menyimpan kontak darurat");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDarurat.create.loading = false;
}
},
resetForm() {
kontakDarurat.create.form = {...defaultForm};
}
},
findUnique: {
data: null as Prisma.KontakDaruratGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kontakdarurat/${id}`);
if (res.ok) {
const data = await res.json();
kontakDarurat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDarurat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDarurat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
try {
kontakDarurat.delete.loading = true;
const response = await fetch(`/api/kesehatan/kontakdarurat/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kontak darurat berhasil dihapus");
await kontakDarurat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kontak darurat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kontak darurat");
} finally {
kontakDarurat.delete.loading = false;
}
}
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kontakdarurat/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching kontak darurat:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(kontakDarurat.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDarurat.edit.loading = true;
const response = await fetch(`/api/kesehatan/kontakdarurat/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Kontak darurat berhasil diupdate");
await kontakDarurat.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update kontak darurat");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate kontak darurat");
return false;
} finally {
kontakDarurat.edit.loading = false;
}
},
reset() {
kontakDarurat.edit.id = "";
kontakDarurat.edit.form = { ...defaultForm };
},
},
}); });
export default kontakDarurat const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
};
const kontakDarurat = proxy({
findMany: {
data: null as
| Prisma.KontakDaruratGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kontakDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
kontakDarurat.findMany.page = page;
kontakDarurat.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.kontakdarurat[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
kontakDarurat.findMany.data = res.data.data ?? [];
kontakDarurat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kontakDarurat.findMany.data = [];
kontakDarurat.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kontak darurat paginated:", err);
kontakDarurat.findMany.data = [];
kontakDarurat.findMany.totalPages = 1;
} finally {
kontakDarurat.findMany.loading = false;
}
},
},
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(kontakDarurat.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDarurat.create.loading = true;
const res = await ApiFetch.api.kesehatan.kontakdarurat["create"].post(
kontakDarurat.create.form
);
if (res.status === 200) {
kontakDarurat.findMany.load();
return toast.success("Kontak Darurat berhasil disimpan!");
}
return toast.error("Gagal menyimpan kontak darurat");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDarurat.create.loading = false;
}
},
resetForm() {
kontakDarurat.create.form = { ...defaultForm };
},
},
findUnique: {
data: null as Prisma.KontakDaruratGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kontakdarurat/${id}`);
if (res.ok) {
const data = await res.json();
kontakDarurat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDarurat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDarurat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
try {
kontakDarurat.delete.loading = true;
const response = await fetch(`/api/kesehatan/kontakdarurat/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kontak darurat berhasil dihapus");
await kontakDarurat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kontak darurat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kontak darurat");
} finally {
kontakDarurat.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kontakdarurat/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching kontak darurat:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(kontakDarurat.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDarurat.edit.loading = true;
const response = await fetch(
`/api/kesehatan/kontakdarurat/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Kontak darurat berhasil diupdate");
await kontakDarurat.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update kontak darurat");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat mengupdate kontak darurat"
);
return false;
} finally {
kontakDarurat.edit.loading = false;
}
},
reset() {
kontakDarurat.edit.id = "";
kontakDarurat.edit.form = { ...defaultForm };
},
},
});
export default kontakDarurat;

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";
@@ -17,21 +18,45 @@ const defaultForm = {
} }
const penangananDarurat = proxy({ const penangananDarurat = proxy({
findMany: { findMany: {
data: [] as Prisma.PenangananDaruratGetPayload<{ data: null as
include: { | Prisma.PenangananDaruratGetPayload<{
image: true; include: {
}; image: true;
}>[], };
async load() { }>[]
const res = await ApiFetch.api.kesehatan.penanganandarurat[ | null,
"find-many" page: 1,
].get(); totalPages: 1,
if (res.status === 200) { loading: false,
penangananDarurat.findMany.data = res.data?.data ?? []; search: "",
} load: async (page = 1, limit = 10, search = "") => {
}, penangananDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
penangananDarurat.findMany.page = page;
penangananDarurat.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.penanganandarurat["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
penangananDarurat.findMany.data = res.data.data ?? [];
penangananDarurat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
penangananDarurat.findMany.data = [];
penangananDarurat.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
penangananDarurat.findMany.data = [];
penangananDarurat.findMany.totalPages = 1;
} finally {
penangananDarurat.findMany.loading = false;
}
}, },
},
create:{ create:{
form: {...defaultForm}, form: {...defaultForm},
loading: false, loading: false,

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";
@@ -9,6 +10,7 @@ const templateForm = z.object({
nomor: z.string().min(1, { message: "Nomor is required" }), nomor: z.string().min(1, { message: "Nomor is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }), deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
imageId: z.string().nonempty(), imageId: z.string().nonempty(),
jadwalPelayanan: z.string().min(1, { message: "Jadwal Pelayanan is required" }),
}); });
const defaultForm = { const defaultForm = {
@@ -16,6 +18,7 @@ const defaultForm = {
nomor: "", nomor: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
jadwalPelayanan: "",
}; };
const posyandustate = proxy({ const posyandustate = proxy({
@@ -50,19 +53,43 @@ const posyandustate = proxy({
} }
}, },
findMany: { findMany: {
data: null as data: null as
| Prisma.PosyanduGetPayload<{ | Prisma.PosyanduGetPayload<{
include: { include: {
image: true; image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posyandustate.findMany.loading = true; // ✅ Akses langsung via nama path
posyandustate.findMany.page = page;
posyandustate.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
posyandustate.findMany.data = res.data.data ?? [];
posyandustate.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posyandustate.findMany.data = [];
posyandustate.findMany.totalPages = 1;
} }
}>[] } catch (err) {
| null, console.error("Gagal fetch posyandu paginated:", err);
async load() { posyandustate.findMany.data = [];
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get(); posyandustate.findMany.totalPages = 1;
if (res.status === 200) { } finally {
posyandustate.findMany.data = res.data?.data ?? []; posyandustate.findMany.loading = false;
} }
} },
}, },
findUnique: { findUnique: {
data: null as data: null as
@@ -148,6 +175,7 @@ const posyandustate = proxy({
nomor: data.nomor, nomor: data.nomor,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
imageId: data.imageId || "", imageId: data.imageId || "",
jadwalPelayanan: data.jadwalPelayanan || "",
}; };
return data; return data;
} else { } else {
@@ -181,6 +209,7 @@ const posyandustate = proxy({
nomor: this.form.nomor, nomor: this.form.nomor,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
imageId: this.form.imageId, imageId: this.form.imageId,
jadwalPelayanan: this.form.jadwalPelayanan,
}), }),
}); });

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";
@@ -20,17 +21,43 @@ const defaultForm = {
const programKesehatan = proxy({ const programKesehatan = proxy({
findMany: { findMany: {
data: [] as Prisma.ProgramKesehatanGetPayload<{ data: null as
include: { | Prisma.ProgramKesehatanGetPayload<{
image: true; include: {
}; image: true;
}>[], };
async load() { }>[]
const res = await ApiFetch.api.kesehatan.programkesehatan[ | null,
"find-many" page: 1,
].get(); totalPages: 1,
if (res.status === 200) { loading: false,
programKesehatan.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
programKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
programKesehatan.findMany.page = page;
programKesehatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.programkesehatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKesehatan.findMany.data = res.data.data ?? [];
programKesehatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
programKesehatan.findMany.data = [];
programKesehatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
programKesehatan.findMany.data = [];
programKesehatan.findMany.totalPages = 1;
} finally {
programKesehatan.findMany.loading = false;
} }
}, },
}, },
@@ -97,12 +124,15 @@ const programKesehatan = proxy({
try { try {
programKesehatan.delete.loading = true; programKesehatan.delete.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/del/${id}`, { const response = await fetch(
method: "DELETE", `/api/kesehatan/programkesehatan/del/${id}`,
headers: { {
"Content-Type": "application/json", method: "DELETE",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok && result?.success) { if (response.ok && result?.success) {
@@ -156,57 +186,70 @@ const programKesehatan = proxy({
} }
} catch (error) { } catch (error) {
console.error("Error fetching program kesehatan:", error); console.error("Error fetching program kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data"); toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null; return null;
} }
}, },
async update() { async update() {
const cek = templateForm.safeParse(programKesehatan.edit.form); const cek = templateForm.safeParse(programKesehatan.edit.form);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
.join("\n")}] required`; .join("\n")}] required`;
return toast.error(err); return toast.error(err);
} }
try { try {
programKesehatan.edit.loading = true; programKesehatan.edit.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/${this.id}`, { const response = await fetch(
method: "PUT", `/api/kesehatan/programkesehatan/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
deskripsiSingkat: this.form.deskripsiSingkat, body: JSON.stringify({
deskripsi: this.form.deskripsi, name: this.form.name,
imageId: this.form.imageId, deskripsiSingkat: this.form.deskripsiSingkat,
}), deskripsi: this.form.deskripsi,
}); imageId: this.form.imageId,
if (!response.ok) { }),
const errorData = await response.json().catch(() => ({})); }
throw new Error(errorData.message || `HTTP error! status: ${response.status}`); );
} if (!response.ok) {
const result = await response.json(); const errorData = await response.json().catch(() => ({}));
if (result.success) { throw new Error(
toast.success(result.message || "Program kesehatan berhasil diupdate"); errorData.message || `HTTP error! status: ${response.status}`
await programKesehatan.findMany.load(); );
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate program kesehatan");
return false;
} finally {
programKesehatan.edit.loading = false;
} }
const result = await response.json();
if (result.success) {
toast.success(
result.message || "Program kesehatan berhasil diupdate"
);
await programKesehatan.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat mengupdate program kesehatan"
);
return false;
} finally {
programKesehatan.edit.loading = false;
}
}, },
reset() { reset() {
programKesehatan.edit.id = ""; programKesehatan.edit.id = "";
programKesehatan.edit.form = { ...defaultForm }; programKesehatan.edit.form = { ...defaultForm };
}, },
}, },
}); });

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";
@@ -163,13 +164,43 @@ const puskesmasState = proxy({
}, },
findMany: { findMany: {
data: null as Prisma.PuskesmasGetPayload<{ data: null as
include: { image: true; jam: true; kontak: true }; | Prisma.PuskesmasGetPayload<{
}>[] | null, include: {
async load() { image: true;
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get(); jam: true;
if (res.status === 200) { kontak: true;
puskesmasState.findMany.data = res.data?.data ?? []; };
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
puskesmasState.findMany.loading = true; // ✅ Akses langsung via nama path
puskesmasState.findMany.page = page;
puskesmasState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
puskesmasState.findMany.data = res.data.data ?? [];
puskesmasState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
puskesmasState.findMany.data = [];
puskesmasState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
puskesmasState.findMany.data = [];
puskesmasState.findMany.totalPages = 1;
} finally {
puskesmasState.findMany.loading = false;
} }
}, },
}, },

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

@@ -181,7 +181,13 @@ const responden = proxy({
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(this.form), body: JSON.stringify({
name: this.form.name,
tanggal: this.form.tanggal,
jenisKelaminId: this.form.jenisKelaminId,
ratingId: this.form.ratingId,
kelompokUmurId: this.form.kelompokUmurId,
}),
}); });
const result = await response.json(); const result = await response.json();
if (!response.ok || !result?.success) { if (!response.ok || !result?.success) {

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;
} }
}, },
}, },
@@ -299,18 +333,64 @@ const lembagaPendidikan = proxy({
Prisma.LembagaGetPayload<{ Prisma.LembagaGetPayload<{
include: { include: {
jenjangPendidikan: true; jenjangPendidikan: true;
siswa: true;
pengajar: true;
}; };
}> }> & {
siswa?: [];
pengajar?: [];
}
> | null, > | null,
async load() { page: 1,
const res = totalPages: 1,
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[ total: 0,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
lembagaPendidikan.findMany.data = res.data?.data ?? []; lembagaPendidikan.findMany.loading = true;
lembagaPendidikan.findMany.page = page;
lembagaPendidikan.findMany.search = search;
try {
const query: any = {
page,
limit,
...(search && { search }),
...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
};
console.log('Fetching lembaga with query:', query);
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
console.log('API Response:', res);
if (res.status === 200 && res.data?.success) {
const data = Array.isArray(res.data.data) ? res.data.data : [];
const total = typeof res.data.total === 'number' ? res.data.total : 0;
const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
lembagaPendidikan.findMany.data = data;
lembagaPendidikan.findMany.total = total;
lembagaPendidikan.findMany.totalPages = totalPages;
console.log('Successfully loaded lembaga data:', {
count: data.length,
total,
totalPages
});
} else {
console.error(
"Failed to load lembaga pendidikan:",
res.data?.message || 'No error message provided'
);
throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
}
} catch (error) {
console.error("Error loading lembaga pendidikan:", error);
lembagaPendidikan.findMany.data = [];
lembagaPendidikan.findMany.total = 0;
lembagaPendidikan.findMany.totalPages = 1;
} finally {
lembagaPendidikan.findMany.loading = false;
} }
}, },
}, },
@@ -554,16 +634,55 @@ const siswa = proxy({
data: null as Array< data: null as Array<
Prisma.SiswaGetPayload<{ Prisma.SiswaGetPayload<{
include: { include: {
lembaga: true; lembaga: {
include: {
jenjangPendidikan: true;
};
};
}; };
}> }>
> | null, > | null,
async load() { page: 1,
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[ totalPages: 1,
"find-many" total: 0,
].get(); loading: false,
if (res.status === 200) { search: "",
siswa.findMany.data = res.data?.data ?? []; jenjangPendidikan: "",
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
siswa.findMany.loading = true;
siswa.findMany.page = page;
siswa.findMany.search = search;
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
siswa.findMany.data = res.data.data || [];
siswa.findMany.total = res.data.total || 0;
siswa.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load siswa:",
res.data?.message
);
siswa.findMany.data = [];
siswa.findMany.total = 0;
siswa.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading siswa:", error);
siswa.findMany.data = [];
siswa.findMany.total = 0;
siswa.findMany.totalPages = 1;
} finally {
siswa.findMany.loading = false;
} }
}, },
}, },
@@ -794,16 +913,56 @@ const pengajar = proxy({
data: null as Array< data: null as Array<
Prisma.PengajarGetPayload<{ Prisma.PengajarGetPayload<{
include: { include: {
lembaga: true; lembaga: {
include: {
jenjangPendidikan: true
}
}
}; };
}> }>
> | null, > | null,
async load() { page: 1,
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[ totalPages: 1,
"find-many" total: 0,
].get(); loading: false,
if (res.status === 200) { search: "",
pengajar.findMany.data = res.data?.data ?? []; jenjangPendidikan: "",
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
// Change to arrow function
pengajar.findMany.loading = true; // Use the full path to access the property
pengajar.findMany.page = page;
pengajar.findMany.search = search;
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
pengajar.findMany.data = res.data.data || [];
pengajar.findMany.total = res.data.total || 0;
pengajar.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pengajar:",
res.data?.message
);
pengajar.findMany.data = [];
pengajar.findMany.total = 0;
pengajar.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pengajar:", error);
pengajar.findMany.data = [];
pengajar.findMany.total = 0;
pengajar.findMany.totalPages = 1;
} finally {
pengajar.findMany.loading = false;
} }
}, },
}, },
@@ -815,7 +974,9 @@ const pengajar = proxy({
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`); const res = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/${id}`
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
pengajar.findUnique.data = data.data ?? null; pengajar.findUnique.data = data.data ?? null;
@@ -948,7 +1109,8 @@ const pengajar = proxy({
result result
); );
throw new Error( throw new Error(
result?.message || `Gagal mengupdate pengajar (${response.status})` result?.message ||
`Gagal mengupdate pengajar (${response.status})`
); );
} }

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";
@@ -54,23 +55,46 @@ const dataPerpustakaan = proxy({
}, },
}, },
findMany: { findMany: {
data: [] as Prisma.DataPerpustakaanGetPayload<{ data: null as
include: { | Prisma.DataPerpustakaanGetPayload<{
kategori: true; include: {
image: true; image: true;
}; kategori: true;
}>[], };
loading: false, }>[]
async load() { | null,
const res = page: 1,
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[ totalPages: 1,
"findMany" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "", kategori = "") => {
dataPerpustakaan.findMany.data = res.data?.data ?? []; dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
} dataPerpustakaan.findMany.page = page;
dataPerpustakaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
dataPerpustakaan.findMany.data = res.data.data ?? [];
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data perpustakaan paginated:", err);
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
} finally {
dataPerpustakaan.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{ data: null as Prisma.DataPerpustakaanGetPayload<{
include: { include: {
@@ -293,14 +317,34 @@ const kategoriBuku = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = load: async (page = 1, limit = 10, search = "") => {
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[ kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
"findMany" kategoriBuku.findMany.page = page;
].get(); kategoriBuku.findMany.search = search;
if (res.status === 200) {
kategoriBuku.findMany.data = res.data?.data ?? []; try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriBuku.findMany.data = [];
kategoriBuku.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data kategori buku paginated:", err);
kategoriBuku.findMany.data = [];
kategoriBuku.findMany.totalPages = 1;
} finally {
kategoriBuku.findMany.loading = false;
} }
}, },
}, },

View File

@@ -49,35 +49,38 @@ const daftarInformasiPublik = proxy({
}, },
}, },
findMany: { findMany: {
data: null as any[] | null, data: null as
| Prisma.DaftarInformasiPublikGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
daftarInformasiPublik.findMany.loading = true; // Use the full path to access the property load: async (page = 1, limit = 10, search = "") => {
daftarInformasiPublik.findMany.loading = true; // ✅ Akses langsung via nama path
daftarInformasiPublik.findMany.page = page; daftarInformasiPublik.findMany.page = page;
try { daftarInformasiPublik.findMany.search = search;
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"find-many"
].get({
query: { page, limit },
});
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
daftarInformasiPublik.findMany.data = res.data.data || []; daftarInformasiPublik.findMany.data = res.data.data ?? [];
daftarInformasiPublik.findMany.total = res.data.total || 0; daftarInformasiPublik.findMany.totalPages = res.data.totalPages ?? 1;
daftarInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
} else { } else {
console.error("Failed to load daftar informasi publik:", res.data?.message);
daftarInformasiPublik.findMany.data = []; daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.total = 0;
daftarInformasiPublik.findMany.totalPages = 1; daftarInformasiPublik.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Error loading daftar informasi publik:", error); console.error("Gagal fetch daftar informasi publik paginated:", err);
daftarInformasiPublik.findMany.data = []; daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.total = 0;
daftarInformasiPublik.findMany.totalPages = 1; daftarInformasiPublik.findMany.totalPages = 1;
} finally { } finally {
daftarInformasiPublik.findMany.loading = false; daftarInformasiPublik.findMany.loading = false;

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

@@ -1,124 +1,43 @@
import { proxy } from 'valtio' /* eslint-disable @typescript-eslint/no-explicit-any */
import { toast } from 'react-toastify' import { proxy } from "valtio";
import ApiFetch from '@/lib/api-fetch' import { toast } from "react-toastify";
import { Prisma } from '@prisma/client' import ApiFetch from "@/lib/api-fetch";
import { z } from 'zod' import { Prisma } from "@prisma/client";
import { z } from "zod";
// 1. Validasi Zod // State Valtio
const userSchema = z.object({
nama: z.string().min(1, 'Nama harus diisi'),
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Password minimal 6 karakter'),
roleId: z.string().optional(),
})
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
// 2. State Valtio
const userState = proxy({ const userState = proxy({
// Register
register: {
form: { ...defaultForm },
loading: false,
async submit() {
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(', ')
return toast.error(err)
}
try {
userState.register.loading = true
const res = await ApiFetch.api.user.register.post(userState.register.form)
if (res.status === 200) {
toast.success('Registrasi berhasil, silakan login')
userState.register.form = { ...defaultForm } // reset
} else {
toast.error(res.data?.message || 'Gagal registrasi')
}
} catch (e) {
console.error(e)
toast.error('Terjadi kesalahan saat registrasi')
} finally {
userState.register.loading = false
}
},
},
// Login
login: {
form: { email: '', password: '' },
loading: false,
async submit() {
try {
userState.login.loading = true
const res = await ApiFetch.api.user.login.post(userState.login.form)
if (res.status === 200) {
toast.success('Login berhasil')
const token = res.data?.data?.token
if (typeof token === 'string') {
localStorage.setItem('token', token)
// Optional: simpan user role untuk otorisasi
const user = res.data?.data?.user
localStorage.setItem('user', JSON.stringify(user))
}
} else {
toast.error(res.data?.message || 'Login gagal')
}
} catch (e) {
console.error(e)
toast.error('Terjadi kesalahan saat login')
} finally {
userState.login.loading = false
}
},
},
// CRUD User (untuk admin)
create: {
form: { ...defaultForm },
loading: false,
async create(isAdmin = false) {
const valid = userSchema.safeParse(userState.create.form)
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(', ')
return toast.error(err)
}
try {
userState.create.loading = true
const endpoint = isAdmin ? 'create' : 'register'
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
if (res.status === 200) {
toast.success('User berhasil dibuat')
userState.findMany.load() // refresh list
userState.create.form = { ...defaultForm } // reset form
} else {
toast.error(res.data?.message || 'Gagal membuat user')
}
} catch (e) {
console.error(e)
toast.error('Gagal membuat user')
} finally {
userState.create.loading = false
}
},
},
// Find Many // Find Many
findMany: { findMany: {
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[], data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
this.loading = true load: async (page = 1, limit = 10, search = "") => {
userState.findMany.loading = true; // ✅ Akses langsung via nama path
userState.findMany.page = page;
userState.findMany.search = search;
try { try {
const res = await ApiFetch.api.user.findMany.get() const query: any = { page, limit };
if (res.status === 200) { if (search) query.search = search;
this.data = res.data?.data || []
const res = await ApiFetch.api.user["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
userState.findMany.data = res.data.data ?? [];
userState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
userState.findMany.data = [];
userState.findMany.totalPages = 1;
} }
} catch (e) { } catch (err) {
console.error(e) console.error("Gagal fetch user paginated:", err);
toast.error('Gagal muat data user') userState.findMany.data = [];
userState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false userState.findMany.loading = false;
} }
}, },
}, },
@@ -128,71 +47,20 @@ const userState = proxy({
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null, data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
loading: false, loading: false,
async load(id: string) { async load(id: string) {
this.loading = true this.loading = true;
try { try {
const res = await fetch(`/api/user/findUnique/${id}`) const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json() const data = await res.json();
if (res.status === 200) { if (res.status === 200) {
this.data = data.data this.data = data.data;
} else { } else {
toast.error(data.message) toast.error(data.message);
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e);
toast.error('Gagal ambil data user') toast.error("Gagal ambil data user");
} finally { } finally {
this.loading = false this.loading = false;
}
},
},
// Update
update: {
id: '',
form: { ...defaultForm },
loading: false,
async load(id: string) {
this.loading = true
try {
const res = await fetch(`/api/user/findUnique/${id}`)
const data = await res.json()
if (res.status === 200) {
const user = data.data
this.id = user.id
this.form = {
nama: user.nama,
email: user.email,
password: '', // jangan kirim password lama
roleId: user.roleId,
}
}
} catch (e) {
console.error(e)
toast.error('Gagal muat data')
} finally {
this.loading = false
}
},
async submit() {
this.loading = true
try {
const res = await fetch(`/api/user/update/${this.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form),
})
const data = await res.json()
if (res.status === 200) {
toast.success('Update berhasil')
userState.findMany.load()
} else {
toast.error(data.message || 'Gagal update')
}
} catch (e) {
console.error(e)
toast.error('Gagal update user')
} finally {
this.loading = false
} }
}, },
}, },
@@ -201,35 +69,37 @@ const userState = proxy({
delete: { delete: {
loading: false, loading: false,
async submit(id: string) { async submit(id: string) {
this.loading = true this.loading = true;
try { try {
const res = await fetch(`/api/user/del/${id}`, { const res = await fetch(`/api/user/del/${id}`, {
method: 'PUT', method: "PUT",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
}) });
const data = await res.json() const data = await res.json();
if (res.status === 200) { if (res.status === 200) {
toast.success('User dinonaktifkan') toast.success("User dinonaktifkan");
userState.findMany.load() userState.findMany.load();
} else { } else {
toast.error(data.message || 'Gagal hapus') toast.error(data.message || "Gagal hapus");
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e);
toast.error('Gagal hapus user') toast.error("Gagal hapus user");
} finally { } finally {
this.loading = false this.loading = false;
} }
}, },
}, },
}) });
const templateRole = z.object({ const templateRole = z.object({
name: z.string().min(1, "Nama harus diisi"), name: z.string().min(1, "Nama harus diisi"),
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
}); });
const defaultRole = { const defaultRole = {
name: "", name: "",
permissions: [] as string[],
}; };
const roleState = proxy({ const roleState = proxy({
@@ -247,10 +117,9 @@ const roleState = proxy({
try { try {
roleState.create.loading = true; roleState.create.loading = true;
const res = const res = await ApiFetch.api.role["create"].post(
await ApiFetch.api.role[ roleState.create.form
"create" );
].post(roleState.create.form);
if (res.status === 200) { if (res.status === 200) {
roleState.findMany.load(); roleState.findMany.load();
return toast.success("Data role Berhasil Dibuat"); return toast.success("Data role Berhasil Dibuat");
@@ -273,10 +142,7 @@ const roleState = proxy({
}>[], }>[],
loading: false, loading: false,
async load() { async load() {
const res = const res = await ApiFetch.api.role["findMany"].get();
await ApiFetch.api.role[
"findMany"
].get();
if (res.status === 200) { if (res.status === 200) {
roleState.findMany.data = res.data?.data ?? []; roleState.findMany.data = res.data?.data ?? [];
} }
@@ -291,9 +157,7 @@ const roleState = proxy({
loading: false, loading: false,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch( const res = await fetch(`/api/role/${id}`);
`/api/role/${id}`
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
roleState.findUnique.data = data.data ?? null; roleState.findUnique.data = data.data ?? null;
@@ -315,22 +179,17 @@ const roleState = proxy({
try { try {
roleState.delete.loading = true; roleState.delete.loading = true;
const response = await fetch( const response = await fetch(`/api/role/del/${id}`, {
`/api/role/del/${id}`, method: "DELETE",
{ headers: {
method: "DELETE", "Content-Type": "application/json",
headers: { },
"Content-Type": "application/json", });
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok && result?.success) { if (response.ok && result?.success) {
toast.success( toast.success(result.message || "Data role berhasil dihapus");
result.message || "Data role berhasil dihapus"
);
await roleState.findMany.load(); // refresh list await roleState.findMany.load(); // refresh list
} else { } else {
toast.error(result?.message || "Gagal menghapus Data role"); toast.error(result?.message || "Gagal menghapus Data role");
@@ -354,15 +213,12 @@ const roleState = proxy({
} }
try { try {
const response = await fetch( const response = await fetch(`/api/role/${id}`, {
`/api/role/${id}`, method: "GET",
{ headers: {
method: "GET", "Content-Type": "application/json",
headers: { },
"Content-Type": "application/json", });
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -374,6 +230,7 @@ const roleState = proxy({
this.id = data.id; this.id = data.id;
this.form = { this.form = {
name: data.name, name: data.name,
permissions: data.permissions,
}; };
return data; // Return the loaded data return data; // Return the loaded data
} else { } else {
@@ -400,18 +257,16 @@ const roleState = proxy({
try { try {
roleState.update.loading = true; roleState.update.loading = true;
const response = await fetch( const response = await fetch(`/api/role/${this.id}`, {
`/api/role/${this.id}`, method: "PUT",
{ headers: {
method: "PUT", "Content-Type": "application/json",
headers: { },
"Content-Type": "application/json", body: JSON.stringify({
}, name: this.form.name,
body: JSON.stringify({ permissions: this.form.permissions,
name: this.form.name, }),
}), });
}
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
@@ -451,6 +306,6 @@ const roleState = proxy({
const user = proxy({ const user = proxy({
userState, userState,
roleState, roleState,
}) });
export default user export default user;

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,26 +13,35 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
{ {
label: "Pelayanan Surat Keterangan", label: "Pelayanan Surat Keterangan",
value: "pelayanansuratketerangan", value: "pelayanansuratketerangan",
href: "/admin/desa/layanan/pelayanan_surat_keterangan" href: "/admin/desa/layanan/pelayanan_surat_keterangan",
icon: <IconFileText size={18} stroke={1.8} />,
tooltip: "Layanan terkait surat keterangan resmi desa"
}, },
{ {
label: "Pelayanan Perizinan Berusaha", label: "Pelayanan Perizinan Berusaha",
value: "pelayananperizinanusaha", value: "pelayananperizinanusaha",
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha" href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
icon: <IconBuildingStore size={18} stroke={1.8} />,
tooltip: "Layanan untuk izin usaha masyarakat"
}, },
{ {
label: "Pelayanan Telunjuk Sakti Desa", label: "Pelayanan Telunjuk Sakti Desa",
value: "pelayanantelunjuksaktidesa", value: "pelayanantelunjuksaktidesa",
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa" href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
icon: <IconSparkles size={18} stroke={1.8} />,
tooltip: "Layanan inovasi khusus desa"
}, },
{ {
label: "Pelayanan Penduduk Non-Permanent", label: "Pelayanan Penduduk Non-Permanent",
value: "pelayanantelunjuknonpermanent", value: "pelayanannonpermanent",
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent" href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent"
} }
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -49,24 +59,65 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Layanan</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabsLayanan; export default LayoutTabsLayanan;

View File

@@ -1,63 +1,110 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconNews, IconCategory } from '@tabler/icons-react';
function LayoutTabsBerita({ children }: { children: React.ReactNode }) { function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [ const tabs = [
{ {
label: "List Berita", label: "List Berita",
value: "list_berita", value: "list_berita",
href: "/admin/desa/berita/list-berita" href: "/admin/desa/berita/list-berita",
icon: <IconNews size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola semua berita desa"
}, },
{ {
label: "Kategori Berita", label: "Kategori Berita",
value: "kategori_berita", value: "kategori_berita",
href: "/admin/desa/berita/kategori-berita" href: "/admin/desa/berita/kategori-berita",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori berita desa"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value);
if (tab) { if (tab) {
router.push(tab.href) router.push(tab.href);
} }
setActiveTab(value) setActiveTab(value);
} };
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Gallery</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant="pills"
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabsBerita; export default LayoutTabsBerita;

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,67 +19,102 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriBerita() { function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita) const editState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editState.update.form.name || '', name: editState.update.form.name || '',
}); });
useEffect(() => { useEffect(() => {
const loadKategori = async () => { const loadKategori = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori Berita:", error);
toast.error("Gagal memuat data kategori Berita");
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try { try {
editState.update.form = { const data = await editState.update.load(id);
...editState.update.form, if (data) {
name: formData.name, setFormData({
}; name: data.name || '',
await editState.update.update(); });
toast.success('Kategori Berita berhasil diperbarui!'); }
router.push('/admin/desa/berita/kategori-berita');
} catch (error) { } catch (error) {
console.error('Error updating kategori Berita:', error); console.error('Error loading kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita'); toast.error('Gagal memuat data kategori Berita');
} }
}; };
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
name: formData.name,
};
await editState.update.update();
toast.success('Kategori Berita berhasil diperbarui!');
router.push('/admin/desa/berita/kategori-berita');
} catch (error) {
console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
}
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button + Title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> onClick={() => router.back()}
<Stack gap={"xs"}> p="xs"
<Title order={3}>Edit Kategori Berita</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Berita
</Title>
</Group>
{/* Form Wrapper */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>} required
placeholder="masukkan nama kategori Berita"
/> />
<Button onClick={handleSubmit}>Simpan</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,50 +1,87 @@
'use client' 'use client';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() { function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita) const createState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
name: "", name: '',
}; };
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/berita/kategori-berita") router.push('/admin/desa/berita/kategori-berita');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Berita
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form utama */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Berita</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
placeholder='Masukkan nama kategori Berita' placeholder="Masukkan nama kategori berita"
value={createState.create.form.name} value={createState.create.form.name || ''}
onChange={(val) => { onChange={(e) => (createState.create.form.name = e.target.value)}
createState.create.form.name = val.target.value; required
}}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,25 +1,40 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDashboardBerita from '../../../_state/desa/berita'; import stateDashboardBerita from '../../../_state/desa/berita';
function KategoriBerita() { function KategoriBerita() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Berita' title="Kategori Berita"
placeholder='pencarian' placeholder="Cari nama kategori berita..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,99 +45,155 @@ function KategoriBerita() {
} }
function ListKategoriBerita({ search }: { search: string }) { function ListKategoriBerita({ search }: { search: string }) {
const listDataState = useProxy(stateDashboardBerita.kategoriBerita) const listDataState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
loading,
load,
page,
totalPages,
} = listDataState.findMany;
useEffect(() => { useEffect(() => {
listDataState.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
load(page, 10, search);
listDataState.findMany.load()
} }
} };
const filteredData = (listDataState.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Kategori Berita</Title>
title='List Kategori Berita' <Tooltip label="Tambah Kategori Berita" withArrow>
href='/admin/desa/berita/kategori-berita/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: 'auto' }}> color="blue"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() =>
<TableTr> router.push('/admin/desa/berita/kategori-berita/create')
<TableTh>No</TableTh> }
<TableTh>Nama</TableTh> >
<TableTh>Edit</TableTh> Tambah Baru
<TableTh>Hapus</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Group>
<TableTbody>
{filteredData.map((item, index) => ( <Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh style={{ width: '50%' }}>Nama</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fz="sm">{index + 1}</Text>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/pendidikan/perpustakaan-digital/kategori-Berita/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Text fw={500} truncate="end" lineClamp={1}>
color='red' {item.name}
disabled={listDataState.delete.loading} </Text>
onClick={() => { </TableTd>
setSelectedId(item.id) <TableTd>
setModalHapus(true) <Tooltip label="Edit Kategori Berita" withArrow>
}}> <Button
<IconTrash size={20} /> variant="light"
</Button> color="green"
onClick={() =>
router.push(
`/admin/desa/berita/kategori-berita/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus Kategori Berita" withArrow>
<Button
variant="light"
color="red"
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">
Tidak ada data kategori berita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus kategori Berita ini?' text="Apakah anda yakin ingin menghapus kategori berita ini?"
/> />
</Box> </Box>
) );
} }
export default KategoriBerita; export default KategoriBerita;

View File

@@ -1,10 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { import {
Box, Box,
Button, Button,
Center, Group,
Image, Image,
Paper, Paper,
Select, Select,
@@ -12,18 +16,14 @@ import {
Text, Text,
TextInput, TextInput,
Title, Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { FileInput } from "@mantine/core";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
function EditBerita() { function EditBerita() {
const beritaState = useProxy(stateDashboardBerita); const beritaState = useProxy(stateDashboardBerita);
@@ -33,29 +33,29 @@ function EditBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || '', judul: beritaState.berita.edit.form.judul || "",
deskripsi: beritaState.berita.edit.form.deskripsi || '', deskripsi: beritaState.berita.edit.form.deskripsi || "",
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '', kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
content: beritaState.berita.edit.form.content || '', content: beritaState.berita.edit.form.content || "",
imageId: beritaState.berita.edit.form.imageId || '' imageId: beritaState.berita.edit.form.imageId || "",
}); });
// Load berita by id saat pertama kali // Load berita by id saat pertama kali
useEffect(() => { useEffect(() => {
beritaState.kategoriBerita.findMany.load() beritaState.kategoriBerita.findMany.load();
const loadBerita = async () => { const loadBerita = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy const data = await stateDashboardBerita.berita.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
judul: data.judul || '', judul: data.judul || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
kategoriBeritaId: data.kategoriBeritaId || '', kategoriBeritaId: data.kategoriBeritaId || "",
content: data.content || '', content: data.content || "",
imageId: data.imageId || '', imageId: data.imageId || "",
}); });
if (data?.image?.link) { if (data?.image?.link) {
@@ -69,31 +69,26 @@ function EditBerita() {
}; };
loadBerita(); loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state with form data
beritaState.berita.edit.form = { beritaState.berita.edit.form = {
...beritaState.berita.edit.form, ...beritaState.berita.edit.form,
judul: formData.judul, ...formData,
deskripsi: formData.deskripsi,
content: formData.content,
kategoriBeritaId: formData.kategoriBeritaId || '',
imageId: formData.imageId // Keep existing imageId if not changed
}; };
// Jika ada file baru, upload
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error("Gagal upload gambar");
} }
// Update imageId in global state
beritaState.berita.edit.form.imageId = uploaded.id; beritaState.berita.edit.form.imageId = uploaded.id;
} }
@@ -107,52 +102,111 @@ function EditBerita() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button
</Button> variant="subtle"
</Box> onClick={() => router.back()}
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> p="xs"
<Stack gap={"xs"}> radius="md"
<Title order={3}>Edit Berita</Title> >
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Berita
</Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.judul} value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} setFormData({ ...formData, judul: e.target.value })
placeholder="masukkan judul" }
required
/> />
<TextInput <TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi"
value={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>} setFormData({ ...formData, deskripsi: e.target.value })
placeholder="masukkan deskripsi" }
required
/> />
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
}}
/>
</Box>
)}
</Box>
<Box>
<Text fz="sm" fw="bold">
Konten
</Text>
<EditEditor <EditEditor
value={formData.content} value={formData.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -164,13 +218,15 @@ function EditBerita() {
<Select <Select
value={formData.kategoriBeritaId} value={formData.kategoriBeritaId}
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })} onChange={(val) =>
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} setFormData({ ...formData, kategoriBeritaId: val || "" })
placeholder='Pilih kategori' }
label="Kategori"
placeholder="Pilih kategori"
data={ data={
beritaState.kategoriBerita.findMany.data?.map((v) => ({ beritaState.kategoriBerita.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name label: v.name,
})) || [] })) || []
} }
clearable clearable
@@ -179,7 +235,20 @@ function EditBerita() {
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined} error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/> />
<Button onClick={handleSubmit}>Edit Berita</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,9 +1,8 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -12,107 +11,146 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
function DetailBerita() { function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita) const beritaState = useProxy(stateDashboardBerita);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
beritaState.berita.findUnique.load(params?.id as string) beritaState.berita.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
beritaState.berita.delete.byId(selectedId) beritaState.berita.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/berita/list-berita") router.push("/admin/desa/berita/list-berita");
} }
} };
if (!beritaState.berita.findUnique.data) { if (!beritaState.berita.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = beritaState.berita.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Berita</Text> Kembali
{beritaState.berita.findUnique.data ? ( </Button>
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Detail Berita */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Kategori</Text> withBorder
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text> w={{ base: "100%", md: "70%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Judul</Text> radius="md"
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text> Detail Berita
</Box> </Text>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" /> <Stack gap="sm">
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Kategori</Text>
<Text fw={"bold"} fz={"lg"}>Konten</Text> <Text fz="md" c="dimmed">{data.kategoriBerita?.name || '-'}</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} /> </Box>
</Box>
<Flex gap={"xs"} mt={10}> <Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.judul || 'Gambar Berita'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">Konten</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
/>
</Box>
{/* Action Button */}
<Group gap="sm">
<Tooltip label="Hapus Berita" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (beritaState.berita.findUnique.data) { setSelectedId(data.id);
setSelectedId(beritaState.berita.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Berita" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (beritaState.berita.findUnique.data) { onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!beritaState.berita.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} {/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus berita ini?"
/> />
</Box> </Box>
); );
} }
export default DetailBerita; export default DetailBerita;

View File

@@ -3,46 +3,54 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
export default function CreateBerita() { export default function CreateBerita() {
const beritaState = useProxy(stateDashboardBerita); const beritaState = useProxy(stateDashboardBerita);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load() beritaState.kategoriBerita.findMany.load();
}, []); }, []);
const resetForm = () => { const resetForm = () => {
// Reset state di valtio
beritaState.berita.create.form = { beritaState.berita.create.form = {
judul: "", judul: '',
deskripsi: "", deskripsi: '',
kategoriBeritaId: "", kategoriBeritaId: '',
imageId: "", imageId: '',
content: "", content: '',
}; };
// Reset state lokal
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
@@ -50,40 +58,55 @@ export default function CreateBerita() {
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
// Simpan ID gambar ke form
beritaState.berita.create.form.imageId = uploaded.id; beritaState.berita.create.form.imageId = uploaded.id;
// Submit data berita
await beritaState.berita.create.create(); await beritaState.berita.create.create();
// Reset form setelah submit
resetForm(); resetForm();
router.push("/admin/desa/berita/list-berita") router.push('/admin/desa/berita/list-berita');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> onClick={() => router.back()}
<Stack gap={"xs"}> p="xs"
<Title order={3}>Create Berita</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Berita
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul berita"
value={beritaState.berita.create.form.judul} value={beritaState.berita.create.form.judul}
onChange={(val) => { onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
beritaState.berita.create.form.judul = val.target.value; required
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/> />
<Select <Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>} label="Kategori"
placeholder="Pilih kategori" placeholder="Pilih kategori"
data={beritaState.kategoriBerita.findMany.data.map((item) => ({ data={beritaState.kategoriBerita.findMany.data.map((item) => ({
label: item.name, label: item.name,
@@ -92,48 +115,83 @@ export default function CreateBerita() {
value={beritaState.berita.create.form.kategoriBeritaId || null} value={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => { onChange={(val: string | null) => {
if (val) { if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find((item) => item.id === val); const selected = beritaState.kategoriBerita.findMany.data?.find(
(item) => item.id === val
);
if (selected) { if (selected) {
beritaState.berita.create.form.kategoriBeritaId = selected.id; beritaState.berita.create.form.kategoriBeritaId = selected.id;
} }
} else { } else {
beritaState.berita.create.form.kategoriBeritaId = ""; beritaState.berita.create.form.kategoriBeritaId = '';
} }
}} }}
searchable searchable
clearable clearable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
/> required
<TextInput
value={beritaState.berita.create.form.deskripsi}
onChange={(val) => {
beritaState.berita.create.form.deskripsi = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/> />
<FileInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>} label="Deskripsi Singkat"
value={file} placeholder="Masukkan deskripsi berita"
onChange={async (e) => { value={beritaState.berita.create.form.deskripsi}
if (!e) return; onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/> />
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<CreateEditor <CreateEditor
value={beritaState.berita.create.form.content} value={beritaState.berita.create.form.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -141,7 +199,21 @@ export default function CreateBerita() {
}} }}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,6 +1,25 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -9,15 +28,13 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import stateDashboardBerita from '../../../_state/desa/berita'; import stateDashboardBerita from '../../../_state/desa/berita';
function Berita() { function Berita() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Berita' title="Berita"
placeholder='pencarian' placeholder="Cari judul atau kategori berita..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,103 +45,125 @@ function Berita() {
} }
function ListBerita({ search }: { search: string }) { function ListBerita({ search }: { search: string }) {
const beritaState = useProxy(stateDashboardBerita) const beritaState = useProxy(stateDashboardBerita);
const router = useRouter() const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = beritaState.berita.findMany;
const { data, page, totalPages, loading, load } = beritaState.berita.findMany;
// Fetch data when page or search changes
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search); load(page, 10, search);
}, [page, search]); }, [page, search]);
if (loading || !data) { if (loading || !data) {
return <Skeleton h={500} />; return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
} }
const filteredData = data || []; const filteredData = data || [];
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors["white-1"]} p={"md"}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Grid> <Title order={4}>Daftar Berita</Title>
<GridCol span={{ base: 12, md: 11 }}> <Tooltip label="Tambah Berita" withArrow>
<Text fz={"xl"} fw={"bold"}> <Button
List Berita leftSection={<IconCircleDashedPlus size={18} />}
</Text> color="blue"
</GridCol> variant="light"
<GridCol span={{ base: 12, md: 1 }}> onClick={() => router.push('/admin/desa/berita/list-berita/create')}
<Button
onClick={() => router.push("/admin/desa/berita/list-berita/create")}
bg={colors["blue-button"]}
>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
<Box style={{ overflowX: "auto" }}>
<Table
striped
withRowBorders
withTableBorder
style={{ minWidth: "700px" }}
> >
<TableThead> Tambah Baru
<TableTr> </Button>
<TableTh w={250}>Judul</TableTh> </Tooltip>
<TableTh w={250}>Kategori</TableTh> </Group>
<TableTh w={250}>Image</TableTh>
<TableTh w={200}>Detail</TableTh> <Box style={{ overflowX: 'auto' }}>
</TableTr> <Table highlightOnHover>
</TableThead> <TableThead>
<TableTbody> <TableTr>
{filteredData.map((item) => ( <TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd style={{ width: '30%' }}>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}> {item.judul}
{item.judul} </Text>
</Text> </TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed">
{item.kategoriBerita?.name || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Box
w={80}
h={80}
style={{ borderRadius: 8, overflow: 'hidden' }}
>
{item.image?.link ? (
<Image src={item.image.link} alt="gambar" fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box> </Box>
</TableTd> </TableTd>
<TableTd>{item.kategoriBerita?.name}</TableTd> <TableTd style={{ width: '15%' }}>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Button <Button
bg={"green"} variant="light"
color="blue"
onClick={() => onClick={() =>
router.push(`/admin/desa/berita/list-berita/${item.id}`) router.push(`/admin/desa/berita/list-berita/${item.id}`)
} }
> >
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">
Tidak ada data berita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
); );
} }
export default Berita; export default Berita;

View File

@@ -1,112 +0,0 @@
'use client'
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import React from 'react';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailFoto() {
const fotoState = useProxy(stateGallery.foto)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
fotoState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
fotoState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/gallery/foto")
}
}
if (!fotoState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Foto</Text>
{fotoState.findUnique.data ? (
<Paper key={fotoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{fotoState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Foto</Text>
<Text fz={"lg"}>{new Date(fotoState.findUnique.data?.createdAt).toDateString()}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: fotoState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 300, md: 350}} src={fotoState.findUnique.data?.imageGalleryFoto?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (fotoState.findUnique.data) {
setSelectedId(fotoState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={fotoState.delete.loading || !fotoState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (fotoState.findUnique.data) {
router.push(`/admin/desa/gallery/foto/${fotoState.findUnique.data.id}/edit`);
}
}}
disabled={!fotoState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
}
export default DetailFoto;

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import colors from "@/con/colors";
import stateFileStorage from "@/state/state-list-image"; import stateFileStorage from "@/state/state-list-image";
import { import {
ActionIcon, ActionIcon,
Box, Box,
Card,
Flex, Flex,
Group, Group,
Image, Image,
@@ -13,7 +13,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react"; import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
@@ -29,95 +30,128 @@ export default function ListImage() {
}, []); }, []);
let timeOut: NodeJS.Timer; let timeOut: NodeJS.Timer;
return ( return (
<Stack p={"lg"}> <Stack p="lg" gap="lg">
<Flex justify="space-between"> <Flex justify="space-between" align="center" wrap="wrap" gap="md">
<Title order={3}>List Foto</Title> <Title order={2} fw={700}>
Galeri Foto
</Title>
<TextInput <TextInput
radius={"lg"} radius="xl"
leftSection={<IconSearch />} size="md"
placeholder="Cari foto berdasarkan nama..."
leftSection={<IconSearch size={18} />}
rightSection={ rightSection={
<ActionIcon <ActionIcon
variant="transparent" variant="light"
onClick={() => { color="gray"
stateFileStorage.load(); radius="xl"
}} onClick={() => stateFileStorage.load()}
> >
<IconX /> <IconX size={18} />
</ActionIcon> </ActionIcon>
} }
placeholder="Pencarian"
onChange={(e) => { onChange={(e) => {
if (timeOut) clearTimeout(timeOut); if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => { timeOut = setTimeout(() => {
stateFileStorage.load({ search: e.target.value }); stateFileStorage.load({ search: e.target.value });
}, 200); }, 300);
}} }}
/> />
</Flex> </Flex>
<Paper bg={colors['white-1']} p={'md'}>
<SimpleGrid <Paper withBorder radius="lg" p="md" shadow="sm">
cols={{ {list && list.length > 0 ? (
base: 3, <SimpleGrid
md: 5, cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
lg: 10, spacing="md"
}} verticalSpacing="md"
> >
{list && {list.map((v, k) => (
list.map((v, k) => { <Card
return ( key={k}
<Paper key={k} shadow="sm"> withBorder
<Stack pos={"relative"} gap={0} justify="space-between"> radius="md"
<motion.div shadow="sm"
onClick={() => { className="hover:shadow-md transition-all duration-200"
// copy to clipboard >
navigator.clipboard.writeText(v.url); <Stack gap="xs">
toast("Berhasil disalin"); <motion.div
}} onClick={() => {
whileHover={{ scale: 1.05 }} navigator.clipboard.writeText(v.url);
whileTap={{ scale: 0.8 }} toast("Tautan foto berhasil disalin");
> }}
<Image whileHover={{ scale: 1.05 }}
h={100} whileTap={{ scale: 0.95 }}
src={v.url + "?size=100"} style={{ cursor: "pointer" }}
alt={v.name} >
fit="cover" <Image
loading="lazy" src={`${v.url}?size=200`}
style={{ alt={v.name}
objectFit: "cover", radius="md"
objectPosition: "center", h={120}
}} fit="cover"
/> loading="lazy"
</motion.div> />
<Box p={"md"} h={54}> </motion.div>
<Text lineClamp={2} fz={"xs"}>
{v.name} <Box>
</Text> <Text size="sm" fw={500} lineClamp={2}>
</Box> {v.name}
<Group justify="end"> </Text>
<IconTrash </Box>
<Group justify="space-between" align="center" pt="xs">
<Tooltip label="Hapus foto" withArrow>
<ActionIcon
variant="subtle"
color="red" color="red"
onClick={() => { radius="md"
stateFileStorage.del({ name: v.name }).finally(() => { onClick={() => {
toast("Berhasil dihapus"); stateFileStorage
}); .del({ name: v.name })
}} .finally(() => toast("Foto berhasil dihapus"));
/> }}
</Group> >
</Stack> <IconTrash size={18} />
</Paper> </ActionIcon>
); </Tooltip>
})} </Group>
</SimpleGrid> </Stack>
</Card>
))}
</SimpleGrid>
) : (
<Stack align="center" justify="center" py="xl" gap="sm">
<Image
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
alt="Kosong"
w={120}
h={120}
fit="contain"
opacity={0.7}
/>
<Text c="dimmed" ta="center">
Belum ada foto yang tersedia
</Text>
</Stack>
)}
</Paper> </Paper>
{total && (
<Pagination {total && total > 1 && (
total={total} <Flex justify="center">
onChange={(e) => { <Pagination
stateFileStorage.page = e; total={total}
stateFileStorage.load(); size="md"
}} radius="md"
/> withEdges
onChange={(page) => {
stateFileStorage.page = page;
stateFileStorage.load();
}}
/>
</Flex>
)} )}
</Stack> </Stack>
); );

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconPhoto, IconVideo } from '@tabler/icons-react';
function LayoutTabsGallery({ children }: { children: React.ReactNode }) { function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,16 +13,21 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
{ {
label: "Foto", label: "Foto",
value: "foto", value: "foto",
href: "/admin/desa/gallery/foto" href: "/admin/desa/gallery/foto",
icon: <IconPhoto size={18} stroke={1.8} />,
tooltip: "Kelola foto-foto galeri desa"
}, },
{ {
label: "Video", label: "Video",
value: "video", value: "video",
href: "/admin/desa/gallery/video" href: "/admin/desa/gallery/video",
icon: <IconVideo size={18} stroke={1.8} />,
tooltip: "Kelola video galeri desa"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -39,24 +45,64 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Gallery</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabsGallery; export default LayoutTabsGallery;

View File

@@ -3,7 +3,16 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -12,9 +21,9 @@ import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils'; import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
function EditVideo() { function EditVideo() {
const router = useRouter(); const router = useRouter();
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video);
const params = useParams() const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -30,7 +39,7 @@ function EditVideo() {
const data = await videoState.update.load(id); const data = await videoState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
linkVideo: data.linkVideo || '', linkVideo: data.linkVideo || '',
}); });
@@ -66,27 +75,36 @@ function EditVideo() {
console.error('Error updating video:', error); console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video'); toast.error('Terjadi kesalahan saat memperbarui video');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Edit Video
<Title order={4}>Edit Video</Title> </Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>} label="Judul Video"
placeholder='Masukkan judul video' placeholder="Masukkan judul video"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ ...formData, name: val.target.value }); required
}}
/> />
<Box> <Box>
@@ -94,36 +112,46 @@ function EditVideo() {
label="Link Video YouTube" label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123" placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo} value={formData.linkVideo}
onChange={(e) => { onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
setFormData({ ...formData, linkVideo: e.currentTarget.value });
}}
required required
/> />
{embedLink && ( {embedLink && (
<iframe <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
className="rounded" <iframe
width="100%" className="rounded"
height="200" width="100%"
src={embedLink} height="220"
title="Preview Video" src={embedLink}
allowFullScreen title="Preview Video"
></iframe> allowFullScreen
></iframe>
</Box>
)} )}
</Box> </Box>
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text> <Title order={6} fw="bold" fz="sm" mb={6}>
Deskripsi Video
</Title>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setFormData({ ...formData, deskripsi: val });
}}
/> />
</Box> </Box>
<Group> <Group justify="right">
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,107 +2,145 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailVideo() { function DetailVideo() {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
videoState.findUnique.load(params?.id as string) videoState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
videoState.delete.byId(selectedId) videoState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/gallery/video") router.push("/admin/desa/gallery/video");
} }
} };
if (!videoState.findUnique.data) { if (!videoState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = videoState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Video</Text> Kembali
{videoState.findUnique.data ? ( </Button>
<Paper key={videoState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Detail Video */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Judul</Text> withBorder
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text> w={{ base: "100%", md: "50%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Video</Text> radius="md"
<Box component="iframe" shadow="sm"
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)} >
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Video
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Video</Text>
{data?.linkVideo ? (
<Box
component="iframe"
src={convertToEmbedUrl(data.linkVideo)}
width="100%" width="100%"
height={300} height={300}
allowFullScreen allowFullScreen
style={{ borderRadius: 8 }} style={{ borderRadius: 8 }}
/> />
) : (
<Text fz="sm" c="dimmed">Tidak ada video</Text>
)}
</Box>
</Box> <Box>
<Text fz="lg" fw="bold">Tanggal Video</Text>
<Text fz="md" c="dimmed">
{data?.createdAt ? new Date(data.createdAt).toDateString() : '-'}
</Text>
</Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text> {data?.deskripsi ? (
</Box> <Text
<Box> fz="md"
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> c="dimmed"
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: videoState.findUnique.data?.deskripsi }} /> dangerouslySetInnerHTML={{ __html: data.deskripsi }}
</Box> />
<Flex gap={"xs"} mt={10}> ) : (
<Text fz="sm" c="dimmed">Tidak ada deskripsi</Text>
)}
</Box>
{/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Video" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (videoState.findUnique.data) { setSelectedId(data.id);
setSelectedId(videoState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={videoState.delete.loading || !videoState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Video" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (videoState.findUnique.data) { onClick={() =>
router.push(`/admin/desa/gallery/video/${videoState.findUnique.data.id}/edit`); router.push(`/admin/desa/gallery/video/${data.id}/edit`)
} }
}} variant="light"
disabled={!videoState.findUnique.data} radius="md"
color={"green"} size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -111,17 +149,16 @@ function DetailVideo() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus video ini?"
/> />
</Box> </Box>
); );
function convertToEmbedUrl(youtubeUrl: string): string { function convertToEmbedUrl(youtubeUrl: string): string {
try { try {
const url = new URL(youtubeUrl); const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v"); const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl; if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`; return `https://www.youtube.com/embed/${videoId}`;
} catch (err) { } catch (err) {
console.error("Error converting YouTube URL to embed:", err); console.error("Error converting YouTube URL to embed:", err);

View File

@@ -1,8 +1,18 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -10,77 +20,104 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils'; import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
function CreateVideo() { function CreateVideo() {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video);
const router = useRouter(); const router = useRouter();
const [link, setLink] = useState(""); const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link); const embedLink = convertYoutubeUrlToEmbed(link);
const resetForm = () => { const resetForm = () => {
videoState.create.form = { videoState.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
linkVideo: "", linkVideo: '',
}; };
setLink('');
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!embedLink) { if (!embedLink) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar."); toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return; return;
} }
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga) videoState.create.form.linkVideo = embedLink;
await videoState.create.create(); await videoState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/gallery/video"); router.push('/admin/desa/gallery/video');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header Back Button + Title */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Video
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Video</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>} label="Judul Video"
placeholder='Masukkan judul video' placeholder="Masukkan judul video"
value={videoState.create.form.name} value={videoState.create.form.name}
onChange={(val) => { onChange={(e) => {
videoState.create.form.name = val.target.value; videoState.create.form.name = e.currentTarget.value;
}} }}
required
/> />
<Box>
<Stack gap={"xs"}>
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={link}
onChange={(e) => {
setLink(e.currentTarget.value);
}}
required
/>
{embedLink && ( {/* Link YouTube */}
<iframe <TextInput
style={{ borderRadius: 10, width: "100%", height: 400 }} label="Link Video YouTube"
src={embedLink} placeholder="https://www.youtube.com/watch?v=abc123"
title="Preview Video" value={link}
allowFullScreen onChange={(e) => setLink(e.currentTarget.value)}
></iframe> required
)} />
</Stack>
</Box> {/* Preview Video */}
{embedLink && (
<Box mt="sm">
<iframe
style={{
borderRadius: 10,
width: '100%',
height: 400,
border: '1px solid #ddd',
}}
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
</Box>
)}
{/* Deskripsi */}
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi Video
</Text>
<CreateEditor <CreateEditor
value={videoState.create.form.deskripsi} value={videoState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -88,8 +125,21 @@ function CreateVideo() {
}} }}
/> />
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> {/* Button Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,13 +1,30 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateGallery from '../../../_state/desa/gallery'; import stateGallery from '../../../_state/desa/gallery';
function Video() { function Video() {
@@ -15,8 +32,8 @@ function Video() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posisi Organisasi' title='Video'
placeholder='pencarian' placeholder='Cari judul atau deskripsi video...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,6 +46,7 @@ function Video() {
function ListVideo({ search }: { search: string }) { function ListVideo({ search }: { search: string }) {
const videoState = useProxy(stateGallery.video) const videoState = useProxy(stateGallery.video)
const router = useRouter(); const router = useRouter();
const { const {
data, data,
page, page,
@@ -41,72 +59,104 @@ function ListVideo({ search }: { search: string }) {
load(page, 10, search) load(page, 10, search)
}, [page, search]) }, [page, search])
const filteredData = (data || []) const filteredData = data || []
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Video' <Title order={4}>Daftar Video</Title>
href='/admin/desa/gallery/video/create' <Tooltip label="Tambah Video Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Judul Video</TableTh> onClick={() => router.push('/admin/desa/gallery/video/create')}
<TableTh>Tanggal Video</TableTh> >
<TableTh>Deskripsi Video</TableTh> Tambah Baru
<TableTh>Detail</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Group>
<TableTbody> <Box style={{ overflowX: "auto" }}>
{filteredData.map((item) => ( <Table highlightOnHover>
<TableTr key={item.id}> <TableThead>
<TableTd> <TableTr>
<Box w={200}> <TableTh style={{ width: '25%' }}>Judul Video</TableTh>
<Text lineClamp={1}>{item.name}</Text> <TableTh style={{ width: '20%' }}>Tanggal</TableTh>
</Box> <TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
</TableTd> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTd>
<Box w={200}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text fz="sm" c="dimmed">
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada video yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -3,7 +3,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -49,52 +49,74 @@ function EditPelayananPendudukNonPermanent() {
} }
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="xs">
<Box> <Group mb="md">
<Button <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
variant={'subtle'} <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
onClick={() => router.back()} <IconArrowBack color={colors['blue-button']} size={24} />
> </Button>
<IconArrowBack color={colors['blue-button']} size={20} /> </Tooltip>
</Button> <Title order={4} ml="sm" c="dark">
</Box> Edit Pelayanan Penduduk Non Permanent
<Box> </Title>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> </Group>
<Stack gap={'xs'}>
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title> <Paper
<Text fw={"bold"}>Judul</Text> w={{ base: "100%", md: "50%" }}
<TextInput bg={colors['white-1']}
value={formData.name} p="md"
onChange={(val) => { radius="md"
setFormData({ shadow="sm"
...formData, style={{ border: '1px solid #e0e0e0' }}
name: val.target.value, >
}) <Stack gap="xs">
}} <Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
/>
<Text fw={"bold"}>Deskripsi</Text> {/* Nama Field */}
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required
/>
{/* Posisi Field */}
<Box>
<Text fz="sm" fw="bold">
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(htmlContent) => {
setFormData({ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
...formData,
deskripsi: val,
})
}} }}
/> />
</Box>
<Group>
<Button {/* Submit Button */}
bg={colors['blue-button']} <Group>
onClick={handleSubmit} <Button
loading={statePendudukNonPermanent.update.loading} bg={colors['blue-button']}
> onClick={handleSubmit}
Submit loading={statePendudukNonPermanent.update.loading}
</Button> disabled={!formData.name}
</Group> >
</Stack> {statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Paper> </Button>
</Box>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePendudukNonPermanent.update.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,51 +1,103 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Divider,
Grid,
GridCol,
Paper,
Skeleton,
Stack,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
function SuratKeterangan() { function PelayananPendudukNonPermanent() {
const router = useRouter() const router = useRouter();
const pelayananPendudukNonPermanen = useProxy(stateLayananDesa.pelayananPendudukNonPermanen) const pelayananPendudukNonPermanen = useProxy(
stateLayananDesa.pelayananPendudukNonPermanen
);
useShallowEffect(() => { useShallowEffect(() => {
pelayananPendudukNonPermanen.findById.load('1') pelayananPendudukNonPermanen.findById.load('1');
}, []) }, []);
if (!pelayananPendudukNonPermanen.findById.data) { if (!pelayananPendudukNonPermanen.findById.data) {
return ( return (
<Stack> <Stack align="center" justify="center" py="xl">
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={800} />
</Stack> </Stack>
) );
} }
const data = pelayananPendudukNonPermanen.findById.data;
return ( return (
<Box py={10}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Paper bg={colors['white-1']} p={'md'}> <Stack gap="md">
<Paper bg={colors['BG-trans']} p={'md'}> {/* Header */}
<Box py={15}> <Grid align="center">
<Stack gap={"xs"}> <GridCol span={{ base: 12, md: 11 }}>
<Grid> <Title order={3} c={colors['blue-button']}>
<GridCol span={{ base: 12, md: 11 }}> Preview Pelayanan Penduduk Non Permanen
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit')}> <Tooltip label="Edit Data Pelayanan" withArrow>
<IconEdit size={16} /> <Button
</Button> c="green"
</GridCol> variant="light"
</Grid> leftSection={<IconEdit size={18} stroke={2} />}
</Stack> radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
)
}
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
{/* Content */}
<Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: 0, md: 50 }} pb="xl">
<Center>
<Text
ta="center"
fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']}
>
{data.name}
</Text>
</Center>
<Divider my="md" color={colors['blue-button']} />
<Box mt="lg">
<Text
py={10}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
</Box>
</Box> </Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPendudukNonPermanen.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{ __html: pelayananPendudukNonPermanen.findById.data.deskripsi }} />
</Paper> </Paper>
</Paper> </Stack>
</Box> </Paper>
); );
} }
export default SuratKeterangan; export default PelayananPendudukNonPermanent;

View File

@@ -3,7 +3,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -14,6 +14,7 @@ function EditPelayananPerizinanBerusaha() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams()
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha) const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: statePerizinanBerusaha.findById.data?.name || '', name: statePerizinanBerusaha.findById.data?.name || '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '', deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
@@ -50,64 +51,81 @@ function EditPelayananPerizinanBerusaha() {
} }
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha') router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
} }
return ( return (
<Box> <Box>
<Stack gap={'xs'}> <Stack gap="xs">
<Box> {/* Header Section */}
<Button <Group mb="md">
variant={'subtle'} <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
onClick={() => router.back()} <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
> <IconArrowBack color={colors['blue-button']} size={24} />
<IconArrowBack color={colors['blue-button']} size={20} /> </Button>
</Button> </Tooltip>
</Box> <Title order={4} ml="sm" c="dark">
<Box> Edit Pelayanan Perizinan Berusaha
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}> </Title>
<Stack gap={'xs'}> </Group>
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
<Text fw={"bold"}>Judul</Text> {/* Form Section */}
<TextInput <Paper
value={formData.name} w={{ base: "100%", md: "50%" }}
onChange={(val) => { bg={colors['white-1']}
setFormData({ p="md"
...formData, radius="md"
name: val.target.value, shadow="sm"
}) style={{ border: '1px solid #e0e0e0' }}
}} >
/> <Stack gap="xs">
<Text fw={"bold"}>Link</Text> <Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
<TextInput
value={formData.link} {/* Nama Field */}
onChange={(val) => { <TextInput
setFormData({ label="Judul"
...formData, placeholder="Masukkan judul"
link: val.target.value, value={formData.name}
}) onChange={(e) => setFormData({ ...formData, name: e.target.value })}
}} required
/> />
<Text fw={"bold"}>Deskripsi</Text>
{/* Link Field */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
/>
{/* Deskripsi Field */}
<Box>
<Title order={6}>Deskripsi</Title>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => { onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setFormData({
...formData,
deskripsi: val,
})
}}
/> />
</Box>
<Group>
<Button {/* Action Buttons */}
bg={colors['blue-button']} <Group>
onClick={handleSubmit} <Button
loading={statePerizinanBerusaha.update.loading} bg={colors['blue-button']}
> onClick={handleSubmit}
Submit loading={statePerizinanBerusaha.update.loading}
</Button> disabled={!formData.name}
</Group> >
</Stack> {statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Paper> </Button>
</Box>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePerizinanBerusaha.update.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,6 +1,23 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Group, Paper, Skeleton, Stack, Stepper, StepperCompleted, StepperStep, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Divider,
Grid,
GridCol,
Group,
Paper,
Skeleton,
Stack,
Stepper,
StepperCompleted,
StepperStep,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -9,88 +26,158 @@ import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
function PerizinanBerusaha() { function PerizinanBerusaha() {
const router = useRouter() const router = useRouter();
const pelayananPerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha) const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [active, setActive] = useState(1); const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current)); const nextStep = () =>
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => { useShallowEffect(() => {
pelayananPerizinanBerusaha.findById.load('1') pelayananPerizinanBerusaha.findById.load('1');
}, []) }, []);
if(!pelayananPerizinanBerusaha.findById.data) { if (!pelayananPerizinanBerusaha.findById.data) {
return ( return (
<Stack> <Stack align="center" justify="center" py="xl">
<Skeleton radius={10} h={800} /> <Skeleton radius="md" height={800} />
</Stack> </Stack>
) );
} }
const data = pelayananPerizinanBerusaha.findById.data;
return ( return (
<Box py={10}> <Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Paper bg={colors['white-1']} p={'md'}> <Stack gap="md">
<Paper bg={colors['BG-trans']} p={'md'}> {/* Header */}
<Box py={15}> <Grid align="center">
<Stack gap={"xs"}> <GridCol span={{ base: 12, md: 11 }}>
<Grid> <Title order={3} c={colors['blue-button']}>
<GridCol span={{ base: 12, md: 11 }}> Preview Pelayanan Perizinan Berusaha
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text> </Title>
</GridCol> </GridCol>
<GridCol span={{ base: 12, md: 1 }}> <GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha/edit')}> <Tooltip label="Edit Data Perizinan" withArrow>
<IconEdit size={16} /> <Button
</Button> c="green"
</GridCol> variant="light"
</Grid> leftSection={<IconEdit size={18} stroke={2} />}
</Stack> radius="md"
</Box> onClick={() =>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPerizinanBerusaha.findById.data.name}</Text> router.push(
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{__html: pelayananPerizinanBerusaha.findById.data.deskripsi}} /> '/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
<Text py={10} fz={{ base: "sm", md: 'h3' }}>Proses pendaftaran NIB melalui OSS mencakup beberapa langkah umum, seperti:</Text> )
<Box p={"xl"} w={{ base: "100%", md: "100%" }} >
<Stepper active={active} onStepClick={setActive} orientation="vertical"
styles={{
separator: {
marginLeft: 25
},
step: {
padding: '12px 0'
} }
}}> >
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun"> Edit
Pendaftaran akun pada portal OSS </Button>
</StepperStep> </Tooltip>
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan"> </GridCol>
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya </Grid>
</StepperStep>
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI ">
Memilih KBLI dengan jenis usaha yang akan didaftarkan
</StepperStep>
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
</StepperStep>
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
Proses verifikasi dan persetujuan oleh instansi terkait
</StepperStep>
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
</StepperStep>
<StepperCompleted >
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
</StepperCompleted>
</Stepper>
<Group justify="center" mt="xl"> {/* Content */}
<Button variant="default" onClick={prevStep}>Back</Button> <Paper p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Button onClick={nextStep}>Next step</Button> <Box px={{ base: 0, md: 50 }} pb="xl">
</Group> <Center>
<Text py={35} ta={"justify"} fz={{ base: "sm", md: 'h3' }}>Penting untuk diingat bahwa prosedur dan persyaratan dapat berubah <Text
seiring waktu. Untuk informasi yang lebih akurat dan terkini, saya sarankan untuk mengunjungi situs ta="center"
resmi OSS <a href={pelayananPerizinanBerusaha.findById.data.link}>{pelayananPerizinanBerusaha.findById.data.link}</a> atau menghubungi instansi terkait di pemerintah Indonesia yang bertanggung jawab atas urusan perizinan usaha.</Text> fz={{ base: '1.2rem', md: '1.8rem' }}
fw="bold"
c={colors['blue-button']}
>
{data.name}
</Text>
</Center>
<Divider my="md" color={colors['blue-button']} />
<Box mt="lg">
<Text
py={10}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
/>
<Text
py={10}
fz={{ base: '1rem', md: '1.2rem' }}
fw="bold"
c={colors['blue-button']}
>
Proses pendaftaran NIB melalui OSS mencakup beberapa langkah
umum:
</Text>
<Box p="xl" w="100%">
<Stepper
active={active}
onStepClick={setActive}
orientation="vertical"
styles={{
separator: { marginLeft: 25 },
step: { padding: '12px 0' },
}}
>
<StepperStep label="Langkah Pertama" description="Pendaftaran Akun">
Pendaftaran akun pada portal OSS
</StepperStep>
<StepperStep label="Langkah Kedua" description="Pengisian Data Perusahaan">
Mengisi informasi perusahaan, termasuk data pemegang saham, alamat perusahaan, dan lainnya
</StepperStep>
<StepperStep label="Langkah Ketiga" description="Pemilihan KBLI">
Memilih KBLI dengan jenis usaha yang akan didaftarkan
</StepperStep>
<StepperStep label="Langkah Keempat" description="Pengunggahan Dokumen">
Mengunggah dokumen-dokumen yang diperlukan, seperti akta pendirian perusahaan, surat izin usaha, dan dokumen lainnya sesuai dengan ketentuan yang berlaku
</StepperStep>
<StepperStep label="Langkah Kelima" description="Verifikasi dan Persetujuan">
Proses verifikasi dan persetujuan oleh instansi terkait
</StepperStep>
<StepperStep label="Langkah Keenam" description="Penerimaan NIB">
Jika proses sebelumnya berhasil, perusahaan akan menerima NIB sebagai identitas resmi usaha anda
</StepperStep>
<StepperCompleted>
Selesai, anda telah mengikuti proses pendaftaran NIB melalui OSS
</StepperCompleted>
</Stepper>
<Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>
Back
</Button>
<Button onClick={nextStep}>Next step</Button>
</Group>
</Box>
<Text
py={35}
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
>
Penting untuk diingat bahwa prosedur dan persyaratan dapat
berubah seiring waktu. Untuk informasi yang lebih akurat dan
terkini, silakan kunjungi situs resmi OSS{' '}
<a
href={data.link}
target="_blank"
rel="noopener noreferrer"
style={{ color: colors['blue-button'] }}
>
{data.link}
</a>{' '}
atau hubungi instansi terkait di pemerintah Indonesia yang
bertanggung jawab atas urusan perizinan usaha.
</Text>
</Box>
</Box> </Box>
</Paper> </Paper>
</Paper> </Stack>
</Box> </Paper>
); );
} }

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -13,9 +24,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditSuratKeterangan() { function EditSuratKeterangan() {
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan) const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null); const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -25,39 +37,32 @@ function EditSuratKeterangan() {
deskripsi: stateSurat.edit.form.deskripsi, deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId, imageId: stateSurat.edit.form.imageId,
image2Id: stateSurat.edit.form.image2Id, image2Id: stateSurat.edit.form.image2Id,
}) });
useEffect(() => { useEffect(() => {
const loadSurat = async () => { const loadSurat = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateSurat.edit.load(id); const data = await stateSurat.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || "", name: data.name || '',
deskripsi: data.deskripsi || "", deskripsi: data.deskripsi || '',
imageId: data.imageId || "", imageId: data.imageId || '',
image2Id: data.image2Id || "", image2Id: data.image2Id || '',
}); });
if (data.image?.link) { setPreviewImage(data.image?.link || null);
setPreviewImage(data.image.link); setPreviewImage2(data.image2?.link || null);
} else {
setPreviewImage(null);
}
if (data.image2?.link) {
setPreviewImage2(data.image2.link);
} else {
setPreviewImage2(null);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading surat:", error); console.error('Error loading surat:', error);
toast.error("Gagal memuat data surat"); toast.error('Gagal memuat data surat');
} }
}; };
loadSurat(); loadSurat();
}, [params?.id]); }, [params?.id]);
@@ -65,171 +70,199 @@ function EditSuratKeterangan() {
try { try {
stateSurat.edit.form = { stateSurat.edit.form = {
...stateSurat.edit.form, ...stateSurat.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi, };
imageId: formData.imageId,
image2Id: formData.image2Id,
}
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateSurat.edit.form.imageId = uploaded.id; stateSurat.edit.form.imageId = uploaded.id;
} }
if (file2) { if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name }); const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateSurat.edit.form.image2Id = uploaded.id; stateSurat.edit.form.image2Id = uploaded.id;
} }
await stateSurat.edit.update() await stateSurat.edit.update();
toast.success("Surat berhasil diperbarui!") toast.success('Surat berhasil diperbarui!');
router.push("/admin/desa/layanan/pelayanan_surat_keterangan") router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) { } catch (error) {
console.error("Error updating surat:", error); console.error('Error updating surat:', error);
toast.error("Terjadi kesalahan saat memperbarui surat"); toast.error('Terjadi kesalahan saat memperbarui surat');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Edit Surat Keterangan</Title> <Title order={4} ml="sm" c="dark">
Edit Surat Keterangan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
value={formData.name} value={formData.name}
onChange={(val) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
setFormData({ ...formData, name: val.target.value }); required
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
setFormData({ ...formData, deskripsi: htmlContent });
}}
/> />
</Box> </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setFile(file);
setPreviewImage(URL.createObjectURL(file)); // Buat preview
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> {/* Upload Gambar 1 */}
<Text size="xl" inline> <Box>
Drag images here or click to select files <Text fw="bold" fz="sm" mb={6}>
</Text> Gambar 1
<Text size="sm" c="dimmed" inline mt={7}> </Text>
Attach as many files as you like, each file should not exceed 5mb <Dropzone
</Text> onDrop={(files) => {
</div> const selectedFile = files[0];
</Group> if (selectedFile) {
</Dropzone> setFile(selectedFile);
{previewImage && ( setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview" alt="Preview Gambar 1"
width={280} radius="md"
height={180} style={{
fit="cover" maxHeight: 220,
radius="sm" objectFit: 'contain',
mt="md" border: `1px solid ${colors['blue-button']}`,
}}
/> />
)} </Box>
</Box> )}
</Box> </Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setFile2(file);
setPreviewImage2(URL.createObjectURL(file)); // Buat preview
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> {/* Upload Gambar 2 */}
<Text size="xl" inline> <Box>
Drag images here or click to select files <Text fw="bold" fz="sm" mb={6}>
</Text> Gambar 2
<Text size="sm" c="dimmed" inline mt={7}> </Text>
Attach as many files as you like, each file should not exceed 5mb <Dropzone
</Text> onDrop={(files) => {
</div> const selectedFile = files[0];
</Group> if (selectedFile) {
</Dropzone> setFile2(selectedFile);
{previewImage2 && ( setPreviewImage2(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage2 && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage2} src={previewImage2}
alt="Preview" alt="Preview Gambar 2"
width={280} radius="md"
height={180} style={{
fit="cover" maxHeight: 220,
radius="sm" objectFit: 'contain',
mt="md" border: `1px solid ${colors['blue-button']}`,
}}
/> />
)} </Box>
</Box> )}
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,100 +2,177 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailSuratKeterangan() { function DetailSuratKeterangan() {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan) const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
suratKeteranganState.findUnique.load(params?.id as string) suratKeteranganState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
suratKeteranganState.delete.byId(selectedId) suratKeteranganState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/layanan/pelayanan_surat_keterangan") router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} }
} };
if (!suratKeteranganState.findUnique.data) { if (!suratKeteranganState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => ( <Skeleton height={500} radius="md" />
<Skeleton key={k} h={40} />
))}
</Stack> </Stack>
) );
} }
const data = suratKeteranganState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Surat Keterangan</Text> Kembali
{suratKeteranganState.findUnique.data ? ( </Button>
<Paper key={suratKeteranganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Paper
<Box> withBorder
<Text fw={"bold"} fz={"lg"}>Nama</Text> w={{ base: '100%', md: '60%' }}
<Text fz={"lg"}>{suratKeteranganState.findUnique.data?.name}</Text> bg={colors['white-1']}
</Box> p="lg"
<Box> radius="md"
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> shadow="sm"
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: suratKeteranganState.findUnique.data?.deskripsi }}></Text> >
</Box> <Stack gap="md">
<Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fw={"bold"} fz={"lg"}>Gambar</Text> Detail Surat Keterangan
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image?.link} alt="gambar" /> </Text>
</Box>
<Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Text fw={"bold"} fz={"lg"}>Gambar</Text> <Stack gap="sm">
<Image w={{ base: 150, md: 150, lg: 150 }} src={suratKeteranganState.findUnique.data?.image2?.link} alt="gambar" /> <Box>
</Box> <Text fz="lg" fw="bold">
<Flex gap={"xs"} mt={10}> Nama
</Text>
<Text fz="md" c="dimmed">
{data?.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">
Gambar
</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt="gambar"
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">
Gambar 2
</Text>
{data?.image2?.link ? (
<Image
src={data.image2.link}
alt="gambar"
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (suratKeteranganState.findUnique.data) { setSelectedId(data.id);
setSelectedId(suratKeteranganState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={suratKeteranganState.delete.loading || !suratKeteranganState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={suratKeteranganState.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (suratKeteranganState.findUnique.data) { onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${suratKeteranganState.findUnique.data.id}/edit`); router.push(
} `/admin/desa/layanan/pelayanan_surat_keterangan/${data.id}/edit`
}} )
disabled={!suratKeteranganState.findUnique.data} }
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -104,7 +181,7 @@ function DetailSuratKeterangan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus surat keterangan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,9 +1,21 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -12,25 +24,25 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateSuratKeterangan() { function CreateSuratKeterangan() {
const stateSurat = useProxy(stateLayananDesa.suratKeterangan) const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null); const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null); const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateSurat.create.form = { stateSurat.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
image2Id: "" image2Id: '',
} };
setPreviewImage(null) setPreviewImage(null);
setPreviewImage2(null) setPreviewImage2(null);
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!previewImage) { if (!previewImage) {
return toast.warn("Pilih file gambar utama terlebih dahulu"); return toast.warn('Pilih file gambar utama terlebih dahulu');
} }
try { try {
@@ -42,11 +54,10 @@ function CreateSuratKeterangan() {
const uploadedImage1 = res1.data?.data; const uploadedImage1 = res1.data?.data;
if (!uploadedImage1?.id) { if (!uploadedImage1?.id) {
return toast.error("Gagal upload gambar utama"); return toast.error('Gagal upload gambar utama');
} }
let uploadedImage2 = null; let uploadedImage2 = null;
// Upload gambar kedua jika ada
if (previewImage2) { if (previewImage2) {
const res2 = await ApiFetch.api.fileStorage.create.post({ const res2 = await ApiFetch.api.fileStorage.create.post({
file: previewImage2.file, file: previewImage2.file,
@@ -55,44 +66,58 @@ function CreateSuratKeterangan() {
uploadedImage2 = res2.data?.data; uploadedImage2 = res2.data?.data;
} }
// Set form data
stateSurat.create.form.imageId = uploadedImage1.id; stateSurat.create.form.imageId = uploadedImage1.id;
if (uploadedImage2?.id) { if (uploadedImage2?.id) {
stateSurat.create.form.image2Id = uploadedImage2.id; stateSurat.create.form.image2Id = uploadedImage2.id;
} }
// Create the record
await stateSurat.create.create(); await stateSurat.create.create();
// Reset form dan redirect
resetForm(); resetForm();
toast.success("Data surat keterangan berhasil ditambahkan"); toast.success('Data surat keterangan berhasil ditambahkan');
router.push("/admin/desa/layanan/pelayanan_surat_keterangan"); router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) { } catch (error) {
console.error("Error creating surat keterangan:", error); console.error('Error creating surat keterangan:', error);
toast.error("Terjadi kesalahan saat menambahkan surat keterangan"); toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
} }
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Create Surat Keterangan</Title> <Title order={4} ml="sm" c="dark">
Tambah Surat Keterangan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama Surat */}
<TextInput <TextInput
value={stateSurat.create.form.name} value={stateSurat.create.form.name}
onChange={(val) => { onChange={(val) => (stateSurat.create.form.name = val.target.value)}
stateSurat.create.form.name = val.target.value; label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
}} placeholder="Masukkan nama surat keterangan"
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>} required
placeholder="masukkan nama surat keterangan"
/> />
{/* Konten */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<CreateEditor <CreateEditor
value={stateSurat.create.form.deskripsi} value={stateSurat.create.form.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -100,106 +125,124 @@ function CreateSuratKeterangan() {
}} }}
/> />
</Box> </Box>
{/* Gambar Utama */}
<Box> <Box>
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Utama</Text> <Text fw="bold" fz="sm" mb={6}>
Gambar Utama
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const file = files[0]; const file = files[0];
if (file) { if (file) {
setPreviewImage({ setPreviewImage({
file, file,
preview: URL.createObjectURL(file) preview: URL.createObjectURL(file),
}); });
} }
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ accept={{ 'image/*': [] }}
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] radius="md"
}} p="xl"
> >
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
<Text size="sm" c="dimmed" inline mt={7} display="block">
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Image <Box mt="sm" style={{ textAlign: 'center' }}>
src={previewImage.preview} <Image
alt="Preview Gambar Utama" src={previewImage.preview}
width={280} alt="Preview Gambar Utama"
height={180} radius="md"
fit="cover" style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
radius="sm" />
mt="md" </Box>
/>
)} )}
</Box> </Box>
<Box mt="lg"> {/* Gambar Tambahan */}
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Tambahan (Opsional)</Text> <Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Tambahan (Opsional)
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const file = files[0]; const file = files[0];
if (file) { if (file) {
setPreviewImage2({ setPreviewImage2({
file, file,
preview: URL.createObjectURL(file) preview: URL.createObjectURL(file),
}); });
} }
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ accept={{ 'image/*': [] }}
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] radius="md"
}} p="xl"
> >
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={32} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={32} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={32} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<div>
<Text size="md" inline>Seret gambar ke sini atau klik untuk memilih</Text>
<Text size="sm" c="dimmed" inline mt={7} display="block">
Ukuran maksimal 5MB (JPEG, JPG, PNG, WebP)
</Text>
</div>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone> </Dropzone>
{previewImage2 ? ( {previewImage2 ? (
<Image <Box mt="sm" style={{ textAlign: 'center' }}>
src={previewImage2.preview} <Image
alt="Preview Gambar Tambahan" src={previewImage2.preview}
width={280} alt="Preview Gambar Tambahan"
height={180} radius="md"
fit="cover" style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
radius="sm" />
mt="md" </Box>
/>
) : ( ) : (
<Text size="sm" c="dimmed" mt="sm"> <Text size="sm" c="dimmed" mt="sm" ta="center">
Kosongkan jika tidak ada gambar tambahan Kosongkan jika tidak ada gambar tambahan
</Text> </Text>
)} )}
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,13 +1,30 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
function SuratKeterangan() { function SuratKeterangan() {
@@ -16,7 +33,7 @@ function SuratKeterangan() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Pelayanan Surat Keterangan' title='Pelayanan Surat Keterangan'
placeholder='pencarian' placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,8 +44,8 @@ function SuratKeterangan() {
} }
function ListSuratKeterangan({ search }: { search: string }) { function ListSuratKeterangan({ search }: { search: string }) {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan) const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const router = useRouter() const router = useRouter();
const { const {
data, data,
@@ -39,102 +56,111 @@ function ListSuratKeterangan({ search }: { search: string }) {
} = suratKeteranganState.findMany; } = suratKeteranganState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = useMemo(() => {
if (!data) return [];
const keyword = search.toLowerCase();
return data.filter(item =>
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
}, [data, search]);
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Surat Keterangan'
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Surat Keterangan' <Title order={4}>List Surat Keterangan</Title>
href='/admin/desa/layanan/pelayanan_surat_keterangan/create' <Tooltip label="Tambah Surat Keterangan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama</TableTh> onClick={() =>
<TableTh>Deskripsi</TableTh> router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
<TableTh>Detail</TableTh> }
</TableTr> >
</TableThead> Tambah Baru
<TableTbody> </Button>
{filteredData.map((item) => ( </Tooltip>
<TableTr key={item.id}> </Group>
<TableTd> <Box style={{ overflowX: "auto" }}>
<Box w={200}> <Table highlightOnHover>
<Text truncate="end" fz={"sm"}>{item.name}</Text> <TableThead>
</Box> <TableTr>
</TableTd> <TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTd> <TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<Box w={300}> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> </TableTr>
</Box> </TableThead>
</TableTd> <TableTbody>
<TableTd> {filteredData.length > 0 ? (
<Text> filteredData.map((item) => (
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}> <TableTr key={item.id}>
<IconDeviceImac size={20} /> <TableTd style={{ width: '30%' }}>
</Button> <Box w={200}>
</Text> <Text fw={500} truncate="end" lineClamp={1}>
</TableTd> {item.name}
</TableTr> </Text>
))} </Box>
</TableTbody> </TableTd>
</Table> <TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text truncate="end" lineClamp={1} fz="sm" c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data surat keterangan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10, search);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -2,22 +2,34 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() { function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name, name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi, deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link, link: stateTelunjukDesa.edit.form.link,
}) });
useEffect(() => { useEffect(() => {
const loadPelayananTelunjukSakti = async () => { const loadPelayananTelunjukSakti = async () => {
@@ -27,14 +39,14 @@ function EditPelayananTelunjukSakti() {
const data = await stateTelunjukDesa.edit.load(id); const data = await stateTelunjukDesa.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name, name: data.name || '',
deskripsi: data.deskripsi, deskripsi: data.deskripsi || '',
link: data.link, link: data.link || '',
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pelayanan telunjuk sakti:", error); console.error('Error loading pelayanan telunjuk sakti:', error);
toast.error("Gagal memuat data pelayanan telunjuk sakti"); toast.error('Gagal memuat data pelayanan telunjuk sakti');
} }
}; };
loadPelayananTelunjukSakti(); loadPelayananTelunjukSakti();
@@ -44,57 +56,86 @@ function EditPelayananTelunjukSakti() {
try { try {
stateTelunjukDesa.edit.form = { stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form, ...stateTelunjukDesa.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi, };
link: formData.link, await stateTelunjukDesa.edit.update();
} toast.success('Pelayanan telunjuk sakti berhasil diperbarui!');
await stateTelunjukDesa.edit.update() router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
} catch (error) { } catch (error) {
console.error("Error updating pelayanan telunjuk sakti:", error); console.error('Error updating pelayanan telunjuk sakti:', error);
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti"); toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button + Title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pelayanan Telunjuk Sakti Desa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput
label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
{/* Deskripsi pakai editor */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
/>
</Box> </Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}> {/* Link */}
<Title order={3}>Edit Surat Keterangan</Title> <TextInput
<TextInput label="Link"
value={formData.name} placeholder="Masukkan link terkait"
onChange={(val) => { value={formData.link}
setFormData({ ...formData, name: val.target.value }); onChange={(e) => setFormData({ ...formData, link: e.target.value })}
}} />
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan" {/* Tombol Simpan */}
/> <Group justify="right">
<TextInput <Button
value={formData.deskripsi} onClick={handleSubmit}
onChange={(val) => { radius="md"
setFormData({ ...formData, deskripsi: val.target.value }); size="md"
}} style={{
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>} background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
placeholder="masukkan tautan link" color: '#fff',
/> boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
<TextInput }}
value={formData.link} >
onChange={(val) => { Simpan
setFormData({ ...formData, link: val.target.value }); </Button>
}} </Group>
label={<Text fz={"sm"} fw={"bold"}>Link</Text>} </Stack>
placeholder="masukkan link" </Paper>
/> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -2,109 +2,166 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function DetailPelayananTelunjukSakti() { function DetailPelayananTelunjukSakti() {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const telunjukSaktiState = useProxy(
const [modalHapus, setModalHapus] = useState(false) stateLayananDesa.pelayananTelunjukSaktiDesa
const [selectedId, setSelectedId] = useState<string | null>(null) );
const params = useParams() const [modalHapus, setModalHapus] = useState(false);
const router = useRouter() const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
telunjukSaktiState.findUnique.load(params?.id as string) telunjukSaktiState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
telunjukSaktiState.delete.byId(selectedId) telunjukSaktiState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa") router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
} }
} };
if (!telunjukSaktiState.findUnique.data) { if (!telunjukSaktiState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => ( <Skeleton height={500} radius="md" />
<Skeleton key={k} h={40} />
))}
</Stack> </Stack>
) );
} }
const data = telunjukSaktiState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Pelayanan Telunjuk Sakti Desa</Text> Kembali
{telunjukSaktiState.findUnique.data ? ( </Button>
<Paper key={telunjukSaktiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Paper
<Box> withBorder
<Text fw={"bold"} fz={"lg"}>Nama</Text> w={{ base: '100%', md: '60%' }}
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.name}</Text> bg={colors['white-1']}
</Box> p="lg"
<Box> radius="md"
<Text fw={"bold"} fz={"lg"}>Link</Text> shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Pelayanan Telunjuk Sakti Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Nama
</Text>
<Text fz="md" c="dimmed">
{data?.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Link
</Text>
{data?.link ? (
<Text <Text
fz="md"
component="a" component="a"
href={telunjukSaktiState.findUnique.data?.link} href={data.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
c="blue"
style={{ style={{
display: 'block', display: 'block',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap',
}} }}
> >
{telunjukSaktiState.findUnique.data?.link} {data.link}
</Text> </Text>
</Box> ) : (
<Box> <Text fz="sm" c="dimmed">
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> Tidak ada link
<Text fz={"lg"}>{telunjukSaktiState.findUnique.data?.deskripsi}</Text> </Text>
</Box> )}
<Flex gap={"xs"} mt={10}> </Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}}
/>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Layanan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (telunjukSaktiState.findUnique.data) { setSelectedId(data.id);
setSelectedId(telunjukSaktiState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={telunjukSaktiState.delete.loading || !telunjukSaktiState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={telunjukSaktiState.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Layanan" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (telunjukSaktiState.findUnique.data) { onClick={() =>
router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${telunjukSaktiState.findUnique.data.id}/edit`); router.push(
} `/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${data.id}/edit`
}} )
disabled={!telunjukSaktiState.findUnique.data} }
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -113,7 +170,7 @@ function DetailPelayananTelunjukSakti() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?' text="Apakah Anda yakin ingin menghapus layanan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,64 +1,117 @@
'use client' 'use client';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function CreatePelayananTelunjukDesa() { function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateTelunjukDesa.create.form = { stateTelunjukDesa.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
link: "", link: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateTelunjukDesa.create.create() try {
resetForm() await stateTelunjukDesa.create.create();
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa") resetForm();
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
} catch (error) {
console.error('Error create pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat menambahkan data');
}
};
}
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Create Pelayanan Telunjuk Sakti Desa</Title> <Title order={4} ml="sm" c="dark">
Tambah Pelayanan Telunjuk Sakti Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput <TextInput
value={stateTelunjukDesa.create.form.name} value={stateTelunjukDesa.create.form.name}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value; stateTelunjukDesa.create.form.name = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>} label={<Text fz="sm" fw="bold">Nama Pelayanan</Text>}
placeholder="masukkan nama pelayanan telunjuk sakti desa" placeholder="Masukkan nama pelayanan telunjuk sakti desa"
required
/> />
{/* Deskripsi */}
<TextInput <TextInput
value={stateTelunjukDesa.create.form.deskripsi} value={stateTelunjukDesa.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value; stateTelunjukDesa.create.form.deskripsi = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>} label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan tautan link" placeholder="Masukkan deskripsi pelayanan"
/> />
{/* Link */}
<TextInput <TextInput
value={stateTelunjukDesa.create.form.link} value={stateTelunjukDesa.create.form.link}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value; stateTelunjukDesa.create.form.link = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>} label={<Text fz="sm" fw="bold">Link</Text>}
placeholder="masukkan link" placeholder="Masukkan link pelayanan"
/> />
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,13 +1,187 @@
// /* eslint-disable react-hooks/exhaustive-deps */
// 'use client'
// import colors from '@/con/colors';
// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
// import { useRouter } from 'next/navigation';
// import { useEffect, useMemo, useState } from 'react';
// import { useProxy } from 'valtio/utils';
// import HeaderSearch from '../../../_com/header';
// import JudulList from '../../../_com/judulList';
// import stateLayananDesa from '../../../_state/desa/layananDesa';
// function PelayananTelunjukSakti() {
// const [search, setSearch] = useState("");
// return (
// <Box>
// <HeaderSearch
// title='Posisi Organisasi'
// placeholder='pencarian'
// searchIcon={<IconSearch size={20} />}
// value={search}
// onChange={(e) => setSearch(e.currentTarget.value)}
// />
// <ListPelayananTelunjukSakti search={search} />
// </Box>
// );
// }
// function ListPelayananTelunjukSakti({ search }: { search: string }) {
// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
// const router = useRouter()
// const {
// data,
// page,
// totalPages,
// loading,
// load,
// } = telunjukSaktiState.findMany;
// useEffect(() => {
// load(page, 10)
// }, [])
// const filteredData = useMemo(() => {
// if (!data) return [];
// return data.filter(item => {
// const keyword = search.toLowerCase();
// return (
// item.name?.toLowerCase().includes(keyword) ||
// item.link?.toLowerCase().includes(keyword) ||
// item.deskripsi?.toLowerCase().includes(keyword)
// );
// })
// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
// }, [data, search]);
// if (loading || !data) {
// return (
// <Stack py={10}>
// <Skeleton height={300} />
// </Stack>
// );
// }
// if (data.length === 0) {
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <JudulList
// title='List Pelayanan Telunjuk Sakti Desa'
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
// />
// <Table striped withTableBorder withRowBorders>
// <TableThead>
// <TableTr>
// <TableTh>Nama</TableTh>
// <TableTh>Link</TableTh>
// <TableTh>Detail</TableTh>
// </TableTr>
// </TableThead>
// <TableTbody>
// <TableTr>
// <TableTd colSpan={3}>
// <Text fz={"sm"} color="gray.5">
// Tidak ada data
// </Text>
// </TableTd>
// </TableTr>
// </TableTbody>
// </Table>
// </Paper>
// </Box>
// );
// }
// return (
// <Box py={10}>
// <Paper bg={colors['white-1']} p={'md'}>
// <JudulList
// title='List Pelayanan Telunjuk Sakti Desa'
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
// />
// <Table striped withTableBorder withRowBorders>
// <TableThead>
// <TableTr>
// <TableTh>Nama</TableTh>
// <TableTh>Link</TableTh>
// <TableTh>Detail</TableTh>
// </TableTr>
// </TableThead>
// <TableTbody>
// {filteredData.map((item) => (
// <TableTr key={item.id}>
// <TableTd>
// <Box w={100}>
// <Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
// </Box>
// </TableTd>
// <TableTd>
// <Box w={100}>
// <a href={item.link} target="_blank" rel="noopener noreferrer">
// <Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
// </a>
// </Box>
// </TableTd>
// <TableTd>
// <Text>
// <Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
// <IconDeviceImac size={20} />
// </Button>
// </Text>
// </TableTd>
// </TableTr>
// ))}
// </TableTbody>
// </Table>
// </Paper>
// <Center>
// <Pagination
// value={page}
// onChange={(newPage) => {
// load(newPage, 10);
// window.scrollTo(0, 0);
// }}
// total={totalPages}
// mt="md"
// mb="md"
// />
// </Center>
// </Box>
// );
// }
// export default PelayananTelunjukSakti;
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
function PelayananTelunjukSakti() { function PelayananTelunjukSakti() {
@@ -15,8 +189,8 @@ function PelayananTelunjukSakti() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posisi Organisasi' title="Pelayanan Telunjuk Sakti"
placeholder='pencarian' placeholder="Cari layanan..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,125 +201,113 @@ function PelayananTelunjukSakti() {
} }
function ListPelayananTelunjukSakti({ search }: { search: string }) { function ListPelayananTelunjukSakti({ search }: { search: string }) {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa) const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter() const router = useRouter();
const { const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
data,
page,
totalPages,
loading,
load,
} = telunjukSaktiState.findMany;
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || [];
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
}, [data, search]);
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={400} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pelayanan Telunjuk Sakti Desa'
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd colSpan={3}>
<Text fz={"sm"} color="gray.5">
Tidak ada data
</Text>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pelayanan Telunjuk Sakti Desa' <Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create' <Tooltip label="Tambah Layanan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama</TableTh> onClick={() =>
<TableTh>Link</TableTh> router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
<TableTh>Detail</TableTh> }
</TableTr> >
</TableThead> Tambah Baru
<TableTbody> </Button>
{filteredData.map((item) => ( </Tooltip>
<TableTr key={item.id}> </Group>
<TableTd> <Box style={{ overflowX: 'auto' }}>
<Box w={100}> <Table highlightOnHover style={{ minWidth: '700px' }}>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} /> <TableThead>
</Box> <TableTr>
</TableTd> <TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTd> <TableTh style={{ width: '40%' }}>Link</TableTh>
<Box w={100}> <TableTh style={{ width: '30%' }}>Detail</TableTh>
<a href={item.link} target="_blank" rel="noopener noreferrer">
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
</a>
</Box>
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text></Box>
</TableTd>
<TableTd>
<Box w={200}>
<a href={item.link} target="_blank" rel="noopener noreferrer">
<Text lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
</a>
</Box>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data layanan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10, search);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
@@ -153,3 +315,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
} }
export default PelayananTelunjukSakti; export default PelayananTelunjukSakti;

View File

@@ -4,10 +4,22 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core'; import {
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -83,51 +95,104 @@ function EditPenghargaan() {
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Tombol Back + Title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Edit Penghargaan</Title> <Title order={4} ml="sm" c="dark">
Edit Penghargaan
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Judul */}
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul penghargaan"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} required
placeholder="masukkan judul"
/> />
{/* Input Juara */}
<TextInput <TextInput
label="Juara"
placeholder="Masukkan juara"
value={formData.juara} value={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })} onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>} required
placeholder="masukkan juara"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/> />
{previewImage ? ( {/* Upload Gambar */}
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Gambar Penghargaan
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
/>
</Box>
)}
</Box>
{/* Deskripsi */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -137,7 +202,21 @@ function EditPenghargaan() {
/> />
</Box> </Box>
<Button onClick={handleSubmit}>Edit Penghargaan</Button> {/* Tombol Simpan */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -4,105 +4,166 @@ import penghargaanState from '../../../_state/desa/penghargaan';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPenghargaan() { function DetailPenghargaan() {
const statePenghargaan = useProxy(penghargaanState) const statePenghargaan = useProxy(penghargaanState);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
useShallowEffect(() => { useShallowEffect(() => {
statePenghargaan.findUnique.load(params?.id as string) statePenghargaan.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
statePenghargaan.delete.byId(selectedId) statePenghargaan.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/penghargaan") router.push('/admin/desa/penghargaan');
} }
} };
if (!statePenghargaan.findUnique.data) { if (!statePenghargaan.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = statePenghargaan.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Penghargaan</Text> </Button>
{statePenghargaan.findUnique.data ? (
<Paper key={statePenghargaan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: '100%', md: '60%' }}
<Text fw={"bold"} fz={"lg"}>Gambar</Text> bg={colors['white-1']}
<Image w={{ base: 400, md: 400, lg: 400 }} src={statePenghargaan.findUnique.data?.image?.link} alt="gambar" /> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fw={"bold"} fz={"lg"}>Judul</Text> >
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.name}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail Penghargaan
<Text fw={"bold"} fz={"lg"}>Juara</Text> </Text>
<Text fz={"lg"}>{statePenghargaan.findUnique.data?.juara}</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Box>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePenghargaan.findUnique.data?.deskripsi }} /> <Text fz="lg" fw="bold">
</Box> Gambar
<Flex gap={"xs"} mt={10}> </Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Penghargaan'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">
Tidak ada gambar
</Text>
)}
</Box>
<Box>
<Text fz="lg" fw="bold">
Judul
</Text>
<Text fz="md" c="dimmed">
{data.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Juara
</Text>
<Text fz="md" c="dimmed">
{data.juara || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Group gap="sm" mt={10}>
<Tooltip label="Hapus Penghargaan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (statePenghargaan.findUnique.data) { setSelectedId(data.id);
setSelectedId(statePenghargaan.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={statePenghargaan.delete.loading || !statePenghargaan.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Penghargaan" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (statePenghargaan.findUnique.data) { onClick={() =>
router.push(`/admin/desa/penghargaan/${statePenghargaan.findUnique.data.id}/edit`); router.push(`/admin/desa/penghargaan/${data.id}/edit`)
} }
}} variant="light"
disabled={!statePenghargaan.findUnique.data} radius="md"
color={"green"} size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus penghargaan ini?' text="Apakah Anda yakin ingin menghapus penghargaan ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,83 +1,109 @@
'use client' 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import ApiFetch from '@/lib/api-fetch';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import penghargaanState from '../../../_state/desa/penghargaan';
import ApiFetch from '@/lib/api-fetch';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import penghargaanState from '../../../_state/desa/penghargaan';
function CreatePenghargaan() { function CreatePenghargaan() {
const statePenghargaan = useProxy(penghargaanState) const statePenghargaan = useProxy(penghargaanState);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
statePenghargaan.create.form = { statePenghargaan.create.form = {
name: "", name: '',
juara: "", juara: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
} };
setPreviewImage(null) setPreviewImage(null);
setFile(null) setFile(null);
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu") return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file: file, file,
name: file.name name: file.name,
}) });
const uploaded = res.data?.data;
const uploaded = res.data?.data
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar") return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
statePenghargaan.create.form.imageId = uploaded.id statePenghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.create.create() await statePenghargaan.create.create();
resetForm() resetForm();
router.push("/admin/desa/penghargaan") router.push('/admin/desa/penghargaan');
};
}
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Create Penghargaan</Title> <Title order={4} ml="sm" c="dark">
Tambah Penghargaan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={statePenghargaan.create.form.name} value={statePenghargaan.create.form.name}
onChange={(val) => { onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
statePenghargaan.create.form.name = val.target.value; label={<Text fz="sm" fw="bold">Nama Penghargaan</Text>}
}} placeholder="Masukkan nama penghargaan"
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>} required
placeholder="masukkan nama penghargaan"
/> />
<TextInput <TextInput
value={statePenghargaan.create.form.juara} value={statePenghargaan.create.form.juara}
onChange={(val) => { onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
statePenghargaan.create.form.juara = val.target.value; label={<Text fz="sm" fw="bold">Juara</Text>}
}} placeholder="Masukkan juara"
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>} required
placeholder="masukkan juara"
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold" mb={6}>Deskripsi</Text>
<CreateEditor <CreateEditor
value={statePenghargaan.create.form.deskripsi} value={statePenghargaan.create.form.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -85,26 +111,67 @@ function CreatePenghargaan() {
}} }}
/> />
</Box> </Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>} {/* Dropzone Upload */}
value={file} <Box>
onChange={async (e) => { <Text fz="sm" fw="bold" mb={6}>Gambar</Text>
if (!e) return; <Dropzone
setFile(e); onDrop={(files) => {
const base64 = await e.arrayBuffer().then((buf) => const selectedFile = files[0];
"data:image/png;base64," + Buffer.from(buf).toString("base64") if (selectedFile) {
); setFile(selectedFile);
setPreviewImage(base64); setPreviewImage(URL.createObjectURL(selectedFile));
}} }
/> }}
{previewImage ? ( onReject={() => toast.error('File tidak valid, gunakan format gambar')}
<Image alt="" src={previewImage} w={200} h={200} /> maxSize={5 * 1024 ** 2}
) : ( accept={{ 'image/*': [] }}
<Center w={200} h={200} bg={"gray"}> radius="md"
<IconImageInPicture /> p="xl"
</Center> >
)} <Group justify="center" gap="xl" mih={180}>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button> <Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
/>
</Box>
)}
</Box>
{/* Button Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -2,21 +2,38 @@
'use client' 'use client'
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan'; import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
function Penghargaan() { function Penghargaan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Penghargaan' title="Penghargaan"
placeholder='pencarian' placeholder="Cari nama atau deskripsi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,125 +44,114 @@ function Penghargaan() {
} }
function ListPenghargaan({ search }: { search: string }) { function ListPenghargaan({ search }: { search: string }) {
const state = useProxy(penghargaanState) const state = useProxy(penghargaanState);
const router = useRouter() const router = useRouter();
const { const { data, page, totalPages, loading, load } = state.findMany;
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state // Loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { return (
return ( <Box py={10}>
<Box py={10}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={'md'}> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>List Penghargaan</Title>
title='List Penghargaan' <Tooltip label="Tambah Penghargaan" withArrow>
href='/admin/desa/penghargaan/create' <Button
/> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() => router.push('/admin/desa/penghargaan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh>Deskripsi</TableTh> <TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh>Image</TableTh> <TableTh style={{ width: '30%' }}>Aksi</TableTh>
<TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
<TableTr> {filteredData.length > 0 ? (
<TableTd colSpan={4}> filteredData.map((item) => (
<Text ta="center">Tidak ada data</Text> <TableTr key={item.id}>
</TableTd> <TableTd>
</TableTr> <Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text
truncate="end"
lineClamp={1}
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/penghargaan/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data penghargaan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Penghargaan'
href='/admin/desa/penghargaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text lineClamp={1} truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={100}>
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/penghargaan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10, search);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconListDetails, IconCategory } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) { function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,16 +13,21 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
{ {
label: "List Pengumuman", label: "List Pengumuman",
value: "listpengumuman", value: "listpengumuman",
href: "/admin/desa/pengumuman/list-pengumuman" href: "/admin/desa/pengumuman/list-pengumuman",
icon: <IconListDetails size={18} stroke={1.8} />,
tooltip: "Lihat semua daftar pengumuman"
}, },
{ {
label: "Kategori Pengumuman", label: "Kategori Pengumuman",
value: "kategoripengumuman", value: "kategoripengumuman",
href: "/admin/desa/pengumuman/kategori-pengumuman" href: "/admin/desa/pengumuman/kategori-pengumuman",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori pengumuman"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -39,24 +45,59 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Pengumuman</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabsLayanan; export default LayoutTabsLayanan;

View File

@@ -1,8 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
Text,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,67 +20,108 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriPengumuman() { function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category) const editState = useProxy(stateDesaPengumuman.category);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editState.update.form.name || '', name: editState.update.form.name || '',
}); });
useEffect(() => { useEffect(() => {
const loadKategori = async () => { const loadKategori = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori Pengumuman:", error);
toast.error("Gagal memuat data kategori Pengumuman");
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try { try {
editState.update.form = { const data = await editState.update.load(id);
...editState.update.form, if (data) {
name: formData.name, setFormData({
}; name: data.name || '',
await editState.update.update(); });
toast.success('Kategori Pengumuman berhasil diperbarui!'); }
router.push('/admin/desa/pengumuman/kategori-pengumuman');
} catch (error) { } catch (error) {
console.error('Error updating kategori Pengumuman:', error); console.error('Error loading kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman'); toast.error('Gagal memuat data kategori Pengumuman');
} }
}; };
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
name: formData.name,
};
await editState.update.update();
toast.success('Kategori Pengumuman berhasil diperbarui!');
router.push('/admin/desa/pengumuman/kategori-pengumuman');
} catch (error) {
console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
}
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> onClick={() => router.back()}
<Stack gap={"xs"}> p="xs"
<Title order={3}>Edit Kategori Pengumuman</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Kategori Pengumuman
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={
<Text fz="sm" fw="bold">
Nama Kategori Pengumuman
</Text>
}
placeholder="Masukkan nama kategori Pengumuman"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>} setFormData({ ...formData, name: e.target.value })
placeholder="masukkan nama kategori Pengumuman" }
required
/> />
<Button onClick={handleSubmit}>Simpan</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,50 +1,87 @@
'use client' 'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriPengumuman() { function CreateKategoriPengumuman() {
const createState = useProxy(stateDesaPengumuman.category) const createState = useProxy(stateDesaPengumuman.category);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
name: "", name: '',
}; };
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/pengumuman/kategori-pengumuman") router.push('/admin/desa/pengumuman/kategori-pengumuman');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Pengumuman
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form utama */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Pengumuman</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Pengumuman</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Pengumuman</Text>}
placeholder='Masukkan nama kategori Pengumuman' placeholder="Masukkan nama kategori pengumuman"
value={createState.create.form.name} value={createState.create.form.name || ''}
onChange={(val) => { onChange={(e) => (createState.create.form.name = e.target.value)}
createState.create.form.name = val.target.value; required
}}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,25 +1,26 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; Box, Button, Center, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title, Tooltip, Pagination
} from '@mantine/core';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '../../../_state/desa/pengumuman'; import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function KategoriPengumuman() { function KategoriPengumuman() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Pengumuman' title='Kategori Pengumuman'
placeholder='pencarian' placeholder='Cari nama kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,87 +35,121 @@ function ListKategoriPengumuman({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const { data, page, totalPages, loading, load } = listDataState.findMany;
useEffect(() => { useEffect(() => {
listDataState.findMany.load() load(1, 10, search)
}, []) }, [search])
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
load(page, 10, search)
listDataState.findMany.load()
} }
} }
const filteredData = (listDataState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Stack>
<JudulList <Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
title='List Kategori Pengumuman' <Title order={4}>List Kategori Pengumuman</Title>
href='/admin/desa/pengumuman/kategori-pengumuman/create' <Tooltip label="Tambah Kategori Pengumuman" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh>Nama</TableTh> <TableTh style={{ width: '60%' }}>Nama</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '15%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item, index) => (
<TableTd> <TableTr key={item.id}>
<Box w={100}> <TableTd>
<Text truncate="end" fz={"sm"}>{index + 1}</Text> <Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
</Box> </TableTd>
</TableTd> <TableTd>
<TableTd>{item.name}</TableTd> <Text truncate lineClamp={1}>{item.name}</Text>
<TableTd> </TableTd>
<Button color='green' onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}> <TableTd>
<IconEdit size={20} /> <Tooltip label="Edit Kategori Pengumuman" withArrow>
</Button> <Button
</TableTd> variant='light'
<TableTd> color='green'
<Button onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
color='red' >
disabled={listDataState.delete.loading} <IconEdit size={20} />
onClick={() => { </Button>
setSelectedId(item.id) </Tooltip>
setModalHapus(true) </TableTd>
}}> <TableTd>
<IconTrash size={20} /> <Tooltip label="Hapus Kategori Pengumuman" withArrow>
</Button> <Button
variant='light'
color='red'
disabled={listDataState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data kategori pengumuman yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} <Center mt="md">
<Pagination
value={page}
onChange={(newPage) => load(newPage, 10, search)}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -7,12 +7,14 @@ import colors from "@/con/colors";
import { import {
Box, Box,
Button, Button,
Group,
Paper, Paper,
Select, Select,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react"; import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@@ -20,34 +22,34 @@ import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
function EditPengumuman() { function EditPengumuman() {
const editState = useProxy(stateDesaPengumuman); const editState = useProxy(stateDesaPengumuman);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: editState.pengumuman.edit.form.judul || '', judul: editState.pengumuman.edit.form.judul || "",
deskripsi: editState.pengumuman.edit.form.deskripsi || '', deskripsi: editState.pengumuman.edit.form.deskripsi || "",
categoryPengumumanId: editState.pengumuman.edit.form.categoryPengumumanId || '', categoryPengumumanId:
content: editState.pengumuman.edit.form.content || '' editState.pengumuman.edit.form.categoryPengumumanId || "",
content: editState.pengumuman.edit.form.content || "",
}); });
// Load pengumuman by id saat pertama kali // Load pengumuman by id saat pertama kali
useEffect(() => { useEffect(() => {
editState.category.findMany.load() editState.category.findMany.load();
const loadpengumuman = async () => { const loadpengumuman = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateDesaPengumuman.pengumuman.edit.load(id); // akses langsung, bukan dari proxy const data = await stateDesaPengumuman.pengumuman.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
judul: data.judul || '', judul: data.judul || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
categoryPengumumanId: data.categoryPengumumanId || '', categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || '', content: data.content || "",
}); });
} }
} catch (error) { } catch (error) {
@@ -57,21 +59,18 @@ function EditPengumuman() {
}; };
loadpengumuman(); loadpengumuman();
}, [params?.id]); // ✅ hapus editState dari dependency }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// edit global state with form data // update global state
editState.pengumuman.edit.form = { editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form, ...editState.pengumuman.edit.form,
judul: formData.judul, ...formData,
deskripsi: formData.deskripsi,
content: formData.content,
categoryPengumumanId: formData.categoryPengumumanId || ''
}; };
await editState.pengumuman.edit.update(); await editState.pengumuman.edit.update();
toast.success("pengumuman berhasil diperbarui!"); toast.success("Pengumuman berhasil diperbarui!");
router.push("/admin/desa/pengumuman/list-pengumuman"); router.push("/admin/desa/pengumuman/list-pengumuman");
} catch (error) { } catch (error) {
console.error("Error updating pengumuman:", error); console.error("Error updating pengumuman:", error);
@@ -80,57 +79,97 @@ function EditPengumuman() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button
</Button> variant="subtle"
</Box> onClick={() => router.back()}
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> p="xs"
<Stack gap={"xs"}> radius="md"
<Title order={3}>Edit pengumuman</Title> >
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pengumuman
</Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul Pengumuman"
placeholder="Masukkan judul"
value={formData.judul} value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })} onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} required
placeholder="masukkan judul"
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
value={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>} setFormData({ ...formData, deskripsi: e.target.value })
placeholder="masukkan deskripsi" }
required
/> />
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, content: htmlContent }));
editState.pengumuman.edit.form.content = htmlContent;
}}
/>
</Box>
<Select <Select
value={formData.categoryPengumumanId} value={formData.categoryPengumumanId}
onChange={(val) => setFormData({ ...formData, categoryPengumumanId: val || "" })} onChange={(val) =>
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} setFormData({ ...formData, categoryPengumumanId: val || "" })
placeholder='Pilih kategori' }
label="Kategori"
placeholder="Pilih kategori"
data={ data={
editState.category.findMany.data?.map((v) => ({ editState.category.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.name label: v.name,
})) || [] })) || []
} }
clearable clearable
searchable searchable
required required
error={!formData.categoryPengumumanId ? "Pilih kategori" : undefined} error={
!formData.categoryPengumumanId ? "Pilih kategori" : undefined
}
/> />
<Button onClick={handleSubmit}>Edit pengumuman</Button> <Box>
<Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) =>
setFormData({ ...formData, content: htmlContent })
}
/>
</Box>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,116 +1,163 @@
'use client' 'use client'
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { useProxy } from 'valtio/utils'; import {
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
export default function DetailPengumuman() {
function DetailPengumuman() { const pengumumanState = useProxy(stateDesaPengumuman);
const pengumumanState = useProxy(stateDesaPengumuman) const [modalHapus, setModalHapus] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null) const params = useParams();
const params = useParams() const router = useRouter();
const router = useRouter()
useShallowEffect(() => { useShallowEffect(() => {
pengumumanState.pengumuman.findUnique.load(params?.id as string) pengumumanState.pengumuman.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
pengumumanState.pengumuman.delete.byId(selectedId) pengumumanState.pengumuman.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/pengumuman/list-pengumuman") router.push('/admin/desa/pengumuman/list-pengumuman');
} }
} };
if (!pengumumanState.pengumuman.findUnique.data) { if (!pengumumanState.pengumuman.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={400} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = pengumumanState.pengumuman.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Pengumuman</Text> </Button>
{pengumumanState.pengumuman.findUnique.data ? (
<Paper key={pengumumanState.pengumuman.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: '100%', md: '60%' }}
<Text fw={"bold"} fz={"lg"}>Kategori</Text> bg={colors['white-1']}
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.CategoryPengumuman?.name}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fw={"bold"} fz={"lg"}>Judul</Text> >
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.judul}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail Pengumuman
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> </Text>
<Text fz={"lg"}>{pengumumanState.pengumuman.findUnique.data?.deskripsi}</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Konten</Text> <Box>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: pengumumanState.pengumuman.findUnique.data?.content }} /> <Text fz="lg" fw="bold">
</Box> Kategori
<Flex gap={"xs"} mt={10}> </Text>
<Text fz="md" c="dimmed">
{data?.CategoryPengumuman?.name || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Judul
</Text>
<Text fz="md" c="dimmed">
{data?.judul || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Deskripsi
</Text>
<Text fz="md" c="dimmed">
{data?.deskripsi || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Konten
</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{
__html: data?.content || '-',
}}
/>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Pengumuman" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (pengumumanState.pengumuman.findUnique.data) { setSelectedId(data.id);
setSelectedId(pengumumanState.pengumuman.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={pengumumanState.pengumuman.delete.loading || !pengumumanState.pengumuman.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Pengumuman" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (pengumumanState.pengumuman.findUnique.data) { onClick={() =>
router.push(`/admin/desa/pengumuman/list-pengumuman/${pengumumanState.pengumuman.findUnique.data.id}/edit`); router.push(
} `/admin/desa/pengumuman/list-pengumuman/${data.id}/edit`
}} )
disabled={!pengumumanState.pengumuman.findUnique.data} }
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengumuman ini?' text="Apakah anda yakin ingin menghapus pengumuman ini?"
/> />
</Box> </Box>
); );
} }
export default DetailPengumuman;

View File

@@ -1,79 +1,110 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman'; import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePengumuman() { function CreatePengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman) const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
pengumumanState.category.findMany.load() pengumumanState.category.findMany.load();
}, []) }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
await pengumumanState.pengumuman.create.create() await pengumumanState.pengumuman.create.create();
resetForm() resetForm();
router.push("/admin/desa/pengumuman/list-pengumuman") router.push('/admin/desa/pengumuman/list-pengumuman');
} };
const resetForm = () => { const resetForm = () => {
pengumumanState.pengumuman.create.form = { pengumumanState.pengumuman.create.form = {
judul: "", judul: '',
deskripsi: "", deskripsi: '',
content: "", content: '',
categoryPengumumanId: "", categoryPengumumanId: '',
}; };
}; };
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={4}>Create Pengumuman</Title> {/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pengumuman
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>} value={pengumumanState.pengumuman.create.form.judul}
placeholder='Masukkan judul' onChange={(val) => (pengumumanState.pengumuman.create.form.judul = val.target.value)}
onChange={(val) => { label={<Text fz="sm" fw="bold">Judul</Text>}
pengumumanState.pengumuman.create.form.judul = val.target.value placeholder="Masukkan judul pengumuman"
}} required
/> />
{/* Kategori */}
<Select <Select
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder='Pilih kategori' placeholder="Pilih kategori"
value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""}
onChange={(val) => {
pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
}}
data={pengumumanState.category.findMany.data?.map((item) => ({ data={pengumumanState.category.findMany.data?.map((item) => ({
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
onChange={(val) => {
const selected = pengumumanState.category.findMany.data?.find((item) => item.id === val);
if (selected) {
pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
}
}}
searchable searchable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
/> />
{/* Deskripsi Singkat */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>} value={pengumumanState.pengumuman.create.form.deskripsi}
placeholder='Masukkan deskripsi singkat' onChange={(val) => (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
onChange={(val) => { label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
pengumumanState.pengumuman.create.form.deskripsi = val.target.value placeholder="Masukkan deskripsi singkat"
}} required
/> />
{/* Konten Editor */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<CreateEditor <CreateEditor
value={pengumumanState.pengumuman.create.form.content} value={pengumumanState.pengumuman.create.form.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -82,8 +113,20 @@ function CreatePengumuman() {
/> />
</Box> </Box>
<Group> {/* Tombol Submit */}
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,6 +1,25 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Grid, GridCol, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -9,14 +28,13 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import stateDesaPengumuman from '../../../_state/desa/pengumuman'; import stateDesaPengumuman from '../../../_state/desa/pengumuman';
function Pengumuman() { function Pengumuman() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Pengumuman' title="Pengumuman Desa"
placeholder='pencarian' placeholder="Cari judul atau kategori..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,86 +45,107 @@ function Pengumuman() {
} }
function ListPengumuman({ search }: { search: string }) { function ListPengumuman({ search }: { search: string }) {
const pengumumanState = useProxy(stateDesaPengumuman) const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter() const router = useRouter();
const {
data, const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany;
page,
totalPages,
loading,
load,
} = pengumumanState.pengumuman.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
const filteredData = (data || []) const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Grid> <Title order={4}>Daftar Pengumuman</Title>
<GridCol span={{ base: 12, md: 11 }}> <Tooltip label="Tambah Pengumuman" withArrow>
<Text fz={"xl"} fw={"bold"}>List Pengumuman</Text> <Button
</GridCol> leftSection={<IconCircleDashedPlus size={18} />}
<GridCol span={{ base: 12, md: 1 }}> color="blue"
<Button onClick={() => router.push("/admin/desa/pengumuman/list-pengumuman/create")} bg={colors['blue-button']}> variant="light"
<IconCircleDashedPlus size={25} /> onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
</Button> >
</GridCol> Tambah Baru
</Grid> </Button>
<Box style={{ overflowX: "auto" }}> </Tooltip>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> </Group>
<TableThead> <Box style={{ overflowX: 'auto' }}>
<TableTr> <Table highlightOnHover style={{ minWidth: '700px' }}>
<TableTh w={250}>Judul</TableTh> <TableThead>
<TableTh w={250}>Kategori</TableTh> <TableTr>
<TableTh w={200}>Detail</TableTh> <TableTh style={{ width: '40%' }}>Judul</TableTh>
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
</TableTr> <TableTh style={{ width: '20%' }}>Detail</TableTh>
</TableThead> </TableTr>
<TableTbody > </TableThead>
{filteredData.map((item) => ( <TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd > <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.judul}</Text> {item.judul}
</Box> </Text>
</TableTd> </TableTd>
<TableTd >{item.CategoryPengumuman?.name}</TableTd> <TableTd>
<TableTd> <Text fz="sm" c="dimmed">
<Button bg={"green"} onClick={() => router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)}> {item.CategoryPengumuman?.name || '-'}
<IconDeviceImacCog size={25} /> </Text>
</Button> </TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada pengumuman yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default Pengumuman; export default Pengumuman;

View File

@@ -1,7 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconCategory, IconListCheck } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -12,17 +13,21 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
{ {
label: "List Potensi", label: "List Potensi",
value: "list_potensi", value: "list_potensi",
href: "/admin/desa/potensi/list-potensi" href: "/admin/desa/potensi/list-potensi",
icon: <IconListCheck size={18} stroke={1.8} />,
tooltip: "Lihat semua potensi desa"
}, },
{ {
label: "Kategori Potensi", label: "Kategori Potensi",
value: "kategori_potensi", value: "kategori_potensi",
href: "/admin/desa/potensi/kategori-potensi" href: "/admin/desa/potensi/kategori-potensi",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Kelola kategori potensi"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -40,24 +45,59 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Potensi</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabsPotensi; export default LayoutTabsPotensi;

View File

@@ -1,8 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -10,67 +19,95 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKategoriPotensi() { function EditKategoriPotensi() {
const editState = useProxy(potensiDesaState.kategoriPotensi) const editState = useProxy(potensiDesaState.kategoriPotensi);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: editState.update.form.nama || '', nama: editState.update.form.nama || '',
}); });
useEffect(() => { useEffect(() => {
const loadKategori = async () => { const loadKategori = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading kategori potensi:", error);
toast.error("Gagal memuat data kategori potensi");
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try { try {
editState.update.form = { const data = await editState.update.load(id);
...editState.update.form, if (data) {
nama: formData.nama, setFormData({
}; nama: data.nama || '',
await editState.update.update(); });
toast.success('Kategori Potensi berhasil diperbarui!'); }
router.push('/admin/desa/potensi/kategori-potensi');
} catch (error) { } catch (error) {
console.error('Error updating kategori potensi:', error); console.error('Error loading kategori potensi:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori potensi'); toast.error('Gagal memuat data kategori potensi');
} }
}; };
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
nama: formData.nama,
};
await editState.update.update();
toast.success('Kategori Potensi berhasil diperbarui!');
router.push('/admin/desa/potensi/kategori-potensi');
} catch (error) {
console.error('Error updating kategori potensi:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori potensi');
}
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Kategori Potensi</Title> Edit Kategori Potensi
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi"
value={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Potensi</Text>} required
placeholder="masukkan nama kategori potensi"
/> />
<Button onClick={handleSubmit}>Simpan</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,50 +1,87 @@
'use client' 'use client';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriPotensi() { function CreateKategoriPotensi() {
const createState = useProxy(potensiDesaState.kategoriPotensi) const createState = useProxy(potensiDesaState.kategoriPotensi);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
nama: "", nama: '',
}; };
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push("/admin/desa/potensi/kategori-potensi") router.push('/admin/desa/potensi/kategori-potensi');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kategori Potensi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form utama */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Kategori Potensi</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Potensi</Text>} label={<Text fw="bold" fz="sm">Nama Kategori Potensi</Text>}
placeholder='Masukkan nama kategori Potensi' placeholder="Masukkan nama kategori potensi"
value={createState.create.form.nama} value={createState.create.form.nama || ''}
onChange={(val) => { onChange={(e) => (createState.create.form.nama = e.target.value)}
createState.create.form.nama = val.target.value; required
}}
/> />
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,17 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function KategoriPotensi() { function KategoriPotensi() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -19,7 +16,7 @@ function KategoriPotensi() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kategori Potensi' title='Kategori Potensi'
placeholder='pencarian' placeholder='Cari nama kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,87 +31,113 @@ function ListKategoriPotensi({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const { data, page, totalPages, loading, load } = listDataState.findMany;
useEffect(() => { useEffect(() => {
listDataState.findMany.load() load(1, 10, search)
}, []) }, [search])
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
listDataState.delete.delete(selectedId) listDataState.delete.delete(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
load(page, 10, search)
listDataState.findMany.load()
} }
} }
const filteredData = (listDataState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Stack>
<JudulList <Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
title='List Kategori Potensi' <Title order={4}>List Kategori Potensi</Title>
href='/admin/desa/potensi/kategori-potensi/create' <Tooltip label="Tambah Kategori Potensi" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/potensi/kategori-potensi/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Box>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh style={{ width: '10%' }}>No</TableTh>
<TableTh>Nama</TableTh> <TableTh style={{ width: '60%' }}>Nama</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh>Hapus</TableTh> <TableTh style={{ width: '15%' }}>Hapus</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item, index) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item, index) => (
<TableTd> <TableTr key={item.id}>
<Box w={100}> <TableTd>
<Text truncate="end" fz={"sm"}>{index + 1}</Text> <Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
</Box> </TableTd>
</TableTd> <TableTd>
<TableTd>{item.nama}</TableTd> <Text truncate lineClamp={1}>{item.nama}</Text>
<TableTd> </TableTd>
<Button color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}> <TableTd>
<IconEdit size={20} /> <Button variant='light' color='green' onClick={() => router.push(`/admin/desa/potensi/kategori-potensi/${item.id}`)}>
</Button> <IconEdit size={20} />
</TableTd> </Button>
<TableTd> </TableTd>
<Button <TableTd>
color='red' <Button
disabled={listDataState.delete.loading} variant='light'
onClick={() => { color='red'
setSelectedId(item.id) disabled={listDataState.delete.loading}
setModalHapus(true) onClick={() => {
}}> setSelectedId(item.id)
<IconTrash size={20} /> setModalHapus(true)
</Button> }}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kategori potensi yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} <Center mt="md">
<Pagination
value={page}
onChange={(newPage) => load(newPage, 10, search)}
total={totalPages}
color="blue"
radius="md"
/>
</Center>
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -5,7 +5,19 @@ import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi"; import potensiDesaState from "@/app/admin/(dashboard)/_state/desa/potensi";
import colors from "@/con/colors"; import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from "@mantine/core"; import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@@ -13,38 +25,36 @@ import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
function EditPotensi() { function EditPotensi() {
const potensiState = useProxy(potensiDesaState.potensiDesa) const potensiState = useProxy(potensiDesaState.potensiDesa);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: "",
deskripsi: '', deskripsi: "",
kategoriId: '', kategoriId: "",
content: '', content: "",
imageId: '' imageId: "",
}); });
useEffect(() => { useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load() potensiDesaState.kategoriPotensi.findMany.load();
const loadPotensi = async () => { const loadPotensi = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await potensiState.edit.load(id); // ambil data dari API const data = await potensiState.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
kategoriId: data.kategoriId || '', kategoriId: data.kategoriId || "",
content: data.content || '', content: data.content || "",
imageId: data.imageId || '', imageId: data.imageId || "",
}); });
if (data?.image?.link) { if (data?.image?.link) {
@@ -62,13 +72,9 @@ function EditPotensi() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Sinkronkan semua data dari formData ke state global
potensiState.edit.form = { potensiState.edit.form = {
...potensiState.edit.form, ...potensiState.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId,
content: formData.content,
}; };
if (file) { if (file) {
@@ -92,44 +98,52 @@ function EditPotensi() {
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors["blue-button"]} size={24} />
</Box> </Button>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Potensi</Title> Edit Potensi Desa
</Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Judul Potensi"
placeholder="Masukkan judul"
value={formData.name} value={formData.name}
onChange={(e) => { onChange={(e) => setFormData({ ...formData, name: e.target.value })}
const val = e.target.value; required
setFormData((prev) => ({ ...prev, name: val }));
potensiState.edit.form.name = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
value={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => { onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
const val = e.target.value; required
setFormData((prev) => ({ ...prev, deskripsi: val }));
potensiState.edit.form.deskripsi = val;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
/> />
<Select <Select
value={formData.kategoriId} value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })} onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} label="Kategori"
placeholder='Pilih kategori' placeholder="Pilih kategori"
data={ data={
potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({ potensiDesaState.kategoriPotensi.findMany.data?.map((v) => ({
value: v.id, value: v.id,
label: v.nama label: v.nama,
})) || [] })) || []
} }
clearable clearable
@@ -137,77 +151,90 @@ function EditPotensi() {
required required
error={!formData.kategoriId ? "Pilih kategori" : undefined} error={!formData.kategoriId ? "Pilih kategori" : undefined}
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Potensi
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error("File tidak valid, gunakan format gambar")}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ "image/*": [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{
</div> maxHeight: 220,
</Group> objectFit: "contain",
</Dropzone> border: `1px solid ${colors["blue-button"]}`,
}}
{/* Tampilkan preview kalau ada */} />
{previewImage && ( </Box>
<Box mt="sm"> )}
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<EditEditor <EditEditor
value={formData.content} value={formData.content}
onChange={(htmlContent) => { onChange={(htmlContent) => setFormData({ ...formData, content: htmlContent })}
setFormData((prev) => ({ ...prev, content: htmlContent }));
potensiState.edit.form.content = htmlContent;
}}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Potensi</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
} }
export default EditPotensi; export default EditPotensi;

View File

@@ -1,122 +1,151 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
import React from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
export default function DetailPotensi() { export default function DetailPotensi() {
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const potensiState = useProxy(potensiDesaState.potensiDesa) const potensiState = useProxy(potensiDesaState.potensiDesa);
useShallowEffect(() => { useShallowEffect(() => {
potensiState.findUnique.load(params?.id as string) potensiState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
potensiState.delete.byId(selectedId) potensiState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/desa/potensi") router.push("/admin/desa/potensi");
} }
} };
if (!potensiState.findUnique.data) { if (!potensiState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => ( <Skeleton height={500} radius="md" />
<Skeleton key={k} h={40} />
))}
</Stack> </Stack>
) );
} }
const data = potensiState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> <Button
<Button variant="subtle" onClick={() => router.back()}> variant="subtle"
<IconArrowBack color={colors['blue-button']} size={25} /> onClick={() => router.back()}
</Button> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
</Box> mb={15}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> >
<Stack> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text> </Button>
{potensiState.findUnique.data ? (
<Paper key={potensiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> <Paper
<Stack gap={"xs"}> withBorder
<Box> w={{ base: "100%", md: "60%" }}
<Text fz={"lg"} fw={"bold"}>Judul</Text> bg={colors['white-1']}
<Text fz={"lg"}>{potensiState.findUnique.data.name}</Text> p="lg"
</Box> radius="md"
<Box> shadow="sm"
<Text fz={"lg"} fw={"bold"}>Kategori</Text> >
<Text fz={"lg"}>{potensiState.findUnique.data.kategori?.nama}</Text> <Stack gap="md">
</Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Box> Detail Potensi
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> </Text>
<Text fz={"lg"}>{potensiState.findUnique.data.deskripsi}</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Box>
<Image src={potensiState.findUnique.data.image?.link} alt="gambar" /> <Text fz="lg" fw="bold">Judul</Text>
</Box> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
<Box> </Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: potensiState.findUnique.data.content }} /> <Box>
</Box> <Text fz="lg" fw="bold">Kategori</Text>
<Box> <Text fz="md" c="dimmed">{data.kategori?.nama || '-'}</Text>
<Flex gap={"xs"}> </Box>
<Button
onClick={() => { <Box>
if (potensiState.findUnique.data) { <Text fz="lg" fw="bold">Deskripsi</Text>
setSelectedId(potensiState.findUnique.data.id) <Text fz="md" c="dimmed">{data.deskripsi || '-'}</Text>
setModalHapus(true) </Box>
}
}} <Box>
disabled={potensiState.delete.loading || !potensiState.findUnique.data} <Text fz="lg" fw="bold">Gambar</Text>
color="red" {data.image?.link ? (
> <Image
<IconX size={20} /> src={data.image.link}
</Button> alt={data.name || 'Gambar Potensi'}
<Button w={200}
onClick={() => { h={200}
if (potensiState.findUnique.data) { radius="md"
router.push(`/admin/desa/potensi/list-potensi/${potensiState.findUnique.data.id}/edit`) fit="cover"
} />
}} ) : (
disabled={!potensiState.findUnique.data} <Text fz="sm" c="dimmed">Tidak ada gambar</Text>
color="green" )}
> </Box>
<IconEdit size={20} />
</Button> <Box>
</Flex> <Text fz="lg" fw="bold">Konten</Text>
</Box> <Text
</Stack> fz="md"
</Paper> c="dimmed"
) : null} dangerouslySetInnerHTML={{ __html: data.content || '-' }}
/>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Potensi" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Potensi" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/desa/potensi/list-potensi/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?" text="Apakah Anda yakin ingin menghapus potensi ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -4,7 +4,19 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -12,8 +24,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePotensi() { function CreatePotensi() {
const potensiState = useProxy(potensiDesaState.potensiDesa); const potensiState = useProxy(potensiDesaState.potensiDesa);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
@@ -21,8 +31,8 @@ function CreatePotensi() {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load() potensiDesaState.kategoriPotensi.findMany.load();
}, []) }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
@@ -59,34 +69,50 @@ function CreatePotensi() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Create Potensi</Title> Tambah Potensi Desa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul */}
<TextInput <TextInput
value={potensiState.create.form.name} value={potensiState.create.form.name}
onChange={(val) => (potensiState.create.form.name = val.target.value)} onChange={(val) => (potensiState.create.form.name = val.target.value)}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="Masukkan judul potensi"
required
/> />
{/* Deskripsi */}
<TextInput <TextInput
value={potensiState.create.form.deskripsi} value={potensiState.create.form.deskripsi}
onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)} onChange={(val) => (potensiState.create.form.deskripsi = val.target.value)}
label={<Text fz="sm" fw="bold">Deskripsi</Text>} label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi singkat"
required
/> />
{/* Kategori */}
<Select <Select
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>} label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder='Pilih kategori' placeholder="Pilih kategori"
value={potensiState.create.form.kategoriId || ""} value={potensiState.create.form.kategoriId || ""}
onChange={(val) => { onChange={(val) => {
potensiState.create.form.kategoriId = val ?? ""; potensiState.create.form.kategoriId = val ?? "";
@@ -97,65 +123,58 @@ function CreatePotensi() {
}))} }))}
/> />
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Potensi
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ textAlign: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> />
</Group> </Box>
</Dropzone> )}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> </Box>
{/* Konten Editor */}
<Box> <Box>
<Text fz="sm" fw="bold">Konten</Text> <Text fz="sm" fw="bold" mb={6}>
Konten Lengkap
</Text>
<CreateEditor <CreateEditor
value={potensiState.create.form.content} value={potensiState.create.form.content}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -164,9 +183,21 @@ function CreatePotensi() {
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}> {/* Tombol Simpan */}
Simpan Potensi <Group justify="right">
</Button> <Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan Potensi
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,25 +1,40 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import potensiDesaState from '../../../_state/desa/potensi'; import potensiDesaState from '../../../_state/desa/potensi';
function Potensi() { function Potensi() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posisi Organisasi' title='Potensi Desa'
placeholder='pencarian' placeholder='Cari potensi atau kategori...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,8 +45,8 @@ function Potensi() {
} }
function ListPotensi({ search }: { search: string }) { function ListPotensi({ search }: { search: string }) {
const potensiState = useProxy(potensiDesaState) const potensiState = useProxy(potensiDesaState);
const router = useRouter() const router = useRouter();
const { const {
data, data,
@@ -42,117 +57,108 @@ function ListPotensi({ search }: { search: string }) {
} = potensiState.potensiDesa.findMany; } = potensiState.potensiDesa.findMany;
useEffect(() => { useEffect(() => {
potensiState.kategoriPotensi.findMany.load() potensiState.kategoriPotensi.findMany.load();
load(page, 10) load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (potensiState.potensiDesa.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.kategori?.nama.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
// Handle loading state
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={300} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Potensi'
href='/admin/desa/potensi/list-potensi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd colSpan={4}>Tidak Ada Data</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Potensi Desa</Title>
title='List Potensi' <Tooltip label="Tambah Potensi" withArrow>
href='/admin/desa/potensi/list-potensi/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: "auto" }}> color="blue"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() => router.push('/admin/desa/potensi/list-potensi/create')}
<TableTr> >
<TableTh>Judul</TableTh> Tambah Baru
<TableTh>Kategori</TableTh> </Button>
<TableTh>Deskripsi</TableTh> </Tooltip>
<TableTh>Detail</TableTh> </Group>
</TableTr> <Box style={{ overflowX: "auto" }}>
</TableThead> <Table highlightOnHover style={{ minWidth: '700px' }}>
<TableTbody> <TableThead>
{filteredData.map((item) => ( <TableTr>
<TableTh style={{ width: '20%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> {item.name}
</Box></TableTd> </Text>
<TableTd>{item.kategori?.nama}</TableTd> </TableTd>
<TableTd>
<Text fz="sm" c="dimmed">{item.kategori?.nama || '-'}</Text>
</TableTd>
<TableTd> <TableTd>
<Box w={300}> <Box w={300}>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text
truncate
fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/potensi/list-potensi/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada data potensi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
) );
} }
export default Potensi; export default Potensi;

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
function LayoutTabsDetail({ children }: { children: React.ReactNode }) { function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter()
@@ -12,21 +13,28 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
{ {
label: "Profile Desa", label: "Profile Desa",
value: "profiledesa", value: "profiledesa",
href: "/admin/desa/profile/profile-desa" href: "/admin/desa/profile/profile-desa",
icon: <IconUser size={18} stroke={1.8} />,
tooltip: "Lihat dan kelola profil desa"
}, },
{ {
label: "Profile Perbekel", label: "Profile Perbekel",
value: "profileperbekel", value: "profileperbekel",
href: "/admin/desa/profile/profile-perbekel" href: "/admin/desa/profile/profile-perbekel",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola data Perbekel"
}, },
{ {
label: "Profile Perbekel Dari Masa Ke Masa", label: "Profile Perbekel Dari Masa Ke Masa",
value: "profile-perbekel-dari-masa-ke-masa", value: "profile-perbekel-dari-masa-ke-masa",
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa" href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
icon: <IconCalendar size={18} stroke={1.8} />,
tooltip: "Riwayat Perbekel dari masa ke masa"
} }
]; ];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value) const tab = tabs.find(t => t.value === value)
@@ -44,24 +52,59 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
}, [pathname]) }, [pathname])
return ( return (
<Stack> <Stack gap="lg">
<Title order={3}>Profile Desa</Title> <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> color={colors['blue-button']}
{tabs.map((e, i) => ( variant='pills'
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}> {tabs.map((tab, i) => (
{/* Konten dummy, bisa diganti tergantung routing */} <TabsPanel
<></> key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel> </TabsPanel>
))} ))}
</Tabs> </Tabs>
{children}
</Stack> </Stack>
); );
} }
export default LayoutTabsDetail; export default LayoutTabsDetail;

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