Compare commits

...

4 Commits

Author SHA1 Message Date
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
61 changed files with 3118 additions and 825 deletions

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

@@ -85,7 +85,6 @@ model FileStorage {
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
KolaborasiInovasi KolaborasiInovasi[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
KegiatanDesa KegiatanDesa[]
@@ -100,6 +99,8 @@ model FileStorage {
DataPerpustakaan DataPerpustakaan[]
PegawaiPPID PegawaiPPID[]
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
MitraKolaborasi MitraKolaborasi[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -201,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())
@@ -223,7 +224,7 @@ model KategoriPrestasiDesa {
model Responden {
id String @id @default(cuid())
name String @unique
tanggal String // misal: 2025-05-01
tanggal DateTime @db.Date // misal: 2025-05-01
jenisKelamin JenisKelaminResponden @relation(fields: [jenisKelaminId], references: [id])
jenisKelaminId String
rating PilihanRatingResponden @relation(fields: [ratingId], references: [id])
@@ -1644,14 +1645,22 @@ model KolaborasiInovasi {
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)
}
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 ========================================= //
model InfoTekno {
id String @id @default(cuid())

View File

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

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

@@ -59,7 +59,12 @@ const prestasiDesa = proxy({
Prisma.PrestasiDesaGetPayload<{
include: {
image: true;
kategori: true;
kategori: {
select: {
id: true;
name: true;
};
};
};
}>
> | null,

View File

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

View File

@@ -63,7 +63,7 @@ function DetailJumlahPengangguran() {
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text>{stateDetail.findUnique.data?.year}</Text>
<Text>{stateDetail.findUnique.data?.year ? new Date(stateDetail.findUnique.data.year).getFullYear() : ''}</Text>
</Box>
<Box>
<Text fw={"bold"}>Bulan</Text>
@@ -108,4 +108,3 @@ function DetailJumlahPengangguran() {
}
export default DetailJumlahPengangguran;

View File

@@ -57,7 +57,7 @@ function ListDetailDataPengangguran({search}: {search: string}) {
setChartData(stateDetail.findMany.data.map((item) => ({
id: item.id,
month: item.month,
year: item.year,
year: item.year instanceof Date ? item.year.getFullYear() : Number(item.year),
educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,8 +39,22 @@ function ListPrestasi({ search }: { search: string }) {
load,
} = listState.findMany
// Debug log
console.log('ListPrestasi state:', {
loading,
data: data?.length,
page,
totalPages,
search
});
useEffect(() => {
load(page, 10, search)
console.log('Loading data...', { page, search });
load(page, 10, search).then(() => {
console.log('Data loaded:', listState.findMany.data);
}).catch(error => {
console.error('Error loading data:', error);
});
}, [page, search])
const filteredData = data || []
@@ -84,7 +98,7 @@ function ListPrestasi({ search }: { search: string }) {
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd>{item.kategori?.name || 'No Category'}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}>
<IconDeviceImacCog size={25} />

View File

@@ -105,10 +105,12 @@ function ListDaftarInformasi({ search }: { search: string }) {
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Text mt={10} fz={"md"}>{index + 1}</Text>
</TableTd>
<TableTd style={{ wordWrap: 'break-word' }}>
<Box w={200}>
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.jenisInformasi}</Text>
<Text mt={10} fz={"md"} truncate={"end"} lineClamp={1}>{item.jenisInformasi}</Text>
</Box>
</TableTd>
<TableTd style={{ wordWrap: 'break-word' }}>

View File

@@ -17,7 +17,7 @@ export const navBar = [
{
id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey"
path: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
},
{
id: "Landing_Page_4",
@@ -286,7 +286,7 @@ export const navBar = [
{
id: "Inovasi_4",
name: "Kolaborasi Inovasi",
path: "/admin/inovasi/kolaborasi-inovasi"
path: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
},
{
id: "Inovasi_5",

View File

@@ -96,28 +96,36 @@ export default async function detailDataPengangguranCreate(context: Context) {
// Cari bulan sebelumnya
const currentMonthIndex = monthOrder.indexOf(body.month);
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
const prevYear = currentMonthIndex > 0 ? body.year : body.year - 1;
// Handle year as number for calculations
const currentYear = typeof body.year === 'number' ? body.year : new Date(body.year).getFullYear();
const prevYear = currentMonthIndex > 0 ? currentYear : currentYear - 1;
// Create date objects for Prisma
const currentYearDate = new Date(currentYear, 0, 1);
const prevYearDate = new Date(prevYear, 0, 1);
const prevData = await prisma.detailDataPengangguran.findFirst({
where: {
month: prevMonth,
year: prevYear,
year: prevYearDate,
},
});
let percentageChange: number | null = null;
if (prevData) {
const change = ((body.totalUnemployment - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
const change = ((Number(body.totalUnemployment) - Number(prevData.totalUnemployment)) / Number(prevData.totalUnemployment)) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
// Create the new record with properly typed values
const created = await prisma.detailDataPengangguran.create({
data: {
month: body.month,
year: body.year,
totalUnemployment: body.totalUnemployment,
educatedUnemployment: body.educatedUnemployment,
uneducatedUnemployment: body.uneducatedUnemployment,
year: currentYearDate,
totalUnemployment: Number(body.totalUnemployment),
educatedUnemployment: Number(body.educatedUnemployment),
uneducatedUnemployment: Number(body.uneducatedUnemployment),
percentageChange,
},
select: {

View File

@@ -15,7 +15,7 @@ export default async function findByMonthYear(context: Context) {
const data = await prisma.detailDataPengangguran.findFirst({
where: {
month,
year: Number(year),
year: new Date(Number(year), 0, 1), // Convert year number to Date object for Prisma
},
});

View File

@@ -81,13 +81,18 @@ export default async function detailDataPengangguranUpdate(context: Context) {
return { success: false, message: "ID tidak ditemukan" };
}
const { month, year, totalUnemployment, educatedUnemployment, uneducatedUnemployment } = context.body as {
const { month, year: yearInput, totalUnemployment, educatedUnemployment, uneducatedUnemployment } = context.body as {
month: string;
year: number;
totalUnemployment: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
year: number | string | Date;
totalUnemployment: number | string;
educatedUnemployment: number | string;
uneducatedUnemployment: number | string;
};
// Normalize year to Date object
const year = typeof yearInput === 'number' ? new Date(yearInput, 0, 1) :
yearInput instanceof Date ? yearInput :
new Date(parseInt(yearInput as string), 0, 1);
const duplicate = await prisma.detailDataPengangguran.findFirst({
where: {
@@ -109,18 +114,19 @@ export default async function detailDataPengangguranUpdate(context: Context) {
const currentMonthIndex = monthOrder.indexOf(month);
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
const prevYear = currentMonthIndex > 0 ? year : year - 1;
const currentYear = year.getFullYear();
const prevYear = currentMonthIndex > 0 ? currentYear : currentYear - 1;
const prevData = await prisma.detailDataPengangguran.findFirst({
where: {
month: prevMonth,
year: prevYear,
year: new Date(prevYear, 0, 1),
},
});
let percentageChange: number | null = null;
if (prevData) {
const change = ((totalUnemployment - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
const change = ((Number(totalUnemployment) - Number(prevData.totalUnemployment)) / Number(prevData.totalUnemployment)) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
@@ -128,10 +134,10 @@ export default async function detailDataPengangguranUpdate(context: Context) {
where: { id },
data: {
month,
year,
totalUnemployment,
educatedUnemployment,
uneducatedUnemployment,
year: new Date(year), // Ensure it's a new Date instance
totalUnemployment: Number(totalUnemployment),
educatedUnemployment: Number(educatedUnemployment),
uneducatedUnemployment: Number(uneducatedUnemployment),
percentageChange,
},
});

View File

@@ -1,23 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function desaDigitalFindMany() {
try {
const data = await prisma.desaDigital.findMany({
include: {
image: true,
},
});
export default async function desaDigitalFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return {
success: true,
message: "Success fetch desa digital",
data,
};
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch desa digital",
};
}
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.desaDigital.findMany({
where,
include: {
image: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.desaDigital.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil desa digital dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data desa digital",
};
}
}

View File

@@ -5,6 +5,7 @@ import KolaborasiInovasi from "./kolaborasi-inovasi";
import InfoTekno from "./info-teknologi";
import AjukanIdeInovatif from "./ajukan-ide-inovatif";
import LayananOnlineDesa from "./layanan-online-desa";
import MitraKolaborasi from "./kolaborasi-inovasi/mitra-kolaborasi";
const Inovasi = new Elysia({
prefix: "/api/inovasi",
@@ -16,5 +17,6 @@ const Inovasi = new Elysia({
.use(InfoTekno)
.use(AjukanIdeInovatif)
.use(LayananOnlineDesa)
.use(MitraKolaborasi)
export default Inovasi;

View File

@@ -1,23 +1,61 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function infoTeknoFindMany() {
try {
const data = await prisma.infoTekno.findMany({
include: {
image: true,
},
});
// Di findMany.ts
export default async function infoTeknoFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return {
success: true,
message: "Success fetch info teknologi",
data,
};
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch info teknologi",
};
}
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.infoTekno.findMany({
where,
include: {
image: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.infoTekno.count({
where
})
]);
const totalPages = Math.ceil(total / limit);
return {
success: true,
message: "Success fetch info teknologi with pagination",
data,
page,
totalPages,
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch info teknologi with pagination",
data: [],
page: 1,
totalPages: 1,
total: 0,
};
}
}

View File

@@ -1,34 +1,37 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import { Prisma } from "@prisma/client";
type FormCreateKolaborasiInovasi = {
name: string;
tahun: number;
slug: string;
deskripsi: string;
kolaborator: string;
imageId: string;
// Define validation schema
type FormCreateKolaborasiInovasi = Prisma.KolaborasiInovasiGetPayload<{
select: {
name: true;
tahun: true;
slug: true;
deskripsi: true;
kolaborator: true;
};
}>;
export default async function kolaborasiInovasiCreate(context: Context) {
const body = context.body as FormCreateKolaborasiInovasi;
// Create new kolaborasi inovasi
await prisma.kolaborasiInovasi.create({
data: {
name: body.name,
tahun: body.tahun,
slug: body.slug,
deskripsi: body.deskripsi,
kolaborator: body.kolaborator,
},
});
return {
success: true,
message: "Berhasil membuat kolaborasi inovasi",
data: {
...body,
},
};
}
export default async function kolaborasiInovasiCreate(context: Context){
const body = context.body as FormCreateKolaborasiInovasi;
await prisma.kolaborasiInovasi.create({
data: {
name: body.name,
tahun: body.tahun,
slug: body.slug,
deskripsi: body.deskripsi,
kolaborator: body.kolaborator,
imageId: body.imageId,
}
})
return {
success: true,
message: "Success create kolaborasi inovasi",
data: {
...body,
}
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
@@ -5,18 +6,42 @@ import { Context } from "elysia";
export default async function kolaborasiInovasiFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const year = (context.query.year as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan filter tahun (jika ada)
if (year) {
const startDate = new Date(parseInt(year), 0, 1); // 1 Januari tahun tersebut
const endDate = new Date(parseInt(year) + 1, 0, 1); // 1 Januari tahun berikutnya
where.createdAt = {
gte: startDate,
lt: endDate,
};
}
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ slug: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.kolaborasiInovasi.findMany({
where: { isActive: true },
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.kolaborasiInovasi.count({
where: { isActive: true }
where
})
]);

View File

@@ -15,9 +15,6 @@ export default async function kolaborasiInovasiFindUnique(context: Context) {
try {
const kolaborasiInovasi = await prisma.kolaborasiInovasi.findUnique({
where: { id },
include: {
image: true,
}
});
if (!kolaborasiInovasi) {

View File

@@ -21,7 +21,6 @@ const KolaborasiInovasi = new Elysia({
slug: t.String(),
deskripsi: t.String(),
kolaborator: t.String(),
imageId: t.String(),
}),
})
.put(
@@ -37,7 +36,7 @@ const KolaborasiInovasi = new Elysia({
slug: t.String(),
deskripsi: t.String(),
kolaborator: t.String(),
imageId: t.String(),
}),
}
)

View File

@@ -0,0 +1,26 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
imageId: string;
};
export default async function mitraKolaborasiCreate(context: Context) {
const body = context.body as FormCreate;
await prisma.mitraKolaborasi.create({
data: {
name: body.name,
imageId: body.imageId,
},
});
return {
success: true,
message: "Berhasil membuat mitra kolaborasi",
data: {
...body,
},
};
}

View File

@@ -0,0 +1,54 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
export default async function mitraKolaborasiDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const mitraKolaborasi = await prisma.mitraKolaborasi.findUnique({
where: { id },
include: {
image: true,
},
});
if (!mitraKolaborasi) {
return {
status: 404,
body: "Mitra kolaborasi tidak ditemukan",
};
}
if (mitraKolaborasi.image) {
try {
const filePath = path.join(
mitraKolaborasi.image.path,
mitraKolaborasi.image.name
);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: mitraKolaborasi.image.id },
});
} catch (error) {
console.error("Gagal hapus file image:", error);
}
}
await prisma.mitraKolaborasi.delete({
where: { id },
});
return {
success: true,
message: "Mitra kolaborasi berhasil dihapus",
status: 200,
};
}

View File

@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function mitraKolaborasiFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.mitraKolaborasi.findMany({
where,
include: {
image: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.mitraKolaborasi.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil mitra kolaborasi dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data mitra kolaborasi",
};
}
}

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function mitraKolaborasiFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak ditemukan",
}, {status: 400});
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, {status: 400});
}
const data = await prisma.mitraKolaborasi.findUnique({
where: { id },
include: {
image: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Mitra kolaborasi tidak ditemukan",
}, {status: 404});
}
return Response.json({
success: true,
message: "Success fetch mitra kolaborasi by ID",
data,
}, {status: 200});
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil mitra kolaborasi: " + (error instanceof Error ? error.message : 'Unknown error'),
}, {status: 500});
}
}

View File

@@ -0,0 +1,37 @@
import Elysia, { t } from "elysia";
import mitraKolaborasiCreate from "./create";
import mitraKolaborasiDelete from "./del";
import mitraKolaborasiUpdate from "./updt";
import mitraKolaborasiFindUnique from "./findUnique";
import mitraKolaborasiFindMany from "./findMany";
const MitraKolaborasi = new Elysia({
prefix: "/mitrakolaborasi",
tags: ["Inovasi/Mitra Kolaborasi"],
})
.post("/create", mitraKolaborasiCreate, {
body: t.Object({
name: t.String(),
imageId: t.String(),
}),
})
.get("/find-many", mitraKolaborasiFindMany)
.get("/:id", async (context) => {
const response = await mitraKolaborasiFindUnique(context.request);
return response;
})
.delete("/del/:id", mitraKolaborasiDelete)
.put(
"/:id",
async (context) => {
const response = await mitraKolaborasiUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
imageId: t.String(),
}),
}
);
export default MitraKolaborasi;

View File

@@ -0,0 +1,97 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
type FormUpdate = {
id: string;
name: string;
imageId: string;
}
export default async function mitraKolaborasiUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
name,
imageId
} = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak ditemukan",
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
})
}
const existing = await prisma.mitraKolaborasi.findUnique({
where: {id},
include: {
image: true,
}
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "mitra kolaborasi tidak ditemukan",
}), {
status: 404,
headers: {
'Content-Type': 'application/json'
}
})
}
if (existing.imageId && existing.imageId !== imageId) {
const oldImage = existing.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (error) {
console.error("Gagal hapus gambar lama:", error);
}
}
}
const updated = await prisma.mitraKolaborasi.update({
where: { id },
data: {
name,
imageId,
}
})
return new Response(JSON.stringify({
success: true,
message: "Mitra kolaborasi berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error("Error updating mitra kolaborasi:", error);
return new Response(JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate mitra kolaborasi",
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
})
}
}

View File

@@ -9,7 +9,7 @@ type FormUpdateKolaborasiInovasi = {
slug?: string;
deskripsi?: string;
kolaborator?: string;
imageId?: string;
};
export default async function kolaborasiInovasiUpdate(context: Context) {
const body = context.body as FormUpdateKolaborasiInovasi;
@@ -31,7 +31,6 @@ export default async function kolaborasiInovasiUpdate(context: Context) {
slug: body.slug,
deskripsi: body.deskripsi,
kolaborator: body.kolaborator,
imageId: body.imageId,
},
});

View File

@@ -10,7 +10,7 @@ async function prestasiDesaFindMany(context: Context) {
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
const where: any = {};
// Tambahkan pencarian (jika ada)
if (search) {
@@ -19,6 +19,11 @@ async function prestasiDesaFindMany(context: Context) {
{ deskripsi: { contains: search, mode: 'insensitive' } },
{ kategori: { name: { contains: search, mode: 'insensitive' } } },
];
// Tetap filter hanya yang aktif saat melakukan pencarian
where.isActive = true;
} else {
// Jika tidak ada pencarian, tampilkan hanya data yang aktif
where.isActive = true;
}
try {
@@ -27,11 +32,16 @@ async function prestasiDesaFindMany(context: Context) {
where,
include: {
image: true,
kategori: true,
kategori: {
select: {
id: true,
name: true
}
},
},
skip,
take: limit,
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
orderBy: { createdAt: "desc" },
}),
prisma.prestasiDesa.count({
where,

View File

@@ -68,7 +68,7 @@ function Page() {
{state.findMany.data?.[0]?.totalUnemployment || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
Data tahun {state.findMany.data?.[0]?.year || ''}
Data tahun {state.findMany.data?.[0]?.year ? new Date(state.findMany.data[0].year).getFullYear() : ''}
</Text>
</Flex>
</Paper>

View File

@@ -2,7 +2,7 @@
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconMapPinFilled, IconSearch, IconShoppingCartFilled, IconStarFilled } from '@tabler/icons-react';
import { motion } from 'motion/react';
import { useRouter } from 'next/navigation';
@@ -14,6 +14,7 @@ function Page() {
const router = useRouter()
const state = useProxy(pasarDesaState.pasarDesa)
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const {
data,
@@ -35,8 +36,8 @@ function Page() {
: data;
useShallowEffect(() => {
load(page, 4, search, selectedCategory || undefined)
}, [page, search, selectedCategory])
load(page, 4, debouncedSearch, selectedCategory || undefined)
}, [page, debouncedSearch, selectedCategory])
if (loading || !data) {

View File

@@ -1,12 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, SimpleGrid, Paper, Skeleton, Center } from '@mantine/core';
import React from 'react';
import { Stack, Box, Text, SimpleGrid, Paper, Skeleton, Center, Pagination, Grid, GridCol, TextInput } from '@mantine/core';
import React, { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { CartesianGrid, Legend, Line, LineChart as RechartsLineChart, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
interface StatistikData {
id: string;
@@ -29,15 +30,23 @@ interface ProgramKemiskinanData {
}
function Page() {
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const state = useProxy(programKemiskinanState)
const {
data,
page,
totalPages,
loading,
load
} = state.findMany
useShallowEffect(() => {
state.findMany.load()
}, [])
load(page, 2, debouncedSearch)
}, [page, debouncedSearch])
const data = programKemiskinanState.findMany.data
if (!data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
@@ -51,10 +60,24 @@ function Page() {
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kemiskinan
</Text>
<Text ta={'center'} fz={'h4'}>Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat</Text>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Program Kemiskinan
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Program'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
<Text fz={'h4'}>Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
@@ -74,10 +97,18 @@ function Page() {
)
})}
</SimpleGrid>
<Center my={10}>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
<Paper p={'xl'}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
<Box style={{ width: '100%', height: 'auto' }}>
{state.findMany.data.length > 0 && state.findMany.data[0]?.statistik ? (
{data.length > 0 && data[0]?.statistik ? (
<Box w="100%" style={{ overflowX: 'auto' }}>
<Center>
<RechartsLineChart

View File

@@ -1,79 +1,62 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, List, ListItem, Paper, SimpleGrid, Image } from '@mantine/core';
import React from 'react';
import { Stack, Box, Text, Paper, SimpleGrid, Image, Skeleton, Center, Pagination, Grid, GridCol, TextInput } from '@mantine/core';
import React, { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
const data = [
{
id: 1,
image: '/api/img/administrasi-digital.png',
judul: 'Layanan Administrasi Digital',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Sistem pengurusan dokumen kependudukan online.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pembuatan KTP, KK, Surat Keterangan secara daring.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Antrian dan pembayaran pajak berbasis aplikasi mobile.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Sistem informasi kependudukan terintegrasi.</ListItem>
</List>
},
{
id: 2,
image: '/api/img/edukasi-digital.png',
judul: 'Edukasi Digital',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Ruang Belajar Digital dengan akses internet gratis.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pelatihan komputer dan literasi digital untuk semua usia.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Kursus online keterampilan digital (desain, pemrograman, marketing).</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Beasiswa pendidikan teknologi untuk pemuda desa.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Perpustakaan digital dengan koleksi buku elektronik.</ListItem>
</List>
},
{
id: 3,
image: '/api/img/ekonomi-digital.png',
judul: 'Ekonomi Digital',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Marketplace produk UMKM Darmasaba.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Platform pemasaran hasil pertanian dan kerajinan lokal.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Sistem pembayaran digital untuk pelaku usaha desa.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Inkubator bisnis digital untuk wirausaha muda.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pelatihan e-commerce dan digital marketing.</ListItem>
</List>
},
{
id: 4,
image: '/api/img/kesehatan-daring.png',
judul: 'Kesehatan Daring',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Telemedicine dengan dokter dan puskesmas.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Monitoring kesehatan berbasis aplikasi.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pendaftaran antrian puskesmas online.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Edukasi kesehatan melalui platform digital.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Rekam medis elektronik.</ListItem>
</List>
},
{
id: 5,
image: '/api/img/pertanian-cerdas.png',
judul: 'Pertanian Cerdas',
deskripsi: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Sistem informasi cuaca dan prediksi pertanian.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Konsultasi pertanian online dengan ahli.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Penjualan hasil pertanian melalui platform digital.</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pelatihan pertanian modern berbasis teknologi.</ListItem>
</List>
},
]
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const state = useProxy(desaDigitalState)
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => {
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Desa Digital / Smart Village
</Text>
<Text ta={'center'} fz={'h4'}>Mewujudkan Desa Darmasaba sebagai pusat inovasi digital yang memberdayakan masyarakat, meningkatkan kesejahteraan, dan menciptakan peluang ekonomi berbasis teknologi.</Text>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Desa Digital / Smart Village
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Produk'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
<Text fz={'h4'}>Mewujudkan Desa Darmasaba sebagai pusat inovasi digital yang memberdayakan masyarakat, meningkatkan kesejahteraan, dan menciptakan peluang ekonomi berbasis teknologi.</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
@@ -84,13 +67,13 @@ function Page() {
md: 3
}}
>
{data.map((v, k) => {
{filteredData.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Image src={v.image} pb={10} radius={10} alt='' />
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
<Box pr={'lg'}>
{v.deskripsi}
<Image src={v.image.link? v.image.link : ''} pb={10} radius={10} alt='' />
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Box>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Box>
</Paper>
)
@@ -98,6 +81,15 @@ function Page() {
</SimpleGrid>
</Stack>
</Box>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Stack>
);
}

View File

@@ -1,108 +1,63 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, List, ListItem, Paper, SimpleGrid, Image } from '@mantine/core';
import React from 'react';
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
const data = [
{
id: 1,
image: '/api/img/pertanian-cerdas.png',
judul: 'Pertanian Cerdas',
subjudul1: 'Sistem Irigasi Hemat Air',
subjudul2: 'Pengolahan Limbah Pertanian',
deskripsi1: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Penggunaan sensor kelembaban tanah</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Kontrol penyiraman otomatis </ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pengurangan konsumsi air hingga 40%</ListItem>
</List>,
deskripsi2: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mesin pencacah jerami otomatis</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Konversi limbah menjadi pupuk organik</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mengurangi pembakaran sampah pertanian</ListItem>
</List>,
},
{
id: 2,
image: '/api/img/energi-terbarukan.png',
judul: 'Energi Terbarukan',
subjudul1: 'Pembangkit Listrik Mikrohidro',
subjudul2: 'Solar Home System',
deskripsi1: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Memanfaatkan aliran sungai sekitar</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Kapasitas 10-50 kW</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Elektrifikasi mandiri desa</ListItem>
</List>,
deskripsi2: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Panel surya untuk rumah tangga </ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Akses listrik 24 jam</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Hemat biaya energi</ListItem>
</List>,
},
{
id: 3,
image: '/api/img/pengolahan-pangan.png',
judul: 'Pengolahan Pangan',
subjudul1: 'Mesin Pengering Hasil Pertanian',
subjudul2: 'Alat Pengolah Hasil Pertanian',
deskripsi1: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Teknologi pengering tenaga surya</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Menjaga kualitas hasil panen</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mencegah kerusakan pasca panen</ListItem>
</List>,
deskripsi2: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mesin pengupas dan pengemas produk</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Meningkatkan nilai jual komoditas</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Standarisasi kualitas produk</ListItem>
</List>,
},
{
id: 4,
image: '/api/img/pengelolaan-sampah.png',
judul: 'Pengelolaan Sampah',
subjudul1: 'Sistem Pengolahan Sampah Terpadu',
subjudul2: 'Komposter Komunal',
deskripsi1: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pemilahan otomatis</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Daur ulang sampah organik dan anorganik</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pembangkit listrik dari sampah</ListItem>
</List>,
deskripsi2: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Teknologi pengomposan cepat</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Menghasilkan pupuk organik</ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Mengurangi sampah ke TPA</ListItem>
</List>,
},
{
id: 5,
image: '/api/img/kesehatan-daring.png',
judul: 'Kesehatan Masyarakat',
subjudul1: 'Alat Deteksi Dini Penyakit',
subjudul2: 'Air Bersih Mandiri',
deskripsi1: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Skrining kesehatan portable </ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Pemeriksaan tekanan darah dan gula </ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Konsultasi medis jarak jauh</ListItem>
</List>,
deskripsi2: <List>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Sistem filter air canggih </ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Menghilangkan kontaminan </ListItem>
<ListItem ta={'justify'} fz={{ base: 'h4', md: 'lg' }}>Jaminan kualitas air minum
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
import { IconSearch } from '@tabler/icons-react';
</ListItem>
</List>,
},
]
function Page() {
const [search, setSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const state = useProxy(infoTeknoState)
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => {
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Info Teknologi Tepat Guna
</Text>
<Text ta={'center'} fz={'h4'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat, mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Info Teknologi Tepat Guna
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Info Teknologi'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
<Text fz={'h4'}>Desa Darmasaba berkomitmen mengembangkan teknologi tepat guna yang sesuai dengan kebutuhan masyarakat, mendukung pembangunan berkelanjutan, dan meningkatkan kualitas hidup warga.</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} p={'lg'}>
@@ -113,18 +68,13 @@ function Page() {
md: 3
}}
>
{data.map((v, k) => {
{filteredData.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Image src={v.image} pb={10} radius={10} alt='' />
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
<Image src={v.image.link || ''} pb={10} radius={10} alt='' />
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Box pr={'lg'} pb={10}>
<Text fz={'h4'} fw={'bold'} >{v.subjudul1}</Text>
{v.deskripsi1}
</Box>
<Box pr={'lg'}>
<Text fz={'h4'} fw={'bold'} >{v.subjudul2}</Text>
{v.deskripsi2}
<Text fz={'h4'} fw={'bold'} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Box>
</Paper>
)
@@ -132,6 +82,14 @@ function Page() {
</SimpleGrid>
</Stack>
</Box>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
my="md"
/>
</Center>
</Stack>
);
}

View File

@@ -1,26 +1,51 @@
'use client'
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors';
import { Stack, Box, Text, Paper, Flex, Group, SimpleGrid, Button, Image, Center } from '@mantine/core';
import React from 'react';
import { Box, Center, Grid, GridCol, Group, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { IconChevronDown } from '@tabler/icons-react';
const data = [
{
id: 1,
bulan: 'JANUARI 2025',
judul: 'Darmasaba Smart Waste',
deskripsi: 'Sistem manajemen sampah terpadu yang memudahkan warga untuk memilah dan mendaur ulang sampah.',
kolaborator: 'Kolaborator: DLH Kabupaten Badung, Komunitas Peduli Lingkungan, TPS3R Pudak Mesari'
},
{
id: 2,
bulan: 'FEBRUARI 2025',
judul: 'Darmasaba Digital Market',
deskripsi: 'Platform e-commerce untuk produk UMKM desa yang menghubungkan produsen lokal dengan pasar global.',
kolaborator: 'Kolaborator: Kementerian Desa PDTT, UMKM Darmasaba'
}
]
function Page() {
const state = useProxy(kolaborasiInovasiState)
const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<string | null>(null);
// Get unique years from the data
const years = Array.from(
new Set(
state.findMany.data?.map(item =>
new Date(item.createdAt).getFullYear().toString()
) || []
)
)
.sort((a, b) => b.localeCompare(a)) // Sort descending (newest first)
.map(year => ({
value: year,
label: year,
}));
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => {
load(page, 10, search, selectedYear || '')
}, [page, search, selectedYear])
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
return (
<>
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
@@ -28,27 +53,46 @@ function Page() {
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kolaborasi Inovasi
</Text>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kolaborasi Inovasi
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Kolaborasi Inovasi'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Group justify='flex-end'>
<Paper bg={colors['blue-button']} p={5} w={{ base: 150, md: 300 }}>
<Flex px={20} align={'center'} justify={'space-between'}>
<Text fz={'h4'} fw={'bold'} c={colors['white-1']}>Tahun</Text>
<IconChevronDown size={20} color={colors['white-1']} />
</Flex>
</Paper>
<Select
value={selectedYear}
onChange={setSelectedYear}
label={<Text fw={"bold"} fz={"sm"}>Tahun</Text>}
placeholder='Semua Tahun'
clearable
data={[
{ value: '', label: 'Semua Tahun' },
...years
]}
/>
</Group>
<SimpleGrid cols={{ base: 1, md: 2 }} spacing={'lg'}>
{data.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.name}</Text>
<Box pr={'lg'} pb={20}>
{v.deskripsi}
{v.slug}
</Box>
<Box pr={'lg'}>
{v.kolaborator}
@@ -57,9 +101,15 @@ function Page() {
);
})}
</SimpleGrid>
<Group py={40} justify='center'>
<Button px={80} rightSection={<IconChevronDown />} radius={6} bg={colors["blue-button"]} c={colors["white-1"]}>Lihat Laporan Lainnya</Button>
</Group>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Stack>
</Box>
</Stack>
@@ -72,7 +122,7 @@ function Page() {
<Text ta={'center'} fz={'h4'}>Kami berkolaborasi dengan berbagai mitra dari berbagai sektor untuk mewujudkan visi Smart Village Darmasaba.</Text>
</Box>
<Center>
<Image src={'/api/img/logoukm-kolaborasiinvoasi.png'} alt='' w={{base: 500, md: 650}}/>
<Image src={'/api/img/logoukm-kolaborasiinvoasi.png'} alt='' w={{ base: 500, md: 650 }} />
</Center>
</Stack>
</Box>

View File

@@ -2,7 +2,7 @@
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan';
import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -12,6 +12,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
const state = useProxy(keamananLingkunganState)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const {
data,
page,
@@ -21,8 +22,8 @@ function Page() {
} = state.findMany;
useShallowEffect(() => {
load(page, 3, search)
}, [page, search])
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
if (loading || !data) {
return (

View File

@@ -4,7 +4,7 @@ import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skele
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan';
import { useShallowEffect } from '@mantine/hooks';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { IconSearch } from '@tabler/icons-react';
@@ -12,6 +12,7 @@ import { IconSearch } from '@tabler/icons-react';
function Page() {
const state = useProxy(tipsKeamananState)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const {
data,
page,
@@ -21,8 +22,8 @@ function Page() {
} = state.findMany;
useShallowEffect(() => {
load(page, 3, search)
}, [page, search])
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
if (loading || !data) {
return (