Compare commits

...

46 Commits

Author SHA1 Message Date
75475dc62e Fix Package.json Bun 2025-09-08 21:52:17 +08:00
b39800a475 Fix UI Admin Keamanan Lingkungan 2025-09-08 15:45:56 +08:00
797713ef49 Fix UI Admin Menu Kesehatan, Login Admin, OTP 2025-09-08 14:02:21 +08:00
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
212e2db1fb Test 2025-08-13 15:24:47 +08:00
b8a45bc451 Sinkronisasi UI & API Admin - User Submenu Profile, Menu Desa 2025-08-13 14:53:48 +08:00
0777b00a7d Sinkronisasi UI & API Admin - User Menu Landing Page Submenu Indeks Kepuasan Masyarakat 2025-08-13 10:51:17 +08:00
a035039b2c Admin Fix Chart Bar PerMonth Submenu IKM, Menu PPID 2025-08-13 09:47:21 +08:00
a6832cad40 Admin: Ubah Pie Chart Submenu IKM, Menu PPID 2025-08-13 00:34:44 +08:00
a1d55e2b0a Fix Admin Menu PPID, Submenu IKM 2025-08-13 00:07:57 +08:00
c1583c21b1 Sinkronisasi UI & API Admin - User Menu Desa, Submenu Gallery 2025-08-12 11:45:39 +08:00
2fe8b8ce1a Sinkronisasi UI & API Admin - User Submenu Pengumuman 2025-08-11 14:35:47 +08:00
5cbf7810bc API & UI Admin Menu Desa, Submenu Pengumuman 2025-08-11 10:39:06 +08:00
713 changed files with 56389 additions and 27551 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,11 +3,9 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prisma:seed": "bun run prisma/seed.ts"
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
"prisma": {
"seed": "bun run prisma/seed.ts"
@@ -57,6 +55,8 @@
"form-data": "^4.0.2",
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
"jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
@@ -64,7 +64,7 @@
"lodash": "^4.17.21",
"motion": "^12.4.1",
"nanoid": "^5.1.5",
"next": "15.1.6",
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"p-limit": "^6.2.0",
@@ -73,6 +73,7 @@
"prisma": "^6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",

View File

@@ -3,126 +3,108 @@
"id": "cmds9h9ko000pvnberdjnx64b",
"name": "1.1 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA",
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PERENCANAAN, PELAKSANAAN, PENATAUSAHAAN DAN PERTANGGUNG JAWABAN APBDES BESERTA IMPLEMENTASINYA</p>",
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
"fileId": ""
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
},
{
"id": "cmds9sjmz000svnbesv2133of",
"name": "1.2 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA",
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP MENGENAI MEKANISME EVALUASI KINERJA PERANGKAT DESA</p>",
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
"fileId": ""
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
},
{
"id": "cmds9tcpi000vvnbev3ebtlnt",
"name": "1.3 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PENGENDALIAN GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN</p>",
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
"fileId": ""
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
},
{
"id": "cmds9twvj000yvnbep0pq8dzf",
"name": "1.4 PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA",
"deskripsi": "<p>PERJANJIAN KERJA SAMA ANTARA PELAKSANA KEGIATAN ANGGARAN DENGAN PIHAK PENYEDIA, DAN TELAH MELALUI PROSES PENGADAAN BARANG/JASA DI DESA</p>",
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
"fileId": ""
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
},
{
"id": "cmds9ugap0011vnbe118yv871",
"name": "1.5 ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA",
"deskripsi": "<p>ADANYA PERDES/KEPUTUSAN KEPALA DESA/SOP TENTANG PAKTA INTEGRITAS DAN SEJENISNYA</p>",
"kategoriId": "cmds9es2o000ivnbe1o0swrvh",
"fileId": ""
"kategoriId": "cmds9es2o000ivnbe1o0swrvh"
},
{
"id": "cmdsa35310014vnbe6qy6l1rz",
"name": "2.1 ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA",
"deskripsi": "<p>ADANYA KEGIATAN PENGAWASAN DAN EVALUASI KINERJA PERANGKAT DESA</p>",
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
"fileId": ""
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
},
{
"id": "cmdsa46590017vnbepp3noso1",
"name": "2.2 ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH",
"deskripsi": "<p>ADANYA TINDAK LANJUT HASIL PEMBINAAN, PETUNJUK, ARAH, PENGAWASAN, DAN PEMERIKSAAN DARI PEMERINTAH PUSAT/DAERAH</p>",
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
"fileId": ""
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
},
{
"id": "cmdsa5m7z001avnbe4cvfrcz0",
"name": "2.3 TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI",
"deskripsi": "<p>TIDAK ADANYA APARATUR DESA DALAM 3(TIGA) TAHUN TERAKHIR YANG TERJERAT TINDAKAN PIDANA KORUPSI</p>",
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm",
"fileId": ""
"kategoriId": "cmds9f2ua000jvnbeksu1sfwm"
},
{
"id": "cmdsa8q5q001dvnbemch8j24x",
"name": "3.1 ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT",
"deskripsi": "<p>ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
},
{
"id": "cmdsa9lbi001gvnbequn2ba7m",
"name": "3.2 ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA",
"deskripsi": "<p>ADANYA SURVEY KEPUASAN MASYARAKAT (SKM) TERHADAP LAYANAN PEMERINTAH DESA</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
},
{
"id": "cmdsaa7aq001jvnbeizh04e67",
"name": "3.3 ADANYA KETERBUKAAN AKSES MASYARAKAT TERHADAP INFORMASI LAYANAN PEMERINTAH DESA (KESEHATAN, PENDIDIKAN, SOSIAL, LINGKUNGAN, TRANTIBUMLINMAS, PEKERJAAN UMUM) PEMBANGUNAN, KEPENDUDUKAN, KEUANGAN, DAN PELAYANAN LAINNYA",
"deskripsi": "<p>ADANYA KETERBUKAAN AKSES MASYARAKAT TERHADAP INFORMASI LAYANAN PEMERINTAH DESA (KESEHATAN, PENDIDIKAN, SOSIAL, LINGKUNGAN, TRANTIBUMLINMAS, PEKERJAAN UMUM) PEMBANGUNAN, KEPENDUDUKAN, KEUANGAN, DAN PELAYANAN LAINNYA</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
},
{
"id": "cmdsaaw8d001mvnbek3tfefrk",
"name": "3.4 ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT",
"deskripsi": "<p>ADANYA MEDIA INFORMASI TENTANG APBDES DI BALAI DESA DAN/ATAU TEMPAT LAIN YANG MUDAH DIAKSES OLEH MASYARAKAT</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
},
{
"id": "cmdsabhif001pvnbepm06hry6",
"name": "3.5 ADANYA MAKLUMAT PELAYANAN",
"deskripsi": "<p>ADANYA MAKLUMAT PELAYANAN</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
"kategoriId": "cmds9fr73000kvnbe6w281dcl"
},
{
"id": "cmdsag40b001svnbe7krq9khc",
"name": "4.1 ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA",
"deskripsi": "<p>ADANYA PARTISIPASI DAN KETERLIBATAN MASYARAKAT DALAM PENYUSUNAN RKP DESA</p>",
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
"fileId": ""
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
},
{
"id": "cmdsagkaf001vvnbejo26w8sa",
"name": "4.2 ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN",
"deskripsi": "<p>ADANYA KESADARAN MASYARAKAT DALAM MENCEGAH TERJADINYA PRAKTIK GRATIFIKASI, SUAP DAN KONFLIK KEPENTINGAN</p>",
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
"fileId": ""
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
},
{
"id": "cmdsah4qe001yvnbeiy3mwrvb",
"name": "4.3 ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA",
"deskripsi": "<p>ADANYA KETERLIBATAN LEMBAGA KEMASYARAKATAN DALAM PELAKSANAAN PEMBANGUNAN DESA</p>",
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv",
"fileId": ""
"kategoriId": "cmds9g5ow000lvnbel3rkkwrv"
},
{
"id": "cmdsak5vn0021vnbemg86aab4",
"name": "5.1 ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
"deskripsi": "<p>ADANYA BUDAYA LOKAL/HUKUM ADAT YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI</p>",
"kategoriId": "cmds9govb000mvnbesq8b4y99",
"fileId": ""
"kategoriId": "cmds9govb000mvnbesq8b4y99"
},
{
"id": "cmdsalc800024vnbezgulhgrb",
"name": "5.2 ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI",
"deskripsi": "<p>ADANYA TOKOH MASYARAKAT, TOKOH AGAMA, TOKOH ADAT, TOKOH PEMUDA, DAN KAUM PEREMPUAN YANG MENDORONG UPAYA PENCEGAHAN TINDAK PIDANA KORUPSI</p>",
"kategoriId": "cmds9govb000mvnbesq8b4y99",
"fileId": ""
"kategoriId": "cmds9govb000mvnbesq8b4y99"
}
]

View File

@@ -1,8 +1,14 @@
[
{
"id": "1",
"jenisInformasi": "Peraturan Desa",
"deskripsi": "Dokumen yang berisi kebijakan dan regulasi desa",
"tanggal": "15 Januari 2024"
"id": "cmeppcwzk0000vn5exmudcipd",
"jenisInformasi": "Potensi 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": "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

@@ -0,0 +1,10 @@
[
{
"id": "cme8bt5o5000007lb9xp11unb",
"name": "Laki-laki"
},
{
"id": "cme8btctl000107lbh2hocgg8",
"name": "Perempuan"
}
]

View File

@@ -0,0 +1,18 @@
[
{
"id": "cme8buup6000207lb54q9b0az",
"name": "Sangat Baik"
},
{
"id": "cme8bv15o000307lbft9b0vzy",
"name": "Baik"
},
{
"id": "cme8bvjvu000507lbgfsveog6",
"name": "Kurang Baik"
},
{
"id": "cme8bvvm6000607lbh6rn2ubm",
"name": "Sangat Kurang Baik"
}
]

View File

@@ -0,0 +1,14 @@
[
{
"id": "cme8bwgwu000707lbawc6fz3a",
"name": "Muda"
},
{
"id": "cme8hnx09000b07jl3ipifb1k",
"name": "Dewasa"
},
{
"id": "cme8ho7dv000c07jlc7lr4b4w",
"name": "Lansia"
}
]

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

@@ -50,23 +50,22 @@ model AppMenuChild {
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
category String // "image" / "document" / "other"
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
category String // "image" / "document" / "other"
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
Pelapor Pelapor[]
Penghargaan Penghargaan[]
ProfileDesaImage ProfileDesaImage[]
@@ -86,7 +85,6 @@ model FileStorage {
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
KolaborasiInovasi KolaborasiInovasi[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
KegiatanDesa KegiatanDesa[]
@@ -98,10 +96,11 @@ model FileStorage {
APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[]
DataPerpustakaan DataPerpustakaan[]
PegawaiPPID PegawaiPPID[]
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
DataPerpustakaan DataPerpustakaan[]
PegawaiPPID PegawaiPPID[]
MitraKolaborasi MitraKolaborasi[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -203,8 +202,8 @@ model PrestasiDesa {
deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -221,6 +220,53 @@ model KategoriPrestasiDesa {
PrestasiDesa PrestasiDesa[]
}
//========================================= INDEKS KEPUASAAN MASYARAKAT ========================================= //
model Responden {
id String @id @default(cuid())
name String @unique
tanggal DateTime @db.Date // misal: 2025-05-01
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
jenisKelaminId String
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
ratingId String
kelompokUmur UmurResponden @relation(fields: [kelompokUmurId], references: [id])
kelompokUmurId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisKelaminResponden {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Responden Responden[]
}
model PilihanRatingResponden {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Responden Responden[]
}
model UmurResponden {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Responden Responden[]
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
@@ -247,6 +293,9 @@ model PosisiOrganisasiPPID {
pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
}
@@ -508,6 +557,19 @@ model ProfilPerbekel {
isActive Boolean @default(true)
}
model PerbekelDariMasaKeMasa {
id String @id @default(cuid())
nama String @db.Text
periode String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
daerah String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BERITA ========================================= //
model Berita {
id String @id @default(cuid())
@@ -853,26 +915,56 @@ model PendaftaranJadwalKegiatan {
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
model DataKematian_Kelahiran {
id String @id @default(cuid())
tahun String
kematianKasar String
kematianBayi String
kelahiranKasar String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
kematian Kematian @relation(fields: [kematianId], references: [id])
kematianId String
kelahiran Kelahiran @relation(fields: [kelahiranId], references: [id])
kelahiranId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Kelahiran {
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataKematian_Kelahiran DataKematian_Kelahiran[]
}
model Kematian {
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
penyebab String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataKematian_Kelahiran DataKematian_Kelahiran[]
}
// ========================================= GRAFIK KEPUASAN ========================================= //
model GrafikKepuasan {
id String @id @default(cuid())
label String
jumlah String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
nama String
tanggal DateTime
jenisKelamin String
alamat String
penyakit String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= ARTIKEL KESEHATAN ========================================= //
@@ -969,16 +1061,17 @@ model DoctorSign {
// ========================================= POSYANDU ========================================= //
model Posyandu {
id String @id @default(cuid())
name String
nomor String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
nomor String
deskripsi String
jadwalPelayanan String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PUSKESMAS ========================================= //
@@ -1458,7 +1551,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20)
year Int
year DateTime
totalUnemployment Int
educatedUnemployment Int
uneducatedUnemployment Int
@@ -1546,18 +1639,27 @@ model ProgramKreatif {
// ========================================= KOLABORASI INOVASI ========================================= //
model KolaborasiInovasi {
id String @id @default(cuid())
id String @id @default(cuid())
name String
tahun Int
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
kolaborator 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)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model MitraKolaborasi {
id String @id @default(cuid())
name String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
@@ -2001,26 +2103,66 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[]
}
// ========================================= USER ========================================= //
model User {
id String @id @default(cuid())
nama String
email String @unique
password String
role Role @relation(fields: [roleId], references: [id])
roleId String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
username String
nomor String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String @default("1")
instansi String?
UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
isActive Boolean @default(true)
lastLogin DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
}
model Role {
id String @id @default(cuid())
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
description String?
permissions Json // Menyimpan permission dalam format JSON
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
users User[]
@@map("roles")
}
model KodeOtp {
id String @id @default(cuid())
name String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
User User[]
nomor String
otp Int
}
// Tabel untuk menyimpan permission
model Permission {
id String @id @default(cuid())
name String @unique
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("permissions")
}
model UserSession {
id String @id @default(cuid())
token String
expires DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
User User @relation(fields: [userId], references: [id])
userId String @unique
}
// ========================================= DATA PENDIDIKAN ========================================= //

View File

@@ -1,54 +1,119 @@
import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json";
import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
import programInovasi from "./data/landing-page/profile/programInovasi.json";
import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
import desaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/desaantiKorpusi.json";
import kategoriDesaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json";
import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json";
import apbdes from "./data/landing-page/apbdes/apbdes.json";
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
import categoryPengumuman from "./data/category-pengumuman.json";
import kategoriBerita from "./data/kategori-berita.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 kategoriPrestasiDesa from "./data/landing-page/prestasi-desa/kategori-prestasi.json";
import prestasiDesa from "./data/landing-page/prestasi-desa/prestasi-desa.json";
import penghargaan from "./data/landing-page/penghargaan/penghargaan.json";
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json"
import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json";
import categoryPengumuman from "./data/category-pengumuman.json";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json";
import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
import 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 maskotDesa from "./data/desa/profile/maskot_desa.json";
import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import 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 detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import kategoriBerita from "./data/kategori-berita.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 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 programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json";
import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json";
import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json";
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json";
import users from "./data/user/users.json";
(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 ===========
// =========== PROFILE ===========
// =========== SUBMENU PROFILE ===========
// =========== PROFILE PEJABAT DESA ===========
for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({
where: { id: p.id },
@@ -103,6 +168,90 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
}
console.log("media sosial success ...");
// =========== SUBMENU DESA ANTI KORUPSI ===========
// =========== KATEGORI DESA ANTI KORUPSI ===========
for (const k of kategoriDesaAntiKorupsi) {
await prisma.kategoriDesaAntiKorupsi.upsert({
where: { id: k.id },
update: {
name: k.name,
},
create: {
id: k.id,
name: k.name,
},
});
}
console.log("kategori desa anti korupsi success ...");
// =========== DESA ANTI KORUPSI ===========
for (const p of desaAntiKorupsi) {
await prisma.desaAntiKorupsi.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
});
}
console.log("desa anti korupsi success ...");
// =========== KATEGORI DESA ANTI KORUPSI ===========
for (const p of kategoriDesaAntiKorupsi) {
await prisma.kategoriDesaAntiKorupsi.upsert({
where: { id: p.id },
update: {
name: p.name,
},
create: {
id: p.id,
name: p.name,
},
});
}
console.log("desa anti korupsi success ...");
// =========== KATEGORI PRESTASI DESA===========
for (const c of kategoriPrestasiDesa) {
await prisma.kategoriPrestasiDesa.upsert({
where: { id: c.id },
update: {
name: c.name,
},
create: {
id: c.id,
name: c.name,
},
});
}
console.log("kategori prestasi desa success ...");
// =========== PRESTASI DESA===========
for (const p of prestasiDesa) {
await prisma.prestasiDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
});
}
console.log("prestasi desa success ...");
// =========== PENGHARGAAN ===========
for (const p of penghargaan) {
await prisma.penghargaan.upsert({
@@ -122,8 +271,8 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
}
console.log("penghargaan success ...");
// =========== LAYANAN DESA ===========
for (const p of pelayananSuratKeterangan) {
// =========== LAYANAN DESA ===========
for (const p of pelayananSuratKeterangan) {
await prisma.pelayananSuratKeterangan.upsert({
where: { id: p.id },
update: {
@@ -157,23 +306,6 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
}
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 ===========
for (const l of sdgsDesa) {
await prisma.sDGSDesa.upsert({
@@ -214,6 +346,9 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("sdgs desa success ...");
// =========== MENU DESA ===========
// =========== SUBMENU PROFILE ===========
// =========== SEJARAH DESA ===========
for (const l of sejarahDesa) {
await prisma.sejarahDesa.upsert({
where: {
@@ -233,6 +368,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("sejarah desa success ...");
// =========== MASKOT DESA ===========
for (const l of maskotDesa) {
await prisma.maskotDesa.upsert({
where: {
@@ -252,6 +388,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("maskot desa success ...");
// =========== LAMBANG DESA ===========
for (const l of lambangDesa) {
await prisma.lambangDesa.upsert({
where: {
@@ -271,6 +408,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("lambang desa success ...");
// =========== PROFIL PERBEKEL ===========
for (const c of profilPerbekel) {
await prisma.profilPerbekel.upsert({
where: { id: c.id },
@@ -295,6 +433,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
"✅ profilePerbekel seeded without imageId (editable later via UI)"
);
// =========== VISI MISI DESA ===========
for (const l of visiMisiDesa) {
await prisma.visiMisiDesa.upsert({
where: {
@@ -314,63 +453,134 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("visi misi desa success ...");
// Flatten the nested array structure for posisiOrganisasiPPID
const flattenedPosisiOrganisasiPPID = posisiOrganisasiPPID.flat();
// =========== MENU PPID ===========
// =========== SUBMENU PROFILE PPID ===========
for (const c of profilePPID) {
await prisma.profilePPID.upsert({
where: { id: c.id },
update: {
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-update
},
create: {
id: c.id,
name: c.name,
biodata: c.biodata,
riwayat: c.riwayat,
pengalaman: c.pengalaman,
unggulan: c.unggulan,
// imageId tidak di-create
},
});
}
console.log("✅ profilePPID seeded without imageId (editable later via UI)");
// =========== SUBMENU STRUKTUR PPID ===========
// =========== POSISI ORGANISASI PPID ===========
const flattenedPosisi = posisiOrganisasiPPID.flat();
// ✅ Urutkan berdasarkan hierarki
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
for (const p of sortedPosisi) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
if (p.parentId) {
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
if (!parentExists) {
console.warn(
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
);
continue;
}
}
for (const p of flattenedPosisiOrganisasiPPID) {
await prisma.posisiOrganisasiPPID.upsert({
where: {
id: p.id,
},
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,
},
where: { id: p.id },
update: p,
create: p,
});
}
console.log("posisi organisasi success ...");
console.log("posisi organisasi berhasil");
// Flatten the nested array structure for pegawaiPPID
const flattenedPegawaiPPID = pegawaiPPID.flat();
for (const p of flattenedPegawaiPPID) {
// =========== PEGAWAI PPID ===========
const flattenedPegawai = pegawaiPPID.flat();
for (const p of flattenedPegawai) {
await prisma.pegawaiPPID.upsert({
where: { id: p.id },
update: p,
create: p,
});
}
console.log("pegawai berhasil");
// =========== SUBMENU VISI MISI PPID ===========
for (const v of visiMisiPPID) {
await prisma.visiMisiPPID.upsert({
where: {
id: p.id,
id: v.id,
},
update: {
namaLengkap: p.namaLengkap,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
misi: v.misi,
visi: v.visi,
},
create: {
id: p.id,
namaLengkap: p.namaLengkap,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
id: v.id,
misi: v.misi,
visi: v.visi,
},
});
}
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) {
await prisma.pelayananPerizinanBerusaha.upsert({
@@ -504,65 +714,53 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
}
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({
for (const j of jenisKelamin) {
await prisma.jenisKelaminResponden.upsert({
where: {
id: v.id,
id: j.id,
},
update: {
misi: v.misi,
visi: v.visi,
name: j.name,
},
create: {
id: v.id,
misi: v.misi,
visi: v.visi,
id: j.id,
name: j.name,
},
});
}
console.log("visi misi PPID success ...");
console.log("jenis kelamin responden success ...");
for (const v of dasarHukumPPID) {
await prisma.dasarHukumPPID.upsert({
for (const r of pilihanRatingResponden) {
await prisma.pilihanRatingResponden.upsert({
where: {
id: v.id,
id: r.id,
},
update: {
judul: v.judul,
content: v.content,
name: r.name,
},
create: {
id: v.id,
judul: v.judul,
content: v.content,
id: r.id,
name: r.name,
},
});
}
console.log("dasar hukum PPID success ...");
console.log("pilihan rating responden success ...");
for (const u of umurResponden) {
await prisma.umurResponden.upsert({
where: {
id: u.id,
},
update: {
name: u.name,
},
create: {
id: u.id,
name: u.name,
},
});
}
console.log("umur responden success ...");
for (const k of kategoriProduk) {
await prisma.kategoriProduk.upsert({
@@ -651,9 +849,12 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
console.log("hubungan organisasi success ...");
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({
where: {
month_year: { month: d.month, year: d.year },
month_year: { month: d.month, year: yearAsDate },
},
update: {
totalUnemployment: d.totalUnemployment,
@@ -663,7 +864,7 @@ import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSak
},
create: {
month: d.month,
year: d.year,
year: yearAsDate,
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
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,
loading: false,
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.page = page;
berita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -368,11 +368,37 @@ const kategoriBerita = proxy({
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
async load() {
const res = await ApiFetch.api.desa.kategoriberita["findMany"].get();
if (res.status === 200) {
kategoriBerita.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
kategoriBerita.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBerita.findMany.page = page;
kategoriBerita.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.kategoriberita[
"findMany"
].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBerita.findMany.data = res.data.data ?? [];
kategoriBerita.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
kategoriBerita.findMany.data = [];
kategoriBerita.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori berita paginated:", err);
kategoriBerita.findMany.data = [];
kategoriBerita.findMany.totalPages = 1;
} finally {
kategoriBerita.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -68,10 +69,34 @@ const foto = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.gallery.foto["find-many"].get();
if (res.status === 200) {
foto.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
foto.findMany.loading = true; // ✅ Akses langsung via nama path
foto.findMany.page = page;
foto.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.gallery.foto["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
foto.findMany.data = res.data.data ?? [];
foto.findMany.totalPages = res.data.totalPages ?? 1;
} else {
foto.findMany.data = [];
foto.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch foto paginated:", err);
foto.findMany.data = [];
foto.findMany.totalPages = 1;
} finally {
foto.findMany.loading = false;
}
},
},
@@ -215,6 +240,28 @@ const foto = proxy({
foto.update.form = { ...defaultFormFoto };
},
},
findRecent: {
data: [] as Prisma.GalleryFotoGetPayload<{
include: {
imageGalleryFoto: true;
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.desa.gallery.foto["find-recent"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? [];
}
} catch (error) {
console.error("Gagal fetch foto recent:", error);
} finally {
this.loading = false;
}
},
},
});
const video = proxy({
@@ -257,10 +304,34 @@ const video = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.gallery.video["find-many"].get();
if (res.status === 200) {
video.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
video.findMany.loading = true; // ✅ Akses langsung via nama path
video.findMany.page = page;
video.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.gallery.video["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
video.findMany.data = res.data.data ?? [];
video.findMany.totalPages = res.data.totalPages ?? 1;
} else {
video.findMany.data = [];
video.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch video paginated:", err);
video.findMany.data = [];
video.findMany.totalPages = 1;
} finally {
video.findMany.loading = false;
}
},
},

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -45,16 +46,48 @@ const category = proxy({
},
},
findMany: {
data: [] as Prisma.CategoryPengumumanGetPayload<{
data: [] as (Prisma.CategoryPengumumanGetPayload<{
omit: {
isActive: true;
};
}>[],
}> & {
_count: {
pengumumans: number;
};
})[],
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.desa.kategoripengumuman["findMany"].get();
if (res.status === 200) {
category.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
category.findMany.loading = true; // Use the full path to access the property
category.findMany.page = page;
category.findMany.search = search;
try {
const res = await ApiFetch.api.desa.kategoripengumuman[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
category.findMany.data = res.data.data || [];
category.findMany.total = res.data.total || 0;
category.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load potensi desa:", res.data?.message);
category.findMany.data = [];
category.findMany.total = 0;
category.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading potensi desa:", error);
category.findMany.data = [];
category.findMany.total = 0;
category.findMany.totalPages = 1;
} finally {
category.findMany.loading = false;
}
},
},
@@ -110,7 +143,9 @@ const category = proxy({
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Kategori Pengumuman");
toast.error(
"Terjadi kesalahan saat menghapus Data Kategori Pengumuman"
);
} finally {
category.delete.loading = false;
}
@@ -150,7 +185,7 @@ const category = proxy({
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori berita:", error);
console.error("Error loading kategori pengumuman:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
@@ -170,15 +205,18 @@ const category = proxy({
try {
category.update.loading = true;
const response = await fetch(`/api/desa/kategoripengumuman/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
});
const response = await fetch(
`/api/desa/kategoripengumuman/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -224,17 +262,15 @@ const templateFormPengumuman = z.object({
categoryPengumumanId: z.string().nonempty(),
});
type PengumumanForm = Prisma.PengumumanGetPayload<{
select: {
judul: true;
deskripsi: true;
content: true;
categoryPengumumanId: true;
};
}>;
const defaultForm = {
judul: "",
deskripsi: "",
content: "",
categoryPengumumanId: "",
};
const pengumuman = proxy({
create: {
form: {} as PengumumanForm,
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateFormPengumuman.safeParse(pengumuman.create.form);
@@ -270,11 +306,35 @@ const pengumuman = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.pengumuman["find-many"].get();
console.log(res);
if (res.status === 200) {
pengumuman.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
pengumuman.findMany.loading = true; // ✅ Akses langsung via nama path
pengumuman.findMany.page = page;
pengumuman.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.desa.pengumuman["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
pengumuman.findMany.data = res.data.data ?? [];
pengumuman.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pengumuman.findMany.data = [];
pengumuman.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch pengumuman paginated:", err);
pengumuman.findMany.data = [];
pengumuman.findMany.totalPages = 1;
} finally {
pengumuman.findMany.loading = false;
}
},
},
@@ -308,7 +368,7 @@ const pengumuman = proxy({
try {
pengumuman.delete.loading = true;
const response = await fetch(`/api/desa/pengumuman/delete/${id}`, {
const response = await fetch(`/api/desa/pengumuman/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@@ -331,9 +391,9 @@ const pengumuman = proxy({
}
},
},
update: {
edit: {
id: "",
form: {} as PengumumanForm,
form: { ...defaultForm },
loading: false,
async load(id: string) {
@@ -349,6 +409,7 @@ const pengumuman = proxy({
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -364,20 +425,21 @@ const pengumuman = proxy({
content: data.content,
categoryPengumumanId: data.categoryPengumumanId || "",
};
return data;
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal mengambil data pengumuman");
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data pengumuman");
} finally {
pengumuman.update.loading = false;
console.error("Error loading pengumuman:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateFormPengumuman.safeParse(pengumuman.update.form);
const cek = templateFormPengumuman.safeParse(pengumuman.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -387,7 +449,7 @@ const pengumuman = proxy({
}
try {
pengumuman.update.loading = true;
pengumuman.edit.loading = true;
const response = await fetch(`/api/desa/pengumuman/${this.id}`, {
method: "PUT",
@@ -398,7 +460,7 @@ const pengumuman = proxy({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
content: this.form.content,
categoryPengumumanId: this.form.categoryPengumumanId,
categoryPengumumanId: this.form.categoryPengumumanId || null,
}),
});
@@ -427,9 +489,14 @@ const pengumuman = proxy({
);
return false;
} finally {
pengumuman.update.loading = false;
pengumuman.edit.loading = false;
}
},
reset() {
pengumuman.edit.id = "";
pengumuman.edit.form = { ...defaultForm };
},
},
findFirst: {
data: null as Prisma.PengumumanGetPayload<{

View File

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

View File

@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import ApiFetch from "@/lib/api-fetch";
// ========================================= SEJARAH DESA ========================================= //
const sejarahDesaForm = z.object({
@@ -106,16 +108,13 @@ const sejarahDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/sejarah/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const response = await fetch(`/api/desa/profile/sejarah/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -409,16 +408,13 @@ const lambangDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/lambang/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const response = await fetch(`/api/desa/profile/lambang/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -588,14 +584,11 @@ const maskotDesa = proxy({
this.error = null;
try {
const response = await fetch(
`/api/desa/profile/maskot/${this.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form),
}
);
const response = await fetch(`/api/desa/profile/maskot/${this.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form),
});
const result = await response.json();
@@ -818,12 +811,247 @@ const profilPerbekel = proxy({
},
});
//========================================= MANTAN PERBEKEL ========================================= //
const mantanPerbekelForm = z.object({
nama: z.string().min(3, "Nama minimal 3 karakter"),
daerah: z.string().min(3, "Daerah minimal 3 karakter"),
periode: z.string().min(3, "Periode minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const mantanPerbekelDefaultForm = {
nama: "",
daerah: "",
periode: "",
imageId: "",
};
const mantanPerbekel = proxy({
create: {
form: { ...mantanPerbekelDefaultForm },
loading: false,
async create() {
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
mantanPerbekel.create.loading = true;
const res = await ApiFetch.api.desa.mantanperbekel["create"].post(
mantanPerbekel.create.form
);
if (res.status === 200) {
mantanPerbekel.findMany.load();
return toast.success("Foto berhasil disimpan!");
}
return toast.error("Gagal menyimpan foto");
} catch (error) {
console.log((error as Error).message);
} finally {
mantanPerbekel.create.loading = false;
}
},
resetForm() {
mantanPerbekel.create.form = { ...mantanPerbekelDefaultForm };
},
},
findMany: {
data: null as
| Prisma.PerbekelDariMasaKeMasaGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
mantanPerbekel.findMany.loading = true; // ✅ Akses langsung via nama path
mantanPerbekel.findMany.page = page;
mantanPerbekel.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.mantanperbekel["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
mantanPerbekel.findMany.data = res.data.data ?? [];
mantanPerbekel.findMany.totalPages = res.data.totalPages ?? 1;
} else {
mantanPerbekel.findMany.data = [];
mantanPerbekel.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch mantan perbekel paginated:", err);
mantanPerbekel.findMany.data = [];
mantanPerbekel.findMany.totalPages = 1;
} finally {
mantanPerbekel.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PerbekelDariMasaKeMasaGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/mantanperbekel/${id}`);
if (res.ok) {
const data = await res.json();
mantanPerbekel.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch mantan perbekel:", res.statusText);
mantanPerbekel.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching mantan perbekel:", error);
mantanPerbekel.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
mantanPerbekel.delete.loading = true;
const response = await fetch(`/api/desa/mantanperbekel/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Mantan perbekel berhasil dihapus");
await mantanPerbekel.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus mantan perbekel");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus mantan perbekel");
} finally {
mantanPerbekel.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...mantanPerbekelDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/desa/mantanperbekel/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
daerah: data.daerah,
periode: data.periode,
imageId: data.imageId || "",
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading foto:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
mantanPerbekel.update.loading = true;
const response = await fetch(`/api/desa/mantanperbekel/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
daerah: this.form.daerah,
periode: this.form.periode,
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Mantan perbekel berhasil diupdate");
await mantanPerbekel.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate mantan perbekel");
}
} catch (error) {
console.error("Error updating mantan perbekel:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate mantan perbekel"
);
return false;
} finally {
mantanPerbekel.update.loading = false;
}
},
reset() {
mantanPerbekel.update.id = "";
mantanPerbekel.update.form = { ...mantanPerbekelDefaultForm };
},
},
});
const stateProfileDesa = proxy({
lambangDesa,
maskotDesa,
profilPerbekel,
visiMisiDesa,
sejarahDesa,
mantanPerbekel,
});
export default stateProfileDesa;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -61,13 +62,39 @@ const lowonganKerjaState = proxy({
findMany: {
data: null as
| Prisma.LowonganPekerjaanGetPayload<{
omit: { isActive: true };
omit: {
isActive: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get();
if (res.status === 200) {
lowonganKerjaState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
lowonganKerjaState.findMany.loading = true; // ✅ Akses langsung via nama path
lowonganKerjaState.findMany.page = page;
lowonganKerjaState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
lowonganKerjaState.findMany.data = res.data.data ?? [];
lowonganKerjaState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
lowonganKerjaState.findMany.data = [];
lowonganKerjaState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch lowongan kerja paginated:", err);
lowonganKerjaState.findMany.data = [];
lowonganKerjaState.findMany.totalPages = 1;
} finally {
lowonganKerjaState.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -53,22 +54,47 @@ const pasarDesa = proxy({
},
},
findMany: {
data: null as Array<
Prisma.PasarDesaGetPayload<{
include: {
image: true;
KategoriToPasar: {
include: {
kategori: true;
data: null as
| Prisma.PasarDesaGetPayload<{
include: {
image: true;
KategoriToPasar: {
include: {
kategori: true;
};
};
};
};
}>
> | null,
async load() {
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get();
if (res.status === 200) {
pasarDesa.findMany.data = res.data?.data ?? [];
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", categoryId?: string) => {
pasarDesa.findMany.loading = true;
pasarDesa.findMany.page = page;
pasarDesa.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (categoryId) query.categoryId = categoryId;
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
pasarDesa.findMany.data = res.data.data ?? [];
pasarDesa.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pasarDesa.findMany.data = [];
pasarDesa.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch keamanan lingkungan paginated:", err);
pasarDesa.findMany.data = [];
pasarDesa.findMany.totalPages = 1;
} finally {
pasarDesa.findMany.loading = false;
}
},
},
@@ -272,14 +298,41 @@ const kategoriProduk = proxy({
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}> | null,
async load() {
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
if (res.status === 200) {
kategoriProduk.findMany.data = res.data?.data ?? [];
data: null as
| Prisma.KategoriProdukGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search2: "",
load: async (page = 1, limit = 10, search2 = "") => {
kategoriProduk.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriProduk.findMany.page = page;
kategoriProduk.findMany.search2 = search2;
try {
const query: any = { page, limit };
if (search2) query.search2 = search2;
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriProduk.findMany.data = res.data.data ?? [];
kategoriProduk.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriProduk.findMany.data = [];
kategoriProduk.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori produk paginated:", err);
kategoriProduk.findMany.data = [];
kategoriProduk.findMany.totalPages = 1;
} finally {
kategoriProduk.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -11,8 +12,7 @@ const templateForm = z.object({
statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
})
}),
});
const defaultForm = {
@@ -21,8 +21,8 @@ const defaultForm = {
ikonUrl: "",
statistik: {
tahun: "",
jumlah: ""
}
jumlah: "",
},
};
const programKemiskinanState = proxy({
@@ -64,12 +64,35 @@ const programKemiskinanState = proxy({
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get();
if (res.status === 200) {
programKemiskinanState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
search: "",
load: async (page = 1, limit = 10, search = "") => {
programKemiskinanState.findMany.loading = true; // ✅ Akses langsung via nama path
programKemiskinanState.findMany.page = page;
programKemiskinanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKemiskinanState.findMany.data = res.data.data ?? [];
programKemiskinanState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
programKemiskinanState.findMany.data = [];
programKemiskinanState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch program kemiskinan paginated:", err);
programKemiskinanState.findMany.data = [];
programKemiskinanState.findMany.totalPages = 1;
} finally {
programKemiskinanState.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -55,10 +56,34 @@ const desaDigitalState = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get();
if (res.status === 200) {
desaDigitalState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
desaDigitalState.findMany.loading = true; // ✅ Akses langsung via nama path
desaDigitalState.findMany.page = page;
desaDigitalState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
desaDigitalState.findMany.data = res.data.data ?? [];
desaDigitalState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
desaDigitalState.findMany.data = [];
desaDigitalState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch desa digital paginated:", err);
desaDigitalState.findMany.data = [];
desaDigitalState.findMany.totalPages = 1;
} finally {
desaDigitalState.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -55,10 +56,34 @@ const infoTeknoState = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.infotekno["find-many"].get();
if (res.status === 200) {
infoTeknoState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
infoTeknoState.findMany.loading = true; // ✅ Akses langsung via nama path
infoTeknoState.findMany.page = page;
infoTeknoState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.inovasi.infotekno["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
infoTeknoState.findMany.data = res.data.data ?? [];
infoTeknoState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
infoTeknoState.findMany.data = [];
infoTeknoState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch info teknologi paginated:", err);
infoTeknoState.findMany.data = [];
infoTeknoState.findMany.totalPages = 1;
} finally {
infoTeknoState.findMany.loading = false;
}
},
},

View File

@@ -6,12 +6,11 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
kolaborator: z.string().min(1, "Kolaborator minimal 1 karakter"),
imageId: z.string().min(1, "Image ID minimal 1 karakter"),
name: z.string().min(1, "Nama kolaborasi inovasi harus diisi"),
tahun: z.number().min(1900, "Tahun tidak valid").max(new Date().getFullYear() + 1, "Tahun tidak boleh lebih dari tahun depan"),
slug: z.string().min(1, "Slug harus dihasilkan otomatis"),
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
kolaborator: z.string().min(1, "Kolaborator harus diisi"),
})
const defaultForm = {
@@ -20,7 +19,6 @@ const defaultForm = {
slug: "",
deskripsi: "",
kolaborator: "",
imageId: "",
}
const kolaborasiInovasiState = proxy({
@@ -28,27 +26,37 @@ const kolaborasiInovasiState = proxy({
form: { ...defaultForm },
loading: false,
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 {
// Validate form
const validation = templateForm.safeParse(kolaborasiInovasiState.create.form);
if (!validation.success) {
const errorMessages = validation.error.issues
.map(issue => `- ${issue.path.join('.')}: ${issue.message}`)
.join('\n');
return toast.error(`Validasi gagal:\n${errorMessages}`);
}
kolaborasiInovasiState.create.loading = true;
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
kolaborasiInovasiState.create.form
);
if (res.status === 200) {
kolaborasiInovasiState.findMany.load();
return toast.success("success create");
await kolaborasiInovasiState.findMany.load();
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) {
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 {
kolaborasiInovasiState.create.loading = false;
}
@@ -60,13 +68,21 @@ const kolaborasiInovasiState = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
kolaborasiInovasiState.findMany.loading = true; // Use the full path to access the property
search: "",
year: "",
load: async (page = 1, limit = 10, search = "", year?: string) => {
kolaborasiInovasiState.findMany.loading = true;
kolaborasiInovasiState.findMany.page = page;
kolaborasiInovasiState.findMany.search = search;
kolaborasiInovasiState.findMany.year = year || "";
try {
const query: any = { page, limit };
if (search) query.search = search;
if (year) query.year = year;
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
query: { page, limit },
query,
});
if (res.status === 200 && res.data?.success) {
@@ -124,7 +140,6 @@ const kolaborasiInovasiState = proxy({
slug: data.slug,
deskripsi: data.deskripsi,
kolaborator: data.kolaborator,
imageId: data.imageId,
};
return data;
} else {
@@ -179,7 +194,7 @@ const kolaborasiInovasiState = proxy({
},
findUnique: {
data: null as Prisma.KolaborasiInovasiGetPayload<{
include: { image: true };
omit: { isActive: true };
}> | null,
async load(id: string) {
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -53,15 +54,39 @@ const keamananLingkunganState = proxy({
findMany: {
data: null as
| Prisma.KeamananLingkunganGetPayload<{
include: { image: true };
include: {
image: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.keamananlingkungan[
"find-many"
].get();
if (res.status === 200) {
keamananLingkunganState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
keamananLingkunganState.findMany.loading = true; // ✅ Akses langsung via nama path
keamananLingkunganState.findMany.page = page;
keamananLingkunganState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.keamananlingkungan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
keamananLingkunganState.findMany.data = res.data.data ?? [];
keamananLingkunganState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
keamananLingkunganState.findMany.data = [];
keamananLingkunganState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch keamanan lingkungan paginated:", err);
keamananLingkunganState.findMany.data = [];
keamananLingkunganState.findMany.totalPages = 1;
} finally {
keamananLingkunganState.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -63,13 +64,41 @@ const polsekTerdekatState = proxy({
findMany: {
data: null as
| Prisma.PolsekTerdekatGetPayload<{
include: { layananPolsek: true };
include: {
layananPolsek: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get();
if (res.status === 200) {
polsekTerdekatState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
polsekTerdekatState.findMany.loading = true; // ✅ Akses langsung via nama path
polsekTerdekatState.findMany.page = page;
polsekTerdekatState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get(
{ query }
);
if (res.status === 200 && res.data?.success) {
polsekTerdekatState.findMany.data = res.data.data ?? [];
polsekTerdekatState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
polsekTerdekatState.findMany.data = [];
polsekTerdekatState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch polsek terdekat paginated:", err);
polsekTerdekatState.findMany.data = [];
polsekTerdekatState.findMany.totalPages = 1;
} finally {
polsekTerdekatState.findMany.loading = false;
}
},
},
@@ -237,6 +266,29 @@ const polsekTerdekatState = proxy({
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;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -53,15 +54,39 @@ const tipsKeamananState = proxy({
findMany: {
data: null as
| Prisma.MenuTipsKeamananGetPayload<{
include: { image: true };
include: {
image: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.menutipskeamanan[
"find-many"
].get();
if (res.status === 200) {
tipsKeamananState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
tipsKeamananState.findMany.loading = true; // ✅ Akses langsung via nama path
tipsKeamananState.findMany.page = page;
tipsKeamananState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.menutipskeamanan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
tipsKeamananState.findMany.data = res.data.data ?? [];
tipsKeamananState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
tipsKeamananState.findMany.data = [];
tipsKeamananState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch menu tips keamanan paginated:", err);
tipsKeamananState.findMany.data = [];
tipsKeamananState.findMany.totalPages = 1;
} finally {
tipsKeamananState.findMany.loading = false;
}
},
},

View File

@@ -115,27 +115,38 @@ const artikelKesehatanState = proxy({
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
async load() {
search: "",
load: async (page = 1, limit = 10, search = "") => {
artikelKesehatanState.findMany.loading = true; // ✅ Akses langsung via nama path
artikelKesehatanState.findMany.page = page;
artikelKesehatanState.findMany.search = search;
try {
this.loading = true;
const res = await (ApiFetch.api.kesehatan as any)["artikel-kesehatan"][
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["artikel-kesehatan"][
"find-many"
].get();
].get({ query });
if (res.status === 200) {
this.data = res.data?.data ?? [];
if (res.status === 200 && res.data?.success) {
artikelKesehatanState.findMany.data =
res.data.data ?? [];
artikelKesehatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
toast.error("Gagal memuat data artikel kesehatan");
artikelKesehatanState.findMany.data = [];
artikelKesehatanState.findMany.totalPages = 1;
}
return res;
} catch (err) {
toast.error("Terjadi error saat load data");
console.error("LOAD ERROR:", err);
throw err;
console.error("Gagal fetch artikel kesehatan paginated:", err);
artikelKesehatanState.findMany.data = [];
artikelKesehatanState.findMany.totalPages = 1;
} finally {
this.loading = false;
artikelKesehatanState.findMany.loading = false;
}
},
},
@@ -280,12 +291,9 @@ const artikelKesehatanState = proxy({
async byId(id: string) {
try {
artikelKesehatanState.delete.loading = true;
const res = await fetch(
`/api/kesehatan/artikel-kesehatan/del/${id}`,
{
method: "DELETE",
}
);
const res = await fetch(`/api/kesehatan/artikel-kesehatan/del/${id}`, {
method: "DELETE",
});
const result = await res.json();
if (res.ok && result.success) {

View File

@@ -5,6 +5,7 @@ import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
//fasilitas kesehatan aja
// Validasi form
const templateForm = z.object({
name: z.string().min(1, "Nama harus diisi"),
@@ -61,7 +62,7 @@ const defaultForm = {
},
};
const fasilitasKesehatanState = proxy({
const fasilitasKesehatan = proxy({
create: {
form: { ...defaultForm },
loading: false,
@@ -86,7 +87,7 @@ const fasilitasKesehatanState = proxy({
if (res.status === 200) {
toast.success("Berhasil menambahkan fasilitas kesehatan");
this.resetForm();
await fasilitasKesehatanState.findMany.load();
await fasilitasKesehatan.findMany.load();
return res.data;
}
} catch (err: any) {
@@ -102,7 +103,6 @@ const fasilitasKesehatanState = proxy({
this.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.FasilitasKesehatanGetPayload<{
@@ -116,27 +116,38 @@ const fasilitasKesehatanState = proxy({
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
async load() {
search: "",
load: async (page = 1, limit = 10, search = "") => {
fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
fasilitasKesehatanState.fasilitasKesehatan.findMany.page = page;
fasilitasKesehatanState.fasilitasKesehatan.findMany.search = search;
try {
this.loading = true;
const res = await (ApiFetch.api.kesehatan as any)[
"fasilitas-kesehatan"
]["find-many"].get();
const query: any = { page, limit };
if (search) query.search = search;
if (res.status === 200) {
this.data = res.data?.data ?? [];
const res = await ApiFetch.api.kesehatan["fasilitas-kesehatan"][
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
fasilitasKesehatanState.fasilitasKesehatan.findMany.data =
res.data.data ?? [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
toast.error("Gagal memuat data fasilitas kesehatan");
fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
}
return res;
} catch (err) {
toast.error("Terjadi error saat load data");
console.error("LOAD ERROR:", err);
throw err;
console.error("Gagal fetch fasilitas kesehatan paginated:", err);
fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} finally {
this.loading = false;
fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = false;
}
},
},
@@ -156,7 +167,7 @@ const fasilitasKesehatanState = proxy({
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/${id}`);
if (res.ok) {
const data = await res.json();
fasilitasKesehatanState.findUnique.data = data.data ?? null;
fasilitasKesehatan.findUnique.data = data.data ?? null;
} else {
toast.error("Gagal load data fasilitas kesehatan");
}
@@ -176,8 +187,8 @@ const fasilitasKesehatanState = proxy({
const result = await res.json();
const data = result.data;
fasilitasKesehatanState.edit.id = data.id;
fasilitasKesehatanState.edit.form = {
fasilitasKesehatan.edit.id = data.id;
fasilitasKesehatan.edit.form = {
name: data.name,
informasiUmum: {
fasilitas: data.informasiumum.fasilitas,
@@ -205,7 +216,7 @@ const fasilitasKesehatanState = proxy({
};
},
async submit() {
const cek = templateForm.safeParse(fasilitasKesehatanState.edit.form);
const cek = templateForm.safeParse(fasilitasKesehatan.edit.form);
if (!cek.success) {
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
@@ -215,42 +226,38 @@ const fasilitasKesehatanState = proxy({
}
try {
fasilitasKesehatanState.edit.loading = true;
fasilitasKesehatan.edit.loading = true;
const payload = {
name: fasilitasKesehatanState.edit.form.name,
name: fasilitasKesehatan.edit.form.name,
informasiUmum: {
fasilitas:
fasilitasKesehatanState.edit.form.informasiUmum.fasilitas,
alamat: fasilitasKesehatanState.edit.form.informasiUmum.alamat,
fasilitas: fasilitasKesehatan.edit.form.informasiUmum.fasilitas,
alamat: fasilitasKesehatan.edit.form.informasiUmum.alamat,
jamOperasional:
fasilitasKesehatanState.edit.form.informasiUmum.jamOperasional,
fasilitasKesehatan.edit.form.informasiUmum.jamOperasional,
},
layananUnggulan: {
content: fasilitasKesehatanState.edit.form.layananUnggulan.content,
content: fasilitasKesehatan.edit.form.layananUnggulan.content,
},
dokterdanTenagaMedis: {
name: fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.name,
name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
specialist:
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.specialist,
jadwal:
fasilitasKesehatanState.edit.form.dokterdanTenagaMedis.jadwal,
fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
},
fasilitasPendukung: {
content:
fasilitasKesehatanState.edit.form.fasilitasPendukung.content,
content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
},
prosedurPendaftaran: {
content:
fasilitasKesehatanState.edit.form.prosedurPendaftaran.content,
content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
},
tarifDanLayanan: {
layanan: fasilitasKesehatanState.edit.form.tarifDanLayanan.layanan,
tarif: fasilitasKesehatanState.edit.form.tarifDanLayanan.tarif,
layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
},
};
const res = await fetch(
`/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatanState.edit.id}`,
`/api/kesehatan/fasilitas-kesehatan/${fasilitasKesehatan.edit.id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -264,7 +271,7 @@ const fasilitasKesehatanState = proxy({
}
toast.success("Berhasil update fasilitas kesehatan");
await fasilitasKesehatanState.findMany.load();
await fasilitasKesehatan.findMany.load();
return true;
} catch (err) {
toast.error(
@@ -272,37 +279,297 @@ const fasilitasKesehatanState = proxy({
);
return false;
} finally {
fasilitasKesehatanState.edit.loading = false;
fasilitasKesehatan.edit.loading = false;
}
},
resetForm() {
fasilitasKesehatanState.edit.id = "";
fasilitasKesehatanState.edit.form = { ...defaultForm };
fasilitasKesehatan.edit.id = "";
fasilitasKesehatan.edit.form = { ...defaultForm };
},
},
delete: {
loading: false,
async byId(id: string){
async byId(id: string) {
try {
fasilitasKesehatanState.delete.loading = true;
const res = await fetch(`/api/kesehatan/fasilitas-kesehatan/del/${id}`, {
method: "DELETE",
});
fasilitasKesehatan.delete.loading = true;
const res = await fetch(
`/api/kesehatan/fasilitas-kesehatan/del/${id}`,
{
method: "DELETE",
}
);
const result = await res.json();
if (res.ok && result.success) {
toast.success("Fasilitas kesehatan berhasil dihapus");
await fasilitasKesehatanState.findMany.load();
await fasilitasKesehatan.findMany.load();
} else {
toast.error(result.message || "Gagal menghapus");
}
} catch {
toast.error("Terjadi kesalahan saat menghapus");
} 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;

View File

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

View File

@@ -120,27 +120,36 @@ const jadwalkegiatanState = proxy({
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
async load() {
search: "",
load: async (page = 1, limit = 10, search = "") => {
jadwalkegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
jadwalkegiatanState.findMany.page = page;
jadwalkegiatanState.findMany.search = search;
try {
this.loading = true;
const res = await (ApiFetch.api.kesehatan as any)[
"jadwal-kegiatan"
]["find-many"].get();
const query: any = { page, limit };
if (search) query.search = search;
if (res.status === 200) {
this.data = res.data?.data ?? [];
const res = await ApiFetch.api.kesehatan["jadwal-kegiatan"][
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jadwalkegiatanState.findMany.data = res.data.data ?? [];
jadwalkegiatanState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
toast.error("Gagal memuat data jadwal kegiatan");
jadwalkegiatanState.findMany.data = [];
jadwalkegiatanState.findMany.totalPages = 1;
}
return res;
} catch (err) {
toast.error("Terjadi error saat load data");
console.error("LOAD ERROR:", err);
throw err;
console.error("Gagal fetch jadwal kegiatan paginated:", err);
jadwalkegiatanState.findMany.data = [];
jadwalkegiatanState.findMany.totalPages = 1;
} finally {
this.loading = false;
jadwalkegiatanState.findMany.loading = false;
}
},
},
@@ -227,29 +236,42 @@ const jadwalkegiatanState = proxy({
content: jadwalkegiatanState.edit.form.content,
informasiJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
tanggal:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu,
lokasi: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
lokasi:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
},
layananJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
content:
jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
},
deskripsiJadwalKegiatan: {
deskripsi: jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
deskripsi:
jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
},
syaratKetentuanJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan.content,
content:
jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan
.content,
},
dokumenJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.namaOrangtua,
nomor: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
tanggal:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
.namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
},
};
@@ -286,7 +308,7 @@ const jadwalkegiatanState = proxy({
},
delete: {
loading: false,
async byId(id: string){
async byId(id: string) {
try {
jadwalkegiatanState.delete.loading = true;
const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, {
@@ -305,7 +327,7 @@ const jadwalkegiatanState = proxy({
} finally {
jadwalkegiatanState.delete.loading = false;
}
}
},
},
});

View File

@@ -1,10 +1,13 @@
/* 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 templatePersentase = z.object({
//persentase kelahiran kematian
const templatePersentaseKelahiran = z.object({
tahun: z.string().min(4, "Tahun harus diisi"),
kematianKasar: z.string().min(1, "Kematian kasar harus diisi"),
kelahiranKasar: z.string().min(1, "Kelahiran kasar harus diisi"),
@@ -13,18 +16,14 @@ const templatePersentase = z.object({
type Persentase = Prisma.DataKematian_KelahiranGetPayload<{
select: {
tahun: true;
kematianKasar: true;
kelahiranKasar: true;
kematianBayi: true;
kematianId: true;
kelahiranId: true;
};
}>;
const defaultForm: Persentase = {
tahun: "",
kematianKasar: "",
kelahiranKasar: "",
kematianBayi: "",
kematianId: "",
kelahiranId: "",
};
const persentasekelahiran = proxy({
@@ -32,7 +31,9 @@ const persentasekelahiran = proxy({
form: defaultForm,
loading: false,
async create() {
const cek = templatePersentase.safeParse(persentasekelahiran.create.form);
const cek = templatePersentaseKelahiran.safeParse(
persentasekelahiran.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -47,7 +48,7 @@ const persentasekelahiran = proxy({
].post(persentasekelahiran.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
const id = res.data?.data;
if (id) {
toast.success("Success create");
persentasekelahiran.create.form = { ...defaultForm };
@@ -69,21 +70,51 @@ const persentasekelahiran = proxy({
findMany: {
data: null as
| Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true };
include: {
kematian: true;
kelahiran: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
"find-many"
].get();
if (res.status === 200) {
persentasekelahiran.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
persentasekelahiran.findMany.loading = true; // ✅ Akses langsung via nama path
persentasekelahiran.findMany.page = page;
persentasekelahiran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.persentasekelahiran[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
persentasekelahiran.findMany.data = res.data.data ?? [];
persentasekelahiran.findMany.totalPages = res.data.totalPages ?? 1;
} else {
persentasekelahiran.findMany.data = [];
persentasekelahiran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
persentasekelahiran.findMany.data = [];
persentasekelahiran.findMany.totalPages = 1;
} finally {
persentasekelahiran.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true };
include: {
kematian: true;
kelahiran: true;
};
}> | null,
async load(id: string) {
try {
@@ -114,13 +145,11 @@ const persentasekelahiran = proxy({
}
const formData = {
tahun: this.form.tahun,
kematianKasar: this.form.kematianKasar,
kelahiranKasar: this.form.kelahiranKasar,
kematianBayi: this.form.kematianBayi,
kematianId: this.form.kematianId,
kelahiranId: this.form.kelahiranId,
};
const cek = templatePersentase.safeParse(formData);
const cek = templatePersentaseKelahiran.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -20,17 +21,41 @@ const defaultForm = {
const infoWabahPenyakit = proxy({
findMany: {
data: [] as Prisma.InfoWabahPenyakitGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.infowabahpenyakit[
"find-many"
].get();
if (res.status === 200) {
infoWabahPenyakit.findMany.data = res.data?.data ?? [];
data: null as
| Prisma.InfoWabahPenyakitGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
infoWabahPenyakit.findMany.loading = true; // ✅ Akses langsung via nama path
infoWabahPenyakit.findMany.page = page;
infoWabahPenyakit.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.infowabahpenyakit["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
infoWabahPenyakit.findMany.data = res.data.data ?? [];
infoWabahPenyakit.findMany.totalPages = res.data.totalPages ?? 1;
} else {
infoWabahPenyakit.findMany.data = [];
infoWabahPenyakit.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch info wabah penyakit paginated:", err);
infoWabahPenyakit.findMany.data = [];
infoWabahPenyakit.findMany.totalPages = 1;
} finally {
infoWabahPenyakit.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -5,204 +6,241 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
})
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
}
const 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 };
},
},
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
});
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -17,21 +18,45 @@ const defaultForm = {
}
const penangananDarurat = proxy({
findMany: {
data: [] as Prisma.PenangananDaruratGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.penanganandarurat[
"find-many"
].get();
if (res.status === 200) {
penangananDarurat.findMany.data = res.data?.data ?? [];
}
},
findMany: {
data: null as
| Prisma.PenangananDaruratGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
penangananDarurat.findMany.loading = true; // ✅ Akses langsung via nama path
penangananDarurat.findMany.page = page;
penangananDarurat.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.penanganandarurat["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
penangananDarurat.findMany.data = res.data.data ?? [];
penangananDarurat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
penangananDarurat.findMany.data = [];
penangananDarurat.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
penangananDarurat.findMany.data = [];
penangananDarurat.findMany.totalPages = 1;
} finally {
penangananDarurat.findMany.loading = false;
}
},
},
create:{
form: {...defaultForm},
loading: false,

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -9,6 +10,7 @@ const templateForm = z.object({
nomor: z.string().min(1, { message: "Nomor is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
imageId: z.string().nonempty(),
jadwalPelayanan: z.string().min(1, { message: "Jadwal Pelayanan is required" }),
});
const defaultForm = {
@@ -16,6 +18,7 @@ const defaultForm = {
nomor: "",
deskripsi: "",
imageId: "",
jadwalPelayanan: "",
};
const posyandustate = proxy({
@@ -50,19 +53,43 @@ const posyandustate = proxy({
}
},
findMany: {
data: null as
| Prisma.PosyanduGetPayload<{
include: {
data: null as
| Prisma.PosyanduGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posyandustate.findMany.loading = true; // ✅ Akses langsung via nama path
posyandustate.findMany.page = page;
posyandustate.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
posyandustate.findMany.data = res.data.data ?? [];
posyandustate.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posyandustate.findMany.data = [];
posyandustate.findMany.totalPages = 1;
}
}>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get();
if (res.status === 200) {
posyandustate.findMany.data = res.data?.data ?? [];
}
}
} catch (err) {
console.error("Gagal fetch posyandu paginated:", err);
posyandustate.findMany.data = [];
posyandustate.findMany.totalPages = 1;
} finally {
posyandustate.findMany.loading = false;
}
},
},
findUnique: {
data: null as
@@ -148,6 +175,7 @@ const posyandustate = proxy({
nomor: data.nomor,
deskripsi: data.deskripsi,
imageId: data.imageId || "",
jadwalPelayanan: data.jadwalPelayanan || "",
};
return data;
} else {
@@ -181,6 +209,7 @@ const posyandustate = proxy({
nomor: this.form.nomor,
deskripsi: this.form.deskripsi,
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -20,17 +21,43 @@ const defaultForm = {
const programKesehatan = proxy({
findMany: {
data: [] as Prisma.ProgramKesehatanGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.programkesehatan[
"find-many"
].get();
if (res.status === 200) {
programKesehatan.findMany.data = res.data?.data ?? [];
data: null as
| Prisma.ProgramKesehatanGetPayload<{
include: {
image: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
programKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
programKesehatan.findMany.page = page;
programKesehatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.programkesehatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKesehatan.findMany.data = res.data.data ?? [];
programKesehatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
programKesehatan.findMany.data = [];
programKesehatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
programKesehatan.findMany.data = [];
programKesehatan.findMany.totalPages = 1;
} finally {
programKesehatan.findMany.loading = false;
}
},
},
@@ -97,12 +124,15 @@ const programKesehatan = proxy({
try {
programKesehatan.delete.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/kesehatan/programkesehatan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
@@ -156,57 +186,70 @@ const programKesehatan = proxy({
}
} catch (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;
}
},
async update() {
const cek = templateForm.safeParse(programKesehatan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
const cek = templateForm.safeParse(programKesehatan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKesehatan.edit.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsiSingkat: this.form.deskripsiSingkat,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Program kesehatan berhasil diupdate");
await programKesehatan.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate program kesehatan");
return false;
} finally {
programKesehatan.edit.loading = false;
try {
programKesehatan.edit.loading = true;
const response = await fetch(
`/api/kesehatan/programkesehatan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsiSingkat: this.form.deskripsiSingkat,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(
result.message || "Program kesehatan berhasil diupdate"
);
await programKesehatan.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat mengupdate program kesehatan"
);
return false;
} finally {
programKesehatan.edit.loading = false;
}
},
reset() {
programKesehatan.edit.id = "";
programKesehatan.edit.form = { ...defaultForm };
programKesehatan.edit.id = "";
programKesehatan.edit.form = { ...defaultForm };
},
},
});

View File

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

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -50,18 +51,50 @@ const apbdes = proxy({
},
},
findMany: {
data: null as Array<
Prisma.APBDesGetPayload<{
include: {
image: true;
file: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.apbdes["find-many"].get();
if (res.status === 200) {
apbdes.findMany.data = res.data?.data ?? [];
data: null as
| Prisma.APBDesGetPayload<{
include: {
image: true;
file: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
apbdes.findMany.loading = true; // Use the full path to access the property
apbdes.findMany.page = page;
apbdes.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.apbdes[
"findMany"
].get({
query
});
if (res.status === 200 && res.data?.success) {
apbdes.findMany.data = res.data.data || [];
apbdes.findMany.total = res.data.total || 0;
apbdes.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
} finally {
apbdes.findMany.loading = false;
}
},
},

View File

@@ -60,16 +60,22 @@ const desaAntikorupsi = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
desaAntikorupsi.findMany.page = page;
desaAntikorupsi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.desaantikorupsi[
"findMany"
].get({
query: { page, limit },
query,
});
if (res.status === 200 && res.data?.success) {
desaAntikorupsi.findMany.data = res.data.data || [];
desaAntikorupsi.findMany.total = res.data.total || 0;
@@ -305,20 +311,25 @@ const kategoriDesaAntiKorupsi = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
kategoriDesaAntiKorupsi.findMany.page = page;
kategoriDesaAntiKorupsi.findMany.search = search;
try {
const res = await ApiFetch.api.landingpage.kategoridak[
"findMany"
].get({
query: { page, limit },
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.kategoridak["findMany"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kategoriDesaAntiKorupsi.findMany.data = res.data.data || [];
kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0;
kategoriDesaAntiKorupsi.findMany.totalPages = res.data.totalPages || 1;
kategoriDesaAntiKorupsi.findMany.totalPages =
res.data.totalPages || 1;
} else {
console.error("Failed to load media sosial:", res.data?.message);
kategoriDesaAntiKorupsi.findMany.data = [];
@@ -363,27 +374,30 @@ const kategoriDesaAntiKorupsi = proxy({
try {
kategoriDesaAntiKorupsi.delete.loading = true;
const response = await fetch(
`/api/landingpage/kategoridak/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/landingpage/kategoridak/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori desa anti korupsi berhasil dihapus");
toast.success(
result.message || "Kategori desa anti korupsi berhasil dihapus"
);
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori desa anti korupsi");
toast.error(
result?.message || "Gagal menghapus kategori desa anti korupsi"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori desa anti korupsi");
toast.error(
"Terjadi kesalahan saat menghapus kategori desa anti korupsi"
);
} finally {
kategoriDesaAntiKorupsi.delete.loading = false;
}

View File

@@ -0,0 +1,834 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// Template form responden
const templateResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(1, "Tanggal harus diisi"),
jenisKelaminId: z.string().min(1, "Jenis kelamin harus diisi"),
ratingId: z.string().min(1, "Rating harus diisi"),
kelompokUmurId: z.string().min(1, "Kelompok umur harus diisi"),
});
const defaultFormResponden = {
name: "",
tanggal: "",
jenisKelaminId: "",
ratingId: "",
kelompokUmurId: "",
};
const responden = proxy({
create: {
form: { ...defaultFormResponden },
loading: false,
async create() {
const cek = templateResponden.safeParse(responden.create.form);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
try {
responden.create.loading = true;
const res = await ApiFetch.api.landingpage.responden["create"].post(
responden.create.form
);
if (res.status === 200) {
toast.success("Responden berhasil ditambahkan");
await responden.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah responden");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan responden");
} finally {
responden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
responden.findMany.loading = true; // Use the full path to access the property
responden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.responden["findMany"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
responden.findMany.data = res.data.data || [];
responden.findMany.total = res.data.total || 0;
responden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load responden:", res.data?.message);
responden.findMany.data = [];
responden.findMany.total = 0;
responden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading responden:", error);
responden.findMany.data = [];
responden.findMany.total = 0;
responden.findMany.totalPages = 1;
} finally {
responden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.RespondenGetPayload<{
include: {
jenisKelamin: true;
rating: true;
kelompokUmur: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/responden/${id}`);
if (res.ok) {
const data = await res.json();
responden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
responden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading responden:", error);
responden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormResponden },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
responden.update.loading = true;
const response = await fetch(`/api/landingpage/responden/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading responden:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
} finally {
responden.update.loading = false;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/landingpage/responden/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
tanggal: this.form.tanggal,
jenisKelaminId: this.form.jenisKelaminId,
ratingId: this.form.ratingId,
kelompokUmurId: this.form.kelompokUmurId,
}),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await responden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
responden.delete.loading = true;
const response = await fetch(`/api/landingpage/responden/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "responden berhasil dihapus");
await responden.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus responden");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus responden");
} finally {
responden.delete.loading = false;
}
},
},
});
// Template form jenis kelamin responden
const templateJenisKelaminResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultFormJenisKelaminResponden = {
name: "",
};
const jenisKelaminResponden = proxy({
create: {
form: { ...defaultFormJenisKelaminResponden },
loading: false,
async create() {
const cek = templateJenisKelaminResponden.safeParse(
jenisKelaminResponden.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
jenisKelaminResponden.create.loading = true;
try {
jenisKelaminResponden.create.loading = true;
const res = await ApiFetch.api.landingpage.jeniskelaminresponden[
"create"
].post(jenisKelaminResponden.create.form);
if (res.status === 200) {
toast.success("Jenis kelamin responden berhasil ditambahkan");
await jenisKelaminResponden.findMany.load();
} else {
toast.error(
res.data?.message ?? "Gagal tambah jenis kelamin responden"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error(
"Terjadi kesalahan saat menambahkan jenis kelamin responden"
);
} finally {
jenisKelaminResponden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
jenisKelaminResponden.findMany.loading = true; // Use the full path to access the property
jenisKelaminResponden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.jeniskelaminresponden[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
jenisKelaminResponden.findMany.data = res.data.data || [];
jenisKelaminResponden.findMany.total = res.data.total || 0;
jenisKelaminResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load jenis kelamin responden:",
res.data?.message
);
jenisKelaminResponden.findMany.data = [];
jenisKelaminResponden.findMany.total = 0;
jenisKelaminResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading jenis kelamin responden:", error);
jenisKelaminResponden.findMany.data = [];
jenisKelaminResponden.findMany.total = 0;
jenisKelaminResponden.findMany.totalPages = 1;
} finally {
jenisKelaminResponden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.JenisKelaminRespondenGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/jeniskelaminresponden/${id}`);
if (res.ok) {
const data = await res.json();
jenisKelaminResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenisKelaminResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading jenis kelamin responden:", error);
jenisKelaminResponden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormJenisKelaminResponden },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJenisKelaminResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/landingpage/jeniskelaminresponden/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jenisKelaminResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data jenis kelamin responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenisKelaminResponden.delete.loading = true;
const response = await fetch(
`/api/landingpage/jeniskelaminresponden/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "jenis kelamin responden berhasil dihapus"
);
await jenisKelaminResponden.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus jenis kelamin responden"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenis kelamin responden");
} finally {
jenisKelaminResponden.delete.loading = false;
}
},
},
});
// Template form pilihan rating responden
const templatePilihanRatingResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultFormPilihanRatingResponden = {
name: "",
};
const pilihanRatingResponden = proxy({
create: {
form: { ...defaultFormPilihanRatingResponden },
loading: false,
async create() {
const cek = templatePilihanRatingResponden.safeParse(
pilihanRatingResponden.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
pilihanRatingResponden.create.loading = true;
try {
pilihanRatingResponden.create.loading = true;
const res = await ApiFetch.api.landingpage.pilihanratingresponden[
"create"
].post(pilihanRatingResponden.create.form);
if (res.status === 200) {
toast.success("Jenis kelamin responden berhasil ditambahkan");
await pilihanRatingResponden.findMany.load();
} else {
toast.error(
res.data?.message ?? "Gagal tambah jenis kelamin responden"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error(
"Terjadi kesalahan saat menambahkan jenis kelamin responden"
);
} finally {
pilihanRatingResponden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
pilihanRatingResponden.findMany.loading = true; // Use the full path to access the property
pilihanRatingResponden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.pilihanratingresponden[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pilihanRatingResponden.findMany.data = res.data.data || [];
pilihanRatingResponden.findMany.total = res.data.total || 0;
pilihanRatingResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pilihan rating responden:",
res.data?.message
);
pilihanRatingResponden.findMany.data = [];
pilihanRatingResponden.findMany.total = 0;
pilihanRatingResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pilihan rating responden:", error);
pilihanRatingResponden.findMany.data = [];
pilihanRatingResponden.findMany.total = 0;
pilihanRatingResponden.findMany.totalPages = 1;
} finally {
pilihanRatingResponden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PilihanRatingRespondenGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/pilihanratingresponden/${id}`);
if (res.ok) {
const data = await res.json();
pilihanRatingResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pilihanRatingResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading pilihan rating responden:", error);
pilihanRatingResponden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormPilihanRatingResponden },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templatePilihanRatingResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/landingpage/pilihanratingresponden/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await pilihanRatingResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data pilihan rating responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pilihanRatingResponden.delete.loading = true;
const response = await fetch(
`/api/landingpage/pilihanratingresponden/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "pilihan rating responden berhasil dihapus"
);
await pilihanRatingResponden.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pilihan rating responden"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pilihan rating responden");
} finally {
pilihanRatingResponden.delete.loading = false;
}
},
},
});
// Template form kelompok umur responden
const templateKelompokUmurResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultFormKelompokUmurResponden = {
name: "",
};
const kelompokUmurResponden = proxy({
create: {
form: { ...defaultFormKelompokUmurResponden },
loading: false,
async create() {
const cek = templateKelompokUmurResponden.safeParse(
kelompokUmurResponden.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
kelompokUmurResponden.create.loading = true;
try {
kelompokUmurResponden.create.loading = true;
const res = await ApiFetch.api.landingpage.umurresponden["create"].post(
kelompokUmurResponden.create.form
);
if (res.status === 200) {
toast.success("Kelompok umur responden berhasil ditambahkan");
await kelompokUmurResponden.findMany.load();
} else {
toast.error(
res.data?.message ?? "Gagal tambah kelompok umur responden"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error(
"Terjadi kesalahan saat menambahkan kelompok umur responden"
);
} finally {
kelompokUmurResponden.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
kelompokUmurResponden.findMany.loading = true; // Use the full path to access the property
kelompokUmurResponden.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.umurresponden[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
kelompokUmurResponden.findMany.data = res.data.data || [];
kelompokUmurResponden.findMany.total = res.data.total || 0;
kelompokUmurResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load kelompok umur responden:",
res.data?.message
);
kelompokUmurResponden.findMany.data = [];
kelompokUmurResponden.findMany.total = 0;
kelompokUmurResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading kelompok umur responden:", error);
kelompokUmurResponden.findMany.data = [];
kelompokUmurResponden.findMany.total = 0;
kelompokUmurResponden.findMany.totalPages = 1;
} finally {
kelompokUmurResponden.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.UmurRespondenGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/umurresponden/${id}`);
if (res.ok) {
const data = await res.json();
kelompokUmurResponden.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kelompokUmurResponden.findUnique.data = null;
}
} catch (error) {
console.error("Error loading kelompok umur responden:", error);
kelompokUmurResponden.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultFormKelompokUmurResponden },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateKelompokUmurResponden.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/landingpage/umurresponden/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await kelompokUmurResponden.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data kelompok umur responden");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kelompokUmurResponden.delete.loading = true;
const response = await fetch(
`/api/landingpage/umurresponden/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "kelompok umur responden berhasil dihapus"
);
await kelompokUmurResponden.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus kelompok umur responden"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kelompok umur responden");
} finally {
kelompokUmurResponden.delete.loading = false;
}
},
},
});
const indeksKepuasanState = proxy({
responden,
kelompokUmurResponden,
jenisKelaminResponden,
pilihanRatingResponden
})
export default indeksKepuasanState

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -58,16 +59,43 @@ const prestasiDesa = proxy({
Prisma.PrestasiDesaGetPayload<{
include: {
image: true;
kategori: true;
kategori: {
select: {
id: true;
name: true;
};
};
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.prestasidesa[
"find-many"
].get();
if (res.status === 200) {
prestasiDesa.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
prestasiDesa.findMany.loading = true; // ✅ Akses langsung via nama path
prestasiDesa.findMany.page = page;
prestasiDesa.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
prestasiDesa.findMany.data = res.data.data ?? [];
prestasiDesa.findMany.totalPages = res.data.totalPages ?? 1;
} else {
prestasiDesa.findMany.data = [];
prestasiDesa.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch prestasi desa paginated:", err);
prestasiDesa.findMany.data = [];
prestasiDesa.findMany.totalPages = 1;
} finally {
prestasiDesa.findMany.loading = false;
}
},
},
@@ -283,12 +311,34 @@ const kategoriPrestasi = proxy({
id: string;
name: string;
}> | null,
async load() {
const res = await ApiFetch.api.landingpage.kategoriprestasi[
"find-many"
].get();
if (res.status === 200) {
kategoriPrestasi.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
kategoriPrestasi.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriPrestasi.findMany.page = page;
kategoriPrestasi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.kategoriprestasi["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriPrestasi.findMany.data = res.data.data ?? [];
kategoriPrestasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriPrestasi.findMany.data = [];
kategoriPrestasi.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori prestasi paginated:", err);
kategoriPrestasi.findMany.data = [];
kategoriPrestasi.findMany.totalPages = 1;
} finally {
kategoriPrestasi.findMany.loading = false;
}
},
},

View File

@@ -65,14 +65,19 @@ const programInovasi = proxy({
totalPages: 1,
total: 0,
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.page = page;
programInovasi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.programinovasi[
"findMany"
].get({
query: { page, limit },
query
});
if (res.status === 200 && res.data?.success) {
@@ -482,14 +487,19 @@ const mediaSosial = proxy({
totalPages: 1,
total: 0,
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.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[
"findMany"
].get({
query: { page, limit },
query,
});
if (res.status === 200 && res.data?.success) {

View File

@@ -58,14 +58,19 @@ const sdgsDesa = proxy({
totalPages: 1,
total: 0,
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.page = page;
sdgsDesa.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.sdgsdesa[
"findMany"
].get({
query: { page, limit },
query,
});
if (res.status === 200 && res.data?.success) {

View File

@@ -56,13 +56,17 @@ const dataLingkunganDesaState = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
dataLingkunganDesaState.findMany.loading = true; // Use the full path to access the property
dataLingkunganDesaState.findMany.page = page;
dataLingkunganDesaState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.lingkungan.datalingkungandesa["find-many"].get({
query: { page, limit },
query,
});
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -67,10 +68,46 @@ const kegiatanDesa = proxy({
};
}>
> | null,
async load() {
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-many"].get();
if (res.status === 200) {
kegiatanDesa.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
// Change to arrow function
kegiatanDesa.findMany.loading = true; // Use the full path to access the property
kegiatanDesa.findMany.page = page;
kegiatanDesa.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
const res = await ApiFetch.api.lingkungan.kegiatandesa[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
kegiatanDesa.findMany.data = res.data.data || [];
kegiatanDesa.findMany.total = res.data.total || 0;
kegiatanDesa.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load kegiatan desa:",
res.data?.message
);
kegiatanDesa.findMany.data = [];
kegiatanDesa.findMany.total = 0;
kegiatanDesa.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading kegiatan desa:", error);
kegiatanDesa.findMany.data = [];
kegiatanDesa.findMany.total = 0;
kegiatanDesa.findMany.totalPages = 1;
} finally {
kegiatanDesa.findMany.loading = false;
}
},
},
@@ -244,6 +281,35 @@ const kegiatanDesa = proxy({
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 ========================================= //
@@ -269,9 +335,7 @@ const kategoriKegiatan = proxy({
}
try {
kategoriKegiatan.create.loading = true;
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
"create"
].post(kategoriKegiatan.create.form);
const res = await ApiFetch.api.lingkungan.kategorikegiatan["create"].post(kategoriKegiatan.create.form);
if (res.status === 200) {
kategoriKegiatan.findMany.load();
return toast.success("Data berhasil ditambahkan");
@@ -305,9 +369,7 @@ const kategoriKegiatan = proxy({
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/lingkungan/kategorikegiatan/${id}`
);
const res = await fetch(`/api/lingkungan/kategorikegiatan/${id}`);
if (res.ok) {
const data = await res.json();
kategoriKegiatan.findUnique.data = data.data ?? null;
@@ -367,15 +429,12 @@ const kategoriKegiatan = proxy({
}
try {
const response = await fetch(
`/api/lingkungan/kategorikegiatan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/lingkungan/kategorikegiatan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

View File

@@ -52,15 +52,19 @@ const pengelolaanSampah = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
pengelolaanSampah.findMany.page = page;
pengelolaanSampah.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
"find-many"
].get({
query: { page, limit },
query,
});
if (res.status === 200 && res.data?.success) {
@@ -265,7 +269,7 @@ const keteranganSampah = proxy({
try {
keteranganSampah.create.loading = true;
const res =
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
await ApiFetch.api.lingkungan.keteranganbankterdekat[
"create"
].post(keteranganSampah.create.form);
if (res.status === 200) {
@@ -287,14 +291,47 @@ const keteranganSampah = proxy({
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
"find-many"
].get();
if (res.status === 200) {
keteranganSampah.findMany.data = res.data?.data ?? [];
}
},
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
keteranganSampah.findMany.loading = true; // Use the full path to access the property
keteranganSampah.findMany.page = page;
keteranganSampah.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
keteranganSampah.findMany.data = res.data.data || [];
keteranganSampah.findMany.total = res.data.total || 0;
keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load keterangan bank sampah terdekat:",
res.data?.message
);
keteranganSampah.findMany.data = [];
keteranganSampah.findMany.total = 0;
keteranganSampah.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading keterangan bank sampah terdekat:", error);
keteranganSampah.findMany.data = [];
keteranganSampah.findMany.total = 0;
keteranganSampah.findMany.totalPages = 1;
} finally {
keteranganSampah.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
@@ -302,7 +339,7 @@ const keteranganSampah = proxy({
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`);
const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
if (res.ok) {
const data = await res.json();
keteranganSampah.findUnique.data = data.data ?? null;
@@ -324,7 +361,7 @@ const keteranganSampah = proxy({
try {
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",
headers: {
"Content-Type": "application/json",
@@ -359,7 +396,7 @@ const keteranganSampah = proxy({
}
try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, {
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -404,7 +441,7 @@ const keteranganSampah = proxy({
try {
keteranganSampah.edit.loading = true;
const response = await fetch(
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`,
`/api/lingkungan/keteranganbankterdekat/${this.id}`,
{
method: "PUT",
headers: {

View File

@@ -56,13 +56,17 @@ const programPenghijauanState = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
programPenghijauanState.findMany.loading = true; // Use the full path to access the property
programPenghijauanState.findMany.page = page;
programPenghijauanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.lingkungan.programpenghijauan["find-many"].get({
query: { page, limit },
query,
});
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= BEASISWA PENDAFTAR ========================================= //
const templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"),
nik: z.string().min(1, "NIK harus diisi"),
@@ -76,13 +79,34 @@ const beasiswaPendaftar = proxy({
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"findMany"
].get();
if (res.status === 200) {
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
beasiswaPendaftar.findMany.page = page;
beasiswaPendaftar.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
beasiswaPendaftar.findMany.data = res.data.data ?? [];
beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
} else {
beasiswaPendaftar.findMany.data = [];
beasiswaPendaftar.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch beasiswa pendaftar paginated:", err);
beasiswaPendaftar.findMany.data = [];
beasiswaPendaftar.findMany.totalPages = 1;
} finally {
beasiswaPendaftar.findMany.loading = false;
}
},
},
@@ -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({
beasiswaPendaftar,
keunggulanProgram
});
export default beasiswaDesaState;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -51,13 +52,46 @@ const jenjangPendidikan = proxy({
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
"find-many"
].get();
if (res.status === 200) {
jenjangPendidikan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
jenjangPendidikan.findMany.loading = true; // Use the full path to access the property
jenjangPendidikan.findMany.page = page;
jenjangPendidikan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
jenjangPendidikan.findMany.data = res.data.data || [];
jenjangPendidikan.findMany.total = res.data.total || 0;
jenjangPendidikan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load jenjang pendidikan:",
res.data?.message
);
jenjangPendidikan.findMany.data = [];
jenjangPendidikan.findMany.total = 0;
jenjangPendidikan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading jenjang pendidikan:", error);
jenjangPendidikan.findMany.data = [];
jenjangPendidikan.findMany.total = 0;
jenjangPendidikan.findMany.totalPages = 1;
} finally {
jenjangPendidikan.findMany.loading = false;
}
},
},
@@ -299,18 +333,64 @@ const lembagaPendidikan = proxy({
Prisma.LembagaGetPayload<{
include: {
jenjangPendidikan: true;
siswa: true;
pengajar: true;
};
}>
}> & {
siswa?: [];
pengajar?: [];
}
> | null,
async load() {
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
"find-many"
].get();
if (res.status === 200) {
lembagaPendidikan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
lembagaPendidikan.findMany.loading = true;
lembagaPendidikan.findMany.page = page;
lembagaPendidikan.findMany.search = search;
try {
const query: any = {
page,
limit,
...(search && { search }),
...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
};
console.log('Fetching lembaga with query:', query);
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
console.log('API Response:', res);
if (res.status === 200 && res.data?.success) {
const data = Array.isArray(res.data.data) ? res.data.data : [];
const total = typeof res.data.total === 'number' ? res.data.total : 0;
const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
lembagaPendidikan.findMany.data = data;
lembagaPendidikan.findMany.total = total;
lembagaPendidikan.findMany.totalPages = totalPages;
console.log('Successfully loaded lembaga data:', {
count: data.length,
total,
totalPages
});
} else {
console.error(
"Failed to load lembaga pendidikan:",
res.data?.message || 'No error message provided'
);
throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
}
} catch (error) {
console.error("Error loading lembaga pendidikan:", error);
lembagaPendidikan.findMany.data = [];
lembagaPendidikan.findMany.total = 0;
lembagaPendidikan.findMany.totalPages = 1;
} finally {
lembagaPendidikan.findMany.loading = false;
}
},
},
@@ -554,16 +634,55 @@ const siswa = proxy({
data: null as Array<
Prisma.SiswaGetPayload<{
include: {
lembaga: true;
lembaga: {
include: {
jenjangPendidikan: true;
};
};
};
}>
> | null,
async load() {
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"find-many"
].get();
if (res.status === 200) {
siswa.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
jenjangPendidikan: "",
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
siswa.findMany.loading = true;
siswa.findMany.page = page;
siswa.findMany.search = search;
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
siswa.findMany.data = res.data.data || [];
siswa.findMany.total = res.data.total || 0;
siswa.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load siswa:",
res.data?.message
);
siswa.findMany.data = [];
siswa.findMany.total = 0;
siswa.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading siswa:", error);
siswa.findMany.data = [];
siswa.findMany.total = 0;
siswa.findMany.totalPages = 1;
} finally {
siswa.findMany.loading = false;
}
},
},
@@ -794,16 +913,56 @@ const pengajar = proxy({
data: null as Array<
Prisma.PengajarGetPayload<{
include: {
lembaga: true;
lembaga: {
include: {
jenjangPendidikan: true
}
}
};
}>
> | null,
async load() {
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"find-many"
].get();
if (res.status === 200) {
pengajar.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
jenjangPendidikan: "",
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
// Change to arrow function
pengajar.findMany.loading = true; // Use the full path to access the property
pengajar.findMany.page = page;
pengajar.findMany.search = search;
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
pengajar.findMany.data = res.data.data || [];
pengajar.findMany.total = res.data.total || 0;
pengajar.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pengajar:",
res.data?.message
);
pengajar.findMany.data = [];
pengajar.findMany.total = 0;
pengajar.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pengajar:", error);
pengajar.findMany.data = [];
pengajar.findMany.total = 0;
pengajar.findMany.totalPages = 1;
} finally {
pengajar.findMany.loading = false;
}
},
},
@@ -815,7 +974,9 @@ const pengajar = proxy({
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`);
const res = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/${id}`
);
if (res.ok) {
const data = await res.json();
pengajar.findUnique.data = data.data ?? null;
@@ -948,7 +1109,8 @@ const pengajar = proxy({
result
);
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 { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -54,23 +55,46 @@ const dataPerpustakaan = proxy({
},
},
findMany: {
data: [] as Prisma.DataPerpustakaanGetPayload<{
include: {
kategori: true;
image: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get();
if (res.status === 200) {
dataPerpustakaan.findMany.data = res.data?.data ?? [];
}
data: null as
| Prisma.DataPerpustakaanGetPayload<{
include: {
image: true;
kategori: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
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: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {
@@ -293,14 +317,34 @@ const kategoriBuku = proxy({
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
async load() {
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get();
if (res.status === 200) {
kategoriBuku.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBuku.findMany.page = page;
kategoriBuku.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriBuku.findMany.data = [];
kategoriBuku.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data kategori buku paginated:", err);
kategoriBuku.findMany.data = [];
kategoriBuku.findMany.totalPages = 1;
} finally {
kategoriBuku.findMany.loading = false;
}
},
},

View File

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

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateGrafikJenisKelamin = z.object({
laki: z.string().min(1, "Data laki-laki harus diisi"),
perempuan: z.string().min(1, "Data perempuan harus diisi"),
});
});
const defaultForm = {
laki: "",
@@ -17,10 +17,12 @@ const defaultForm = {
const grafikBerdasarkanJenisKelamin = proxy({
create: {
form: {...defaultForm},
form: { ...defaultForm },
loading: false,
async create(){
const cek = templateGrafikJenisKelamin.safeParse(grafikBerdasarkanJenisKelamin.create.form);
async create() {
const cek = templateGrafikJenisKelamin.safeParse(
grafikBerdasarkanJenisKelamin.create.form
);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
@@ -33,14 +35,20 @@ const grafikBerdasarkanJenisKelamin = proxy({
"create"
].post(grafikBerdasarkanJenisKelamin.create.form);
if (res.status === 200) {
toast.success("Grafik berdasarkan jenis kelamin berhasil ditambahkan");
toast.success(
"Grafik berdasarkan jenis kelamin berhasil ditambahkan"
);
await grafikBerdasarkanJenisKelamin.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin");
toast.error(
res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin"
);
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin");
toast.error(
"Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin"
);
} finally {
grafikBerdasarkanJenisKelamin.create.loading = false;
}
@@ -52,8 +60,9 @@ const grafikBerdasarkanJenisKelamin = proxy({
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
load: async (page = 1, limit = 10) => {
// Change to arrow function
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanJenisKelamin.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
@@ -61,13 +70,17 @@ const grafikBerdasarkanJenisKelamin = proxy({
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = res.data.totalPages || 1;
grafikBerdasarkanJenisKelamin.findMany.totalPages =
res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan jenis kelamin:", res.data?.message);
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
grafikBerdasarkanJenisKelamin.findMany.data = [];
grafikBerdasarkanJenisKelamin.findMany.total = 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
@@ -106,7 +119,7 @@ const grafikBerdasarkanJenisKelamin = proxy({
},
update: {
id: "",
form: {...defaultForm},
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
@@ -119,20 +132,24 @@ const grafikBerdasarkanJenisKelamin = proxy({
}
const cek = templateGrafikJenisKelamin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
`/api/ppid/grafikberdasarkanjeniskelamin/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
@@ -156,29 +173,40 @@ const grafikBerdasarkanJenisKelamin = proxy({
try {
grafikBerdasarkanJenisKelamin.delete.loading = true;
const response = await fetch(`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/ppid/grafikberdasarkanjeniskelamin/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik berdasarkan jenis kelamin berhasil dihapus");
toast.success(
result.message ||
"Grafik berdasarkan jenis kelamin berhasil dihapus"
);
await grafikBerdasarkanJenisKelamin.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik berdasarkan jenis kelamin");
toast.error(
result?.message ||
"Gagal menghapus grafik berdasarkan jenis kelamin"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin");
toast.error(
"Terjadi kesalahan saat menghapus grafik berdasarkan jenis kelamin"
);
} finally {
grafikBerdasarkanJenisKelamin.delete.loading = false;
}
},
}
},
});
export default grafikBerdasarkanJenisKelamin;

View File

@@ -348,18 +348,34 @@ const posisiOrganisasi = proxy({
deskripsi: string | null;
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 {
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many"
].get();
if (res.status === 200) {
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? [];
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Find many error:", error);
this.data = [];
} catch (err) {
console.error("Gagal fetch posisi organisasi paginated:", err);
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} finally {
posisiOrganisasi.findMany.loading = false;
}
},
},
@@ -438,9 +454,9 @@ const pegawai = proxy({
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"create"
].post(pegawai.create.form);
const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(
pegawai.create.form
);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
@@ -457,42 +473,55 @@ const pegawai = proxy({
},
// In struktur-organisasi.ts
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many"
].get({
query: { page, limit },
});
findMany: {
data: null as
| Prisma.PegawaiPPIDGetPayload<{
include: {
image: true;
posisi: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
pegawai.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
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);
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
pegawai.findMany.data = res.data.data || [];
pegawai.findMany.total = res.data.total || 0;
pegawai.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
@@ -521,12 +550,9 @@ findMany: {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(
`/api/ppid/strukturppid/pegawai/del/${id}`,
{
method: "DELETE",
}
);
const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, {
method: "DELETE",
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
@@ -555,15 +581,12 @@ findMany: {
}
try {
const response = await fetch(
`/api/ppid/strukturppid/pegawai/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/ppid/strukturppid/pegawai/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -677,7 +700,7 @@ findMany: {
const stateStrukturPPID = proxy({
stateStruktur,
posisiOrganisasi,
pegawai
pegawai,
});
export default stateStrukturPPID;

View File

@@ -0,0 +1,63 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
interface FileItem {
id: string;
name: string;
path: string;
link: string;
mimeType: string;
category: string;
realName: string;
isActive: boolean;
createdAt: string | Date;
updatedAt: string | Date;
deletedAt: string | Date | null;
}
const stateFileStorage = proxy<{
list: FileItem[] | null;
page: number;
limit: number;
total: number | undefined;
load: (params?: { search?: string }) => Promise<void>;
del: (params: { id: string }) => Promise<void>;
}>({
list: null,
page: 1,
limit: 10,
total: undefined,
async load(params?: { search?: string }) {
const { search = "" } = params ?? {};
try {
const { data } = await ApiFetch.api.fileStorage.findMany.get({
query: {
page: this.page,
limit: this.limit,
search,
category: 'image'
},
});
if (data?.data) {
this.list = data.data as FileItem[];
this.total = data.meta?.totalPages;
}
} catch (error) {
console.error('Error loading files:', error);
this.list = [];
this.total = 0;
}
},
async del({ id }: { id: string }) {
try {
await ApiFetch.api.fileStorage.delete({ id });
await this.load();
} catch (error) {
console.error('Error deleting file:', error);
throw error;
}
},
});
export default stateFileStorage;

View File

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

View File

@@ -0,0 +1,111 @@
'use client'
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Login() {
const router = useRouter()
const [phone, setPhone] = useState("")
const [isError, setError] = useState(false)
const [loading, setLoading] = useState(false)
async function onLogin() {
const nomor = phone.substring(1);
if (nomor.length <= 4) return setError(true)
try {
setLoading(true);
const response = await apiFetchLogin({ nomor: nomor })
if (response && response.success) {
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
toast.success(response.message);
router.push("/validasi", { scroll: false });
} else {
setLoading(false);
toast.error(response?.message);
}
} catch (error) {
setLoading(false)
console.log("Error Login", error)
toast.error("Terjadi kesalahan saat login")
}
}
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Login
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
</Box>
<Box>
{/* <Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
<TextInput
label='Username'
placeholder='Username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</Box> */}
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%"}}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
{isError ? (
toast.error("Masukan nomor telepon anda")
) : (
""
)}
<Box py={20} >
<Button
fullWidth
bg={colors['blue-button']}
radius={'xl'}
onClick={onLogin}
loading={loading ? true : false}
>Masuk
</Button>
</Box>
<Flex justify={'center'} align={'center'}>
<Text>Belum punya akun? </Text>
<Button variant='transparent' component={Link} href={'/registrasi'}>
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Login;

View File

@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
'use client'
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Registrasi() {
const [phone, setPhone] = useState("")
const router = useRouter()
const [value, setValue] = useState("")
const [isValue, setIsValue] = useState(false);
const [loading, setLoading] = useState(false);
async function onRegistarsi() {
if (value.length < 5) {
toast.error("Username minimal 5 karakter!");
return;
}
if (value.includes(" ")) {
toast.error("Username tidak boleh ada spasi!");
return;
}
if (!phone) {
toast.error("Nomor telepon wajib diisi!");
return;
}
try {
setLoading(true);
const respone = await apiFetchRegister({ nomor: phone, username: value });
if (respone.success) {
router.push("/login", { scroll: false });
toast.success(respone.message);
} else {
setLoading(false);
toast.error(respone.message);
}
} catch (error) {
setLoading(false);
console.log("Error Registrasi", error);
}
}
return (
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Stack justify='center' align='center' h={"80vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Registrasi
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<TextInput placeholder='Username'
label='Username'
maxLength={50}
error={
value.length > 0 && value.length < 5
? "Minimal 5 karakter !"
: value.includes(" ")
? "Tidak boleh ada spasi"
: isValue
? "Masukan username anda"
: ""
}
onChange={(val) => {
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
setValue(val.currentTarget.value);
}}
required
/>
<Box py={10}>
<Text fz={"sm"} >Nomor Telepon</Text>
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%" }}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
</Box>
<Box pb={10}>
<Checkbox
label="Saya menyetujui syarat dan ketentuan yang berlaku"
/>
</Box>
<Box pb={20} >
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Registrasi;

View File

@@ -0,0 +1,38 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
function Validasi() {
const router = useRouter()
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Kode Verifikasi
</Title>
</Box>
<Box>
<Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
</Box>
<Box py={20} >
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
Page
</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Validasi;

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -12,26 +13,35 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
{
label: "Pelayanan Surat Keterangan",
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",
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",
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",
value: "pelayanantelunjuknonpermanent",
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent"
value: "pelayanannonpermanent",
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 tab = tabs.find(t => t.value === value)
@@ -49,24 +59,65 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}, [pathname])
return (
<Stack>
<Title order={3}>Layanan</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
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>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
{tabs.map((tab, i) => (
<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>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsLayanan;
export default LayoutTabsLayanan;

View File

@@ -1,63 +1,110 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 React, { useEffect, useState } from 'react';
import { IconNews, IconCategory } from '@tabler/icons-react';
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "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",
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 tab = tabs.find(t => t.value === value)
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href)
router.push(tab.href);
}
setActiveTab(value)
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value)
setActiveTab(match.value);
}
}, [pathname])
}, [pathname]);
return (
<Stack>
<Title order={3}>Gallery</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
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>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
{tabs.map((tab, i) => (
<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>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsBerita;
export default LayoutTabsBerita;

View File

@@ -2,7 +2,16 @@
'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -10,67 +19,102 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita)
const editState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
});
name: editState.update.form.name || '',
});
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori Berita:", error);
toast.error("Gagal memuat data kategori Berita");
}
};
loadKategori();
}, [params?.id]);
const loadKategori = async () => {
const id = params?.id as string;
if (!id) return;
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');
const data = await editState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
console.error('Error loading kategori Berita:', error);
toast.error('Gagal memuat data kategori Berita');
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
name: formData.name,
};
await editState.update.update();
toast.success('Kategori Berita berhasil diperbarui!');
router.push('/admin/desa/berita/kategori-berita');
} catch (error) {
console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kategori Berita</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + Title */}
<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">
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
label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Berita</Text>}
placeholder="masukkan nama kategori Berita"
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>
</Paper>
</Box>

View File

@@ -1,50 +1,87 @@
'use client'
'use client';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita)
const createState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter();
const resetForm = () => {
createState.create.form = {
name: "",
name: '',
};
};
const handleSubmit = async () => {
await createState.create.create();
resetForm();
router.push("/admin/desa/berita/kategori-berita")
router.push('/admin/desa/berita/kategori-berita');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<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 Kategori Berita
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kategori Berita</Title>
{/* Form utama */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Berita</Text>}
placeholder='Masukkan nama kategori Berita'
value={createState.create.form.name}
onChange={(val) => {
createState.create.form.name = val.target.value;
}}
label={<Text fw="bold" fz="sm">Nama Kategori Berita</Text>}
placeholder="Masukkan nama kategori berita"
value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.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>
</Stack>
</Paper>

View File

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

View File

@@ -1,10 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
Box,
Button,
Center,
Group,
Image,
Paper,
Select,
@@ -12,18 +16,14 @@ import {
Text,
TextInput,
Title,
Tooltip,
} 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 { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { FileInput } from "@mantine/core";
import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
function EditBerita() {
const beritaState = useProxy(stateDashboardBerita);
@@ -33,29 +33,29 @@ function EditBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || '',
deskripsi: beritaState.berita.edit.form.deskripsi || '',
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || '',
content: beritaState.berita.edit.form.content || '',
imageId: beritaState.berita.edit.form.imageId || ''
judul: beritaState.berita.edit.form.judul || "",
deskripsi: beritaState.berita.edit.form.deskripsi || "",
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
content: beritaState.berita.edit.form.content || "",
imageId: beritaState.berita.edit.form.imageId || "",
});
// Load berita by id saat pertama kali
useEffect(() => {
beritaState.kategoriBerita.findMany.load()
beritaState.kategoriBerita.findMany.load();
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateDashboardBerita.berita.edit.load(id); // akses langsung, bukan dari proxy
const data = await stateDashboardBerita.berita.edit.load(id);
if (data) {
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
kategoriBeritaId: data.kategoriBeritaId || '',
content: data.content || '',
imageId: data.imageId || '',
judul: data.judul || "",
deskripsi: data.deskripsi || "",
kategoriBeritaId: data.kategoriBeritaId || "",
content: data.content || "",
imageId: data.imageId || "",
});
if (data?.image?.link) {
@@ -69,31 +69,26 @@ function EditBerita() {
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
beritaState.berita.edit.form = {
...beritaState.berita.edit.form,
judul: formData.judul,
deskripsi: formData.deskripsi,
content: formData.content,
kategoriBeritaId: formData.kategoriBeritaId || '',
imageId: formData.imageId // Keep existing imageId if not changed
...formData,
};
// Jika ada file baru, upload
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;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
beritaState.berita.edit.form.imageId = uploaded.id;
}
@@ -107,52 +102,111 @@ function EditBerita() {
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Berita</Title>
<Box px={{ base: "sm", md: "lg" }} py="md">
<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">
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
label="Judul"
placeholder="Masukkan judul"
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
onChange={(e) =>
setFormData({ ...formData, judul: e.target.value })
}
required
/>
<TextInput
label="Deskripsi"
placeholder="Masukkan deskripsi"
value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
onChange={(e) =>
setFormData({ ...formData, deskripsi: e.target.value })
}
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>
<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
value={formData.content}
onChange={(htmlContent) => {
@@ -164,13 +218,15 @@ function EditBerita() {
<Select
value={formData.kategoriBeritaId}
onChange={(val) => setFormData({ ...formData, kategoriBeritaId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
placeholder='Pilih kategori'
onChange={(val) =>
setFormData({ ...formData, kategoriBeritaId: val || "" })
}
label="Kategori"
placeholder="Pilih kategori"
data={
beritaState.kategoriBerita.findMany.data?.map((v) => ({
value: v.id,
label: v.name
label: v.name,
})) || []
}
clearable
@@ -179,7 +235,20 @@ function EditBerita() {
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>
</Paper>
</Box>

View File

@@ -1,9 +1,8 @@
'use client'
import { useProxy } from 'valtio/utils';
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 { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
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';
function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const beritaState = useProxy(stateDashboardBerita);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
beritaState.berita.findUnique.load(params?.id as string)
}, [])
beritaState.berita.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
beritaState.berita.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/desa/berita/list-berita")
beritaState.berita.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/desa/berita/list-berita");
}
}
};
if (!beritaState.berita.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = beritaState.berita.findUnique.data;
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 Berita</Text>
{beritaState.berita.findUnique.data ? (
<Paper key={beritaState.berita.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.kategoriBerita?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{beritaState.berita.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} >{beritaState.berita.findUnique.data?.deskripsi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={beritaState.berita.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Konten</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: beritaState.berita.findUnique.data?.content }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Detail Berita */}
<Paper
withBorder
w={{ base: "100%", md: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Berita
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Kategori</Text>
<Text fz="md" c="dimmed">{data.kategoriBerita?.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">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
color="red"
onClick={() => {
if (beritaState.berita.findUnique.data) {
setSelectedId(beritaState.berita.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={beritaState.berita.delete.loading || !beritaState.berita.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Berita" withArrow position="top">
<Button
onClick={() => {
if (beritaState.berita.findUnique.data) {
router.push(`/admin/desa/berita/list-berita/${beritaState.berita.findUnique.data.id}/edit`);
}
}}
disabled={!beritaState.berita.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus berita ini?'
text="Apakah Anda yakin ingin menghapus berita ini?"
/>
</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 colors from '@/con/colors';
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 { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
export default function CreateBerita() {
const beritaState = useProxy(stateDashboardBerita);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const router = useRouter();
useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load()
beritaState.kategoriBerita.findMany.load();
}, []);
const resetForm = () => {
// Reset state di valtio
beritaState.berita.create.form = {
judul: "",
deskripsi: "",
kategoriBeritaId: "",
imageId: "",
content: "",
judul: '',
deskripsi: '',
kategoriBeritaId: '',
imageId: '',
content: '',
};
// Reset state lokal
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
@@ -50,40 +58,55 @@ export default function CreateBerita() {
const uploaded = res.data?.data;
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;
// Submit data berita
await beritaState.berita.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/desa/berita/list-berita")
router.push('/admin/desa/berita/list-berita');
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Berita</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */}
<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 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
label="Judul"
placeholder="Masukkan judul berita"
value={beritaState.berita.create.form.judul}
onChange={(val) => {
beritaState.berita.create.form.judul = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
required
/>
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
label="Kategori"
placeholder="Pilih kategori"
data={beritaState.kategoriBerita.findMany.data.map((item) => ({
label: item.name,
@@ -92,48 +115,83 @@ export default function CreateBerita() {
value={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => {
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) {
beritaState.berita.create.form.kategoriBeritaId = selected.id;
}
} else {
beritaState.berita.create.form.kategoriBeritaId = "";
beritaState.berita.create.form.kategoriBeritaId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
<TextInput
value={beritaState.berita.create.form.deskripsi}
onChange={(val) => {
beritaState.berita.create.form.deskripsi = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>}
placeholder="masukkan deskripsi"
required
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi berita"
value={beritaState.berita.create.form.deskripsi}
onChange={(e) => (beritaState.berita.create.form.deskripsi = e.target.value)}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<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
value={beritaState.berita.create.form.content}
onChange={(htmlContent) => {
@@ -141,7 +199,21 @@ export default function CreateBerita() {
}}
/>
</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>
</Paper>
</Box>

View File

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

View File

@@ -1,119 +0,0 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await fotoState.update.load(id);
if (data) {
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
toast.error('Gagal memuat data foto');
}
};
loadFoto();
}, [params?.id]);
const handleSubmit = async () => {
try {
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
fotoState.update.form.imagesId = uploaded.id;
}
await fotoState.update.update();
toast.success('Foto berhasil diperbarui!');
router.push('/admin/desa/gallery/foto');
} catch (error) {
console.error('Error updating foto:', error);
toast.error('Terjadi kesalahan saat memperbarui foto');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.update.form.name}
onChange={(e) =>
(fotoState.update.form.name = e.target.value)
}
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<EditEditor
value={fotoState.update.form.deskripsi}
onChange={(val) => {
fotoState.update.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditFoto;

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,109 +0,0 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateFoto() {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
fotoState.create.form = {
name: "",
deskripsi: "",
imagesId: "",
};
setPreviewImage(null)
setFile(null)
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
fotoState.create.form.imagesId = uploaded.id;
await fotoState.create.create();
resetForm();
router.push("/admin/desa/gallery/foto")
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Foto</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Foto</Text>}
placeholder='Masukkan judul foto'
value={fotoState.create.form.name}
onChange={(val) => {
fotoState.create.form.name = val.target.value;
}}
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Foto</Text>
<CreateEditor
value={fotoState.create.form.deskripsi}
onChange={(val) => {
fotoState.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateFoto;

View File

@@ -1,93 +1,158 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';
import HeaderSearch from '../../../_com/header';
import { useState } from 'react';
"use client";
import stateFileStorage from "@/state/state-list-image";
import {
ActionIcon,
Box,
Card,
Flex,
Group,
Image,
Pagination,
Paper,
SimpleGrid,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
import { motion } from "framer-motion";
import toast from "react-simple-toasts";
import { useSnapshot } from "valtio";
function Foto() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListFoto search={search} />
</Box>
);
}
function ListFoto({ search }: { search: string }) {
const fotoState = useProxy(stateGallery.foto)
const router = useRouter();
export default function ListImage() {
const { list, total } = useSnapshot(stateFileStorage);
useShallowEffect(() => {
fotoState.findMany.load()
}, [])
stateFileStorage.load();
}, []);
const filteredData = (fotoState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
let timeOut: NodeJS.Timer;
if (!fotoState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Foto'
href='/admin/desa/gallery/foto/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
<Stack p="lg" gap="lg">
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
<Title order={2} fw={700}>
Galeri Foto
</Title>
<TextInput
radius="xl"
size="md"
placeholder="Cari foto berdasarkan nama..."
leftSection={<IconSearch size={18} />}
rightSection={
<ActionIcon
variant="light"
color="gray"
radius="xl"
onClick={() => stateFileStorage.load()}
>
<IconX size={18} />
</ActionIcon>
}
onChange={(e) => {
if (timeOut) clearTimeout(timeOut);
timeOut = setTimeout(() => {
stateFileStorage.load({ search: e.target.value });
}, 300);
}}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Judul Foto</TableTh>
<TableTh>Tanggal Foto</TableTh>
<TableTh>Deskripsi Foto</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</Flex>
<Paper withBorder radius="lg" p="md" shadow="sm">
{list && list.length > 0 ? (
<SimpleGrid
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
spacing="md"
verticalSpacing="md"
>
{list.map((v, k) => (
<Card
key={k}
withBorder
radius="md"
shadow="sm"
className="hover:shadow-md transition-all duration-200"
>
<Stack gap="xs">
<motion.div
onClick={() => {
navigator.clipboard.writeText(v.url);
toast("Tautan foto berhasil disalin");
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{ cursor: "pointer" }}
>
<Image
src={`${v.url}?size=200`}
alt={v.name}
radius="md"
h={120}
fit="cover"
loading="lazy"
/>
</motion.div>
<Box>
<Text size="sm" fw={500} lineClamp={2}>
{v.name}
</Text>
</Box>
<Group justify="space-between" align="center" pt="xs">
<Tooltip label="Hapus foto" withArrow>
<ActionIcon
variant="subtle"
color="red"
radius="md"
onClick={() => {
stateFileStorage
.del({ name: v.name })
.finally(() => toast("Foto berhasil dihapus"));
}}
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</Group>
</Stack>
</Card>
))}
</TableTbody>
</Table>
</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>
</Box>
{total && total > 1 && (
<Flex justify="center">
<Pagination
total={total}
size="md"
radius="md"
withEdges
onChange={(page) => {
stateFileStorage.page = page;
stateFileStorage.load();
}}
/>
</Flex>
)}
</Stack>
);
}
export default Foto;

View File

@@ -1,5 +1,5 @@
'use client'
import LayoutTabsGallery from "../../ppid/_com/layoutTabsGallery"
import LayoutTabsGallery from "./lib/layoutTabs"
export default function Layout({ children }: { children: React.ReactNode }) {
return (

View File

@@ -0,0 +1,108 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconPhoto, IconVideo } from '@tabler/icons-react';
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Foto",
value: "foto",
href: "/admin/desa/gallery/foto",
icon: <IconPhoto size={18} stroke={1.8} />,
tooltip: "Kelola foto-foto galeri desa"
},
{
label: "Video",
value: "video",
href: "/admin/desa/gallery/video",
icon: <IconVideo size={18} stroke={1.8} />,
tooltip: "Kelola video galeri desa"
},
];
const currentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
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>
{tabs.map((tab, i) => (
<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>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsGallery;

View File

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

View File

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

View File

@@ -1,8 +1,18 @@
'use client'
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -10,77 +20,104 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
function CreateVideo() {
const videoState = useProxy(stateGallery.video)
const videoState = useProxy(stateGallery.video);
const router = useRouter();
const [link, setLink] = useState("");
const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link);
const resetForm = () => {
videoState.create.form = {
name: "",
deskripsi: "",
linkVideo: "",
name: '',
deskripsi: '',
linkVideo: '',
};
setLink('');
};
const handleSubmit = async () => {
if (!embedLink) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga)
videoState.create.form.linkVideo = embedLink;
await videoState.create.create();
resetForm();
router.push("/admin/desa/gallery/video");
router.push('/admin/desa/gallery/video');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back Button + Title */}
<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 Video
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Video</Title>
{/* 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">
{/* Judul */}
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
placeholder='Masukkan judul video'
label="Judul Video"
placeholder="Masukkan judul video"
value={videoState.create.form.name}
onChange={(val) => {
videoState.create.form.name = val.target.value;
onChange={(e) => {
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 && (
<iframe
style={{ borderRadius: 10, width: "100%", height: 400 }}
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
)}
</Stack>
</Box>
{/* Link YouTube */}
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={link}
onChange={(e) => setLink(e.currentTarget.value)}
required
/>
{/* 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>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Video
</Text>
<CreateEditor
value={videoState.create.form.deskripsi}
onChange={(val) => {
@@ -88,8 +125,21 @@ function CreateVideo() {
}}
/>
</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>
</Stack>
</Paper>

View File

@@ -1,22 +1,39 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
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 HeaderSearch from '../../../_com/header';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateGallery from '../../../_state/desa/gallery';
function Video() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
title='Video'
placeholder='Cari judul atau deskripsi video...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,62 +47,118 @@ function ListVideo({ search }: { search: string }) {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = videoState.findMany;
useShallowEffect(() => {
videoState.findMany.load()
}, [])
load(page, 10, search)
}, [page, search])
const filteredData = (videoState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (!videoState.findMany.data) {
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Video'
href='/admin/desa/gallery/video/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Judul Video</TableTh>
<TableTh>Tanggal Video</TableTh>
<TableTh>Deskripsi Video</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{new Date(item.createdAt).toDateString()}</TableTd>
<TableTd>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Video</Title>
<Tooltip label="Tambah Video Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/gallery/video/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Judul Video</TableTh>
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{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>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}

View File

@@ -3,7 +3,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -49,52 +49,74 @@ function EditPelayananPendudukNonPermanent() {
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
<Text fw={"bold"}>Judul</Text>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
}}
/>
<Text fw={"bold"}>Deskripsi</Text>
<Stack gap="xs">
<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">
Edit Pelayanan Penduduk Non Permanent
</Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="xs">
<Title order={3}>Edit Pelayanan Penduduk Non Permanent</Title>
{/* 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
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
}}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name}
>
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={() => router.back()}
disabled={statePendudukNonPermanent.update.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);

View File

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

View File

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

View File

@@ -1,6 +1,23 @@
'use client'
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 { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -9,88 +26,158 @@ import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function PerizinanBerusaha() {
const router = useRouter()
const pelayananPerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
const router = useRouter();
const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [active, setActive] = useState(1);
const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
const nextStep = () =>
setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => {
pelayananPerizinanBerusaha.findById.load('1')
}, [])
pelayananPerizinanBerusaha.findById.load('1');
}, []);
if(!pelayananPerizinanBerusaha.findById.data) {
if (!pelayananPerizinanBerusaha.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
<Stack align="center" justify="center" py="xl">
<Skeleton radius="md" height={800} />
</Stack>
)
);
}
const data = pelayananPerizinanBerusaha.findById.data;
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['BG-trans']} p={'md'}>
<Box py={15}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"h4"} fw={"bold"}>Preview Pelayanan Perizinan Berusaha</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
</Stack>
</Box>
<Text fz={{ base: "h4", md: 'h2' }} fw={"bold"}>{pelayananPerizinanBerusaha.findById.data.name}</Text>
<Text py={10} ta={"justify"} fz={{ base: "sm", md: 'h3' }} dangerouslySetInnerHTML={{__html: pelayananPerizinanBerusaha.findById.data.deskripsi}} />
<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'
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
{/* Header */}
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>
Preview Pelayanan Perizinan Berusaha
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Tooltip label="Edit Data Perizinan" withArrow>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() =>
router.push(
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
)
}
}}>
<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>
>
Edit
</Button>
</Tooltip>
</GridCol>
</Grid>
<Group justify="center" mt="xl">
<Button variant="default" onClick={prevStep}>Back</Button>
<Button onClick={nextStep}>Next step</Button>
</Group>
<Text py={35} ta={"justify"} fz={{ base: "sm", md: 'h3' }}>Penting untuk diingat bahwa prosedur dan persyaratan dapat berubah
seiring waktu. Untuk informasi yang lebih akurat dan terkini, saya sarankan untuk mengunjungi situs
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>
{/* 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 }}
/>
<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>
</Paper>
</Paper>
</Box>
</Stack>
</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 colors from '@/con/colors';
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 { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -13,9 +24,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditSuratKeterangan() {
const router = useRouter()
const params = useParams()
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
const router = useRouter();
const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
@@ -25,39 +37,32 @@ function EditSuratKeterangan() {
deskripsi: stateSurat.edit.form.deskripsi,
imageId: stateSurat.edit.form.imageId,
image2Id: stateSurat.edit.form.image2Id,
})
});
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateSurat.edit.load(id);
if (data) {
setFormData({
name: data.name || "",
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
image2Id: data.image2Id || "",
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
image2Id: data.image2Id || '',
});
if (data.image?.link) {
setPreviewImage(data.image.link);
} else {
setPreviewImage(null);
}
if (data.image2?.link) {
setPreviewImage2(data.image2.link);
} else {
setPreviewImage2(null);
}
setPreviewImage(data.image?.link || null);
setPreviewImage2(data.image2?.link || null);
}
} catch (error) {
console.error("Error loading surat:", error);
toast.error("Gagal memuat data surat");
console.error('Error loading surat:', error);
toast.error('Gagal memuat data surat');
}
};
loadSurat();
}, [params?.id]);
@@ -65,171 +70,199 @@ function EditSuratKeterangan() {
try {
stateSurat.edit.form = {
...stateSurat.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
image2Id: formData.image2Id,
}
...formData,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateSurat.edit.form.imageId = uploaded.id;
}
if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateSurat.edit.form.image2Id = uploaded.id;
}
await stateSurat.edit.update()
toast.success("Surat berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_surat_keterangan")
await stateSurat.edit.update();
toast.success('Surat berhasil diperbarui!');
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) {
console.error("Error updating surat:", error);
toast.error("Terjadi kesalahan saat memperbarui surat");
console.error('Error updating surat:', error);
toast.error('Terjadi kesalahan saat memperbarui surat');
}
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Surat Keterangan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<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">
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
label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
/>
</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>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
{/* Upload Gambar 1 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar 1
</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"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
alt="Preview Gambar 1"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
/>
)}
</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>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage2 && (
{/* Upload Gambar 2 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar 2
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile2(selectedFile);
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
src={previewImage2}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
alt="Preview Gambar 2"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
/>
)}
</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>
</Paper>
</Box>

View File

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

View File

@@ -1,9 +1,21 @@
'use client'
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
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';
@@ -12,25 +24,25 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateSuratKeterangan() {
const stateSurat = useProxy(stateLayananDesa.suratKeterangan)
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage2, setPreviewImage2] = 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 = () => {
stateSurat.create.form = {
name: "",
deskripsi: "",
imageId: "",
image2Id: ""
}
setPreviewImage(null)
setPreviewImage2(null)
}
name: '',
deskripsi: '',
imageId: '',
image2Id: '',
};
setPreviewImage(null);
setPreviewImage2(null);
};
const handleSubmit = async () => {
if (!previewImage) {
return toast.warn("Pilih file gambar utama terlebih dahulu");
return toast.warn('Pilih file gambar utama terlebih dahulu');
}
try {
@@ -42,11 +54,10 @@ function CreateSuratKeterangan() {
const uploadedImage1 = res1.data?.data;
if (!uploadedImage1?.id) {
return toast.error("Gagal upload gambar utama");
return toast.error('Gagal upload gambar utama');
}
let uploadedImage2 = null;
// Upload gambar kedua jika ada
if (previewImage2) {
const res2 = await ApiFetch.api.fileStorage.create.post({
file: previewImage2.file,
@@ -55,44 +66,58 @@ function CreateSuratKeterangan() {
uploadedImage2 = res2.data?.data;
}
// Set form data
stateSurat.create.form.imageId = uploadedImage1.id;
if (uploadedImage2?.id) {
stateSurat.create.form.image2Id = uploadedImage2.id;
}
// Create the record
await stateSurat.create.create();
// Reset form dan redirect
resetForm();
toast.success("Data surat keterangan berhasil ditambahkan");
router.push("/admin/desa/layanan/pelayanan_surat_keterangan");
toast.success('Data surat keterangan berhasil ditambahkan');
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) {
console.error("Error creating surat keterangan:", error);
toast.error("Terjadi kesalahan saat menambahkan surat keterangan");
console.error('Error creating surat keterangan:', error);
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Surat Keterangan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* 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 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
value={stateSurat.create.form.name}
onChange={(val) => {
stateSurat.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
onChange={(val) => (stateSurat.create.form.name = val.target.value)}
label={<Text fz="sm" fw="bold">Nama Surat Keterangan</Text>}
placeholder="Masukkan nama surat keterangan"
required
/>
{/* Konten */}
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<Text fz="sm" fw="bold" mb={6}>
Konten
</Text>
<CreateEditor
value={stateSurat.create.form.deskripsi}
onChange={(htmlContent) => {
@@ -100,106 +125,124 @@ function CreateSuratKeterangan() {
}}
/>
</Box>
{/* Gambar Utama */}
<Box>
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Utama</Text>
<Text fw="bold" fz="sm" mb={6}>
Gambar Utama
</Text>
<Dropzone
onDrop={(files) => {
const file = files[0];
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
preview: URL.createObjectURL(file),
});
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<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.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.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>
<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>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
{previewImage && (
<Image
src={previewImage.preview}
alt="Preview Gambar Utama"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
/>
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage.preview}
alt="Preview Gambar Utama"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
/>
</Box>
)}
</Box>
<Box mt="lg">
<Text fz={"md"} fw={"bold"} mb="sm">Gambar Tambahan (Opsional)</Text>
{/* Gambar Tambahan */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Tambahan (Opsional)
</Text>
<Dropzone
onDrop={(files) => {
const file = files[0];
if (file) {
setPreviewImage2({
file,
preview: URL.createObjectURL(file)
preview: URL.createObjectURL(file),
});
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={120} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<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.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.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>
<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>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
{previewImage2 ? (
<Image
src={previewImage2.preview}
alt="Preview Gambar Tambahan"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
/>
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage2.preview}
alt="Preview Gambar Tambahan"
radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
/>
</Box>
) : (
<Text size="sm" c="dimmed" mt="sm">
<Text size="sm" c="dimmed" mt="sm" ta="center">
Kosongkan jika tidak ada gambar tambahan
</Text>
)}
</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>
</Paper>
</Box>

View File

@@ -1,13 +1,30 @@
/* 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 {
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 { 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 SuratKeterangan() {
@@ -16,7 +33,7 @@ function SuratKeterangan() {
<Box>
<HeaderSearch
title='Pelayanan Surat Keterangan'
placeholder='pencarian'
placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,8 +44,8 @@ function SuratKeterangan() {
}
function ListSuratKeterangan({ search }: { search: string }) {
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan)
const router = useRouter()
const suratKeteranganState = useProxy(stateLayananDesa.suratKeterangan);
const router = useRouter();
const {
data,
@@ -39,102 +56,111 @@ function ListSuratKeterangan({ search }: { search: string }) {
} = suratKeteranganState.findMany;
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 (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Surat Keterangan'
href='/admin/desa/layanan/pelayanan_surat_keterangan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={300}>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Text>
<Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_surat_keterangan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</Text>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Surat Keterangan</Title>
<Tooltip label="Tambah Surat Keterangan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/desa/layanan/pelayanan_surat_keterangan/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<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>
<Center>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -2,22 +2,34 @@
/* eslint-disable react-hooks/exhaustive-deps */
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
const params = useParams()
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name,
deskripsi: stateTelunjukDesa.edit.form.deskripsi,
link: stateTelunjukDesa.edit.form.link,
})
});
useEffect(() => {
const loadPelayananTelunjukSakti = async () => {
@@ -27,14 +39,14 @@ function EditPelayananTelunjukSakti() {
const data = await stateTelunjukDesa.edit.load(id);
if (data) {
setFormData({
name: data.name,
deskripsi: data.deskripsi,
link: data.link,
name: data.name || '',
deskripsi: data.deskripsi || '',
link: data.link || '',
});
}
} catch (error) {
console.error("Error loading pelayanan telunjuk sakti:", error);
toast.error("Gagal memuat data pelayanan telunjuk sakti");
console.error('Error loading pelayanan telunjuk sakti:', error);
toast.error('Gagal memuat data pelayanan telunjuk sakti');
}
};
loadPelayananTelunjukSakti();
@@ -44,57 +56,86 @@ function EditPelayananTelunjukSakti() {
try {
stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
link: formData.link,
}
await stateTelunjukDesa.edit.update()
toast.success("Pelayanan telunjuk sakti berhasil diperbarui!")
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
...formData,
};
await stateTelunjukDesa.edit.update();
toast.success('Pelayanan telunjuk sakti berhasil diperbarui!');
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa');
} catch (error) {
console.error("Error updating pelayanan telunjuk sakti:", error);
toast.error("Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti");
console.error('Error updating pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
}
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button + Title */}
<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">
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>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Surat Keterangan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Surat Keterangan</Text>}
placeholder="masukkan nama surat keterangan"
/>
<TextInput
value={formData.deskripsi}
onChange={(val) => {
setFormData({ ...formData, deskripsi: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
placeholder="masukkan tautan link"
/>
<TextInput
value={formData.link}
onChange={(val) => {
setFormData({ ...formData, link: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
placeholder="masukkan link"
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
{/* Link */}
<TextInput
label="Link"
placeholder="Masukkan link terkait"
value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
/>
{/* 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>
</Paper>
</Box>
);
}

View File

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

View File

@@ -1,64 +1,117 @@
'use client'
'use client';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
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 { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
const resetForm = () => {
stateTelunjukDesa.create.form = {
name: "",
deskripsi: "",
link: "",
}
}
name: '',
deskripsi: '',
link: '',
};
};
const handleSubmit = async () => {
await stateTelunjukDesa.create.create()
resetForm()
router.push("/admin/desa/layanan/pelayanan_telunjuk_sakti_desa")
try {
await stateTelunjukDesa.create.create();
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 (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pelayanan Telunjuk Sakti Desa</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* 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 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
value={stateTelunjukDesa.create.form.name}
onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Pelayanan Telunjuk Sakti Desa</Text>}
placeholder="masukkan nama pelayanan telunjuk sakti desa"
label={<Text fz="sm" fw="bold">Nama Pelayanan</Text>}
placeholder="Masukkan nama pelayanan telunjuk sakti desa"
required
/>
{/* Deskripsi */}
<TextInput
value={stateTelunjukDesa.create.form.deskripsi}
onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Tautan Link</Text>}
placeholder="masukkan tautan link"
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="Masukkan deskripsi pelayanan"
/>
{/* Link */}
<TextInput
value={stateTelunjukDesa.create.form.link}
onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Link</Text>}
placeholder="masukkan link"
label={<Text fz="sm" fw="bold">Link</Text>}
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>
</Paper>
</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 */
'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 {
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 { useEffect, useMemo, useState } from 'react';
import { useEffect, 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() {
@@ -15,8 +189,8 @@ function PelayananTelunjukSakti() {
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
title="Pelayanan Telunjuk Sakti"
placeholder="Cari layanan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,125 +201,113 @@ function PelayananTelunjukSakti() {
}
function ListPelayananTelunjukSakti({ search }: { search: string }) {
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
const router = useRouter()
const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = telunjukSaktiState.findMany;
const { data, page, totalPages, loading, load } = telunjukSaktiState.findMany;
useEffect(() => {
load(page, 10)
}, [])
load(page, 10, search);
}, [page, search]);
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]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Skeleton height={400} radius="md" />
</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>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
<Tooltip label="Tambah Layanan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '40%' }}>Link</TableTh>
<TableTh style={{ width: '30%' }}>Detail</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{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>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
@@ -153,3 +315,4 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
}
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 colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Paper, Stack, Title, TextInput, FileInput, Center, Text, Image } from '@mantine/core';
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 { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
@@ -83,51 +95,104 @@ function EditPenghargaan() {
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Penghargaan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol Back + Title */}
<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">
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
label="Judul"
placeholder="Masukkan judul penghargaan"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
required
/>
{/* Input Juara */}
<TextInput
label="Juara"
placeholder="Masukkan juara"
value={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
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);
}}
required
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
{/* Upload Gambar */}
<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
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -137,7 +202,21 @@ function EditPenghargaan() {
/>
</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>
</Paper>
</Box>

View File

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

View File

@@ -1,83 +1,109 @@
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import penghargaanState from '../../../_state/desa/penghargaan';
import ApiFetch from '@/lib/api-fetch';
import CreateEditor from '../../../_com/createEditor';
import penghargaanState from '../../../_state/desa/penghargaan';
function CreatePenghargaan() {
const statePenghargaan = useProxy(penghargaanState)
const statePenghargaan = useProxy(penghargaanState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
statePenghargaan.create.form = {
name: "",
juara: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
name: '',
juara: '',
deskripsi: '',
imageId: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
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({
file: file,
name: file.name
})
file,
name: file.name,
});
const uploaded = res.data?.data;
const uploaded = res.data?.data
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()
resetForm()
router.push("/admin/desa/penghargaan")
await statePenghargaan.create.create();
resetForm();
router.push('/admin/desa/penghargaan');
};
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Penghargaan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* 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 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
value={statePenghargaan.create.form.name}
onChange={(val) => {
statePenghargaan.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Penghargaan</Text>}
placeholder="masukkan nama penghargaan"
onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
label={<Text fz="sm" fw="bold">Nama Penghargaan</Text>}
placeholder="Masukkan nama penghargaan"
required
/>
<TextInput
value={statePenghargaan.create.form.juara}
onChange={(val) => {
statePenghargaan.create.form.juara = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Juara</Text>}
placeholder="masukkan juara"
onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
label={<Text fz="sm" fw="bold">Juara</Text>}
placeholder="Masukkan juara"
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fz="sm" fw="bold" mb={6}>Deskripsi</Text>
<CreateEditor
value={statePenghargaan.create.form.deskripsi}
onChange={(htmlContent) => {
@@ -85,26 +111,67 @@ function CreatePenghargaan() {
}}
/>
</Box>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</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>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Dropzone Upload */}
<Box>
<Text fz="sm" fw="bold" mb={6}>Gambar</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>
{/* 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>
</Paper>
</Box>

View File

@@ -2,21 +2,38 @@
'use client'
import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import {
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 { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
function Penghargaan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Penghargaan'
placeholder='pencarian'
title="Penghargaan"
placeholder="Cari nama atau deskripsi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,125 +44,114 @@ function Penghargaan() {
}
function ListPenghargaan({ search }: { search: string }) {
const state = useProxy(penghargaanState)
const router = useRouter()
const state = useProxy(penghargaanState);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
const { data, page, totalPages, loading, load } = state.findMany;
useEffect(() => {
load(page, 10)
}, [])
load(page, 10, search);
}, [page, search]);
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]);
const filteredData = data || []
// Handle loading state
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
<Skeleton height={600} radius="md" />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Penghargaan'
href='/admin/desa/penghargaan/create'
/>
<Table striped withTableBorder withRowBorders>
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Penghargaan</Title>
<Tooltip label="Tambah Penghargaan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/desa/penghargaan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '35%' }}>Nama</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '30%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd colSpan={4}>
<Text ta="center">Tidak ada data</Text>
</TableTd>
</TableTr>
{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}>
<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>
</Table>
</Paper>
</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>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 React, { useEffect, useState } from 'react';
import { IconListDetails, IconCategory } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -12,16 +13,21 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
{
label: "List Pengumuman",
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",
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 tab = tabs.find(t => t.value === value)
@@ -39,24 +45,59 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}, [pathname])
return (
<Stack>
<Title order={3}>Pengumuman</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{tabs.map((tab, i) => (
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow 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>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
{tabs.map((tab, i) => (
<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>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsLayanan;
export default LayoutTabsLayanan;

View File

@@ -1,8 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
Text,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -10,67 +20,108 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category)
const editState = useProxy(stateDesaPengumuman.category);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
});
name: editState.update.form.name || '',
});
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error("Error loading kategori Pengumuman:", error);
toast.error("Gagal memuat data kategori Pengumuman");
}
};
loadKategori();
}, [params?.id]);
const loadKategori = async () => {
const id = params?.id as string;
if (!id) return;
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');
const data = await editState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
});
}
} catch (error) {
console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
console.error('Error loading kategori Pengumuman:', error);
toast.error('Gagal memuat data kategori Pengumuman');
}
};
loadKategori();
}, [params?.id]);
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
name: formData.name,
};
await editState.update.update();
toast.success('Kategori Pengumuman berhasil diperbarui!');
router.push('/admin/desa/pengumuman/kategori-pengumuman');
} catch (error) {
console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kategori Pengumuman</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* 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">
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
label={
<Text fz="sm" fw="bold">
Nama Kategori Pengumuman
</Text>
}
placeholder="Masukkan nama kategori Pengumuman"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Kategori Pengumuman</Text>}
placeholder="masukkan nama kategori Pengumuman"
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
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>
</Paper>
</Box>

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