Compare commits

..

10 Commits

301 changed files with 19371 additions and 1469 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -14,8 +14,10 @@
},
"dependencies": {
"@cubejs-client/core": "^0.31.0",
"@elysiajs/cookie": "^0.8.0",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/jwt": "^1.3.2",
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
@@ -44,8 +46,10 @@
"@types/lodash": "^4.17.16",
"add": "^2.0.6",
"animate.css": "^4.1.1",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
@@ -54,6 +58,7 @@
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
"jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
"list": "^2.0.19",
"lodash": "^4.17.21",
@@ -63,12 +68,15 @@
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"p-limit": "^6.2.0",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
"prisma": "^6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"swr": "^2.3.2",
@@ -78,15 +86,16 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"parcel": "^2.6.2",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5",
"parcel": "^2.6.2"
"typescript": "^5"
}
}

View File

@@ -0,0 +1,128 @@
[
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"id": "cmdsa8q5q001dvnbemch8j24x",
"name": "3.1 ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT",
"deskripsi": "<p>ADANYA LAYANAN PENGADUAN BAGI MASYARAKAT</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"id": "cmdsabhif001pvnbepm06hry6",
"name": "3.5 ADANYA MAKLUMAT PELAYANAN",
"deskripsi": "<p>ADANYA MAKLUMAT PELAYANAN</p>",
"kategoriId": "cmds9fr73000kvnbe6w281dcl",
"fileId": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
},
{
"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": ""
}
]

View File

@@ -0,0 +1,22 @@
[
{
"id": "cmds9es2o000ivnbe1o0swrvh",
"name": "PENGUATAN TATA LAKSANA"
},
{
"id": "cmds9f2ua000jvnbeksu1sfwm",
"name": "PENGUATAN PENGAWASAN"
},
{
"id": "cmds9fr73000kvnbe6w281dcl",
"name": "PENGUATAN KUALITAS PELAYANAN PUBLIK"
},
{
"id": "cmds9g5ow000lvnbel3rkkwrv",
"name": "PENGUATAN PARTISIPASI MASYARAKAT"
},
{
"id": "cmds9govb000mvnbesq8b4y99",
"name": "KEARIFAN LOKAL"
}
]

View File

@@ -1,8 +0,0 @@
[
{
"id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"position": "Perbekel Darmasaba periode 2021-2027"
}
]

View File

@@ -0,0 +1,32 @@
[
{
"id": "cmds8w2q60002vnbe6i8qhkuo",
"name": "Telephone Desa Darmasaba",
"iconUrl": "081239580000"
},
{
"id": "cmds8z7u20005vnbegyyvnbk0",
"name": "Email Desa Darmasaba",
"iconUrl": "desadarmasaba@badungkab.go.id"
},
{
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",
"iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg"
},
{
"id": "cmds90oul000bvnbe2bqkptoi",
"name": "Pemerintah Desa Darmasaba",
"iconUrl": "https://www.facebook.com/DarmasabaDesaku"
},
{
"id": "cmds91i4e000evnbe8gtf1gub",
"name": "ddarmasaba",
"iconUrl": "https://www.instagram.com/ddarmasaba/"
},
{
"id": "cmds92de5000hvnbemlu6sq5x",
"name": "desa.darmasaba",
"iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"position": "Perbekel Darmasaba periode 2021-2027"
}
]

View File

@@ -0,0 +1,50 @@
[
{
"id": "cmdr7039z0002vn5rttctt9hn",
"name": "Davest",
"description": "Darmasaba Village Festval",
"link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024"
},
{
"id": "cmdr755pf0005vn5rp8tyuubw",
"name": "Dmangan",
"description": "Darmasaba Aman Pangan",
"link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024"
},
{
"id": "cmdr76nqk0008vn5rdddvcxnr",
"name": "Bicara Darmasaba",
"description": "Bicara Darmasaba",
"link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba"
},
{
"id": "cmdr77vbw000bvn5rvpmoq31s",
"name": "Bares",
"description": "Darmasaba Recycling Stock/Exchange",
"link": "http://darmasaba.desa.id/berita/56722-bares"
},
{
"id": "cmdr7bxtp000evn5rmy85wihx",
"name": "Sajjana Dharma Raksaka",
"description": "Sajjana Dharma Raksaka",
"link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf"
},
{
"id": "cmdr7dlnk000hvn5r9lur3z35",
"name": "PDKT",
"description": "Perangkat Desa Kuat Teknologi",
"link": "https://darmasaba.desa.id/berita/53752-p-d-k-t"
},
{
"id": "cmdr7ftob000mvn5rfhgdtg8v",
"name": "GM",
"description": "Galah Melah",
"link": "https://darmasaba.desa.id/berita/52880-galah-melah"
},
{
"id": "cmdr7glue000pvn5r6onzslju",
"name": "Inovasi Desa Darmasaba",
"description": "Inovasi Desa Darmasaba",
"link": "https://darmasaba.desa.id/produk-lokal-desa"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Fasilitas yang Disediakan",
"deskripsi": "<ul><li><p>Buku-buku pelajaran dan alat tulis</p></li><li><p>Ruang belajar nyaman dan kondusif</p></li><li><p>Modul latihan dan pendampingan tugas</p></li><li><p>Minuman ringan dan dukungan motivasi belajar</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Lokasi dan Jadwal",
"deskripsi": "<ul><li><p>Lokasi: Balai Banjar / Balai Desa Darmasaba / Perpustakaan Desa</p></li><li><p>Jadwal: Setiap hari Senin, Rabu, dan Jumat pukul 16.0018.00 WITA</p></li><li><p>Peserta: Terbuka untuk semua siswa SDSMP di wilayah desa</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Program",
"deskripsi": "<ul><li><p>Memberikan pendampingan belajar secara gratis bagi siswa SD hingga SMP</p></li><li><p>Membantu siswa dalam menghadapi ujian dan menyelesaikan tugas sekolah</p></li><li><p>Menumbuhkan kepercayaan diri dan kemandirian dalam belajar</p></li><li><p>Meningkatkan kesetaraan pendidikan untuk seluruh anak desa</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tempat Kegiatan",
"deskripsi": "<p>Program Pendidikan Non Formal yang diselenggarakan di Desa Darmasaba meliputi:</p><p>1) Keaksaraan Fungsional</p><ul><li><p>Untuk warga yang belum bisa membaca dan menulis</p></li></ul><p>2) Pendidikan Kesetaraan (Paket A, B, C)</p><ul><li><p>Setara SD, SMP, dan SMA bagi yang tidak menyelesaikan pendidikan formal</p></li></ul><p>3) Pelatihan Keterampilan</p><ul><li><p>Menjahit, memasak, sablon, pertanian, peternakan, hingga teknologi digital</p></li></ul><p>4) Kursus &amp; Pelatihan Soft Skill</p><ul><li><p>Public speaking, pengelolaan keuangan, kepemimpinan pemuda</p></li></ul><p>5) Pendidikan Keluarga &amp; Parenting</p><ul><li><p>Untuk membekali orang tua dalam mendampingi tumbuh kembang anak</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tempat Kegiatan",
"deskripsi": "<ul><li><p>Balai Desa Darmasaba</p></li><li><p>TPK, Perpustakaan Desa, atau Posyandu</p></li><li><p>Bisa juga dilakukan secara mobile atau door to door</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Program",
"deskripsi": "<ul><li><p>Memberikan kesempatan belajar yang fleksibel bagi warga desa</p></li><li><p>Meningkatkan keterampilan hidup dan kemandirian ekonomi</p></li><li><p>Mendorong partisipasi masyarakat dalam pembangunan desa</p></li><li><p>Mengurangi angka putus sekolah dan meningkatkan kualitas SDM</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Program Unggulan",
"deskripsi": "<ul><li><p>Bimbingan Belajar Gratis: Untuk siswa kurang mampu</p></li><li><p>Gerakan Literasi Desa: Meningkatkan minat baca sejak dini</p></li><li><p>Pelatihan Digital untuk Anak dan Remaja</p></li><li><p>Beasiswa Anak Berprestasi &amp; Kurang Mampu</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Program",
"deskripsi": "<ul><li><p>Meningkatkan akses pendidikan yang merata dan berkualitas</p></li><li><p>Menumbuhkan semangat belajar sejak dini</p></li><li><p>Membentuk karakter anak yang berakhlak dan berwawasan lingkungan</p></li><li><p>Mendukung tumbuh kembang anak melalui pendekatan pendidikan yang holistik</p></li></ul>"
}
]

View File

@@ -0,0 +1,91 @@
[
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"namaLengkap": "Ida Bagus Surya Prabhawa Manuaba, S.H.,M.H., NL.P.",
"gelarAkademik": "S.H.,M.H.,NL.P.",
"tanggalMasuk": "2020-01-01T00:00:00.000Z",
"email": "bagus@desa.id",
"telepon": "081234567891",
"alamat": "Jl. Raya Desa No. 1",
"posisiId": "kepala_desa",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"namaLengkap": "I Ketut Suwanta",
"gelarAkademik": "S.Pt",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "suwanta@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "sekretaris_desa",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440006",
"namaLengkap": "Ni Wayan Supardiati",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "supardiati@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kaur_keuangan",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440011",
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "agus@desa.id",
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_menesa",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440012",
"namaLengkap": "I Wayan Sueca",
"gelarAkademik": "S.H.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "sueca@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_darmasaba",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440017",
"namaLengkap": "Si Gede Ketut Astawa",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "astawa@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_bucu",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440018",
"namaLengkap": "I Kadek Arya Minarta",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "minarta@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_gulingan",
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440021",
"namaLengkap": "I Gede Andika Pradnya Diputra",
"gelarAkademik": "S.E.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
"email": "diputra@desa.id",
"telepon": "081234567893",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "kadus_banjar_dinas_taman",
"isActive": true
}
]

View File

@@ -0,0 +1,158 @@
[
[
{
"id": "kepala_desa",
"nama": "Kepala Desa",
"deskripsi": "Pemimpin desa Darmasaba",
"hierarki": 1,
"parentId": null
},
{
"id": "kepala_urusan",
"nama": "Kepala Urusan",
"deskripsi": "Pemimpin urusan desa Darmasaba",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "sekretaris_desa",
"nama": "Sekretaris Desa",
"deskripsi": "Pengelola administrasi desa",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kaur_keuangan",
"nama": "Kaur Keuangan",
"deskripsi": "Pengelola keuangan desa",
"hierarki": 3,
"parentId": "kaur_umum"
},
{
"id": "kaur_perencanaan",
"nama": "Kaur Perencanaan",
"deskripsi": "Penyusun program kerja desa",
"hierarki": 3,
"parentId": "kaur_umum"
},
{
"id": "kaur_umum",
"nama": "Kaur Umum & TU",
"deskripsi": "Pelayanan umum dan administrasi",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kasi_pemerintahan",
"nama": "Kasi Pemerintahan",
"deskripsi": "Urusan pemerintahan dan keamanan",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kasi_pelayanan",
"nama": "Kasi Pelayanan",
"deskripsi": "Urusan pelayanan masyarakat",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kasi_kesejahteraan",
"nama": "Kasi Kesejahteraan",
"deskripsi": "Urusan sosial dan kesejahteraan",
"hierarki": 2,
"parentId": "kepala_desa"
},
{
"id": "kadus_banjar_dinas_cabe",
"nama": "Kepala Dusun Banjar Dinas Cabe",
"deskripsi": "Pimpinan wilayah Banjar Dinas Cabe",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_menesa",
"nama": "Kepala Dusun Banjar Dinas Menesa",
"deskripsi": "Pimpinan wilayah Banjar Menesa",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_penenjoan",
"nama": "Kepala Dusun Banjar Dinas Penenjoan",
"deskripsi": "Pimpinan wilayah Banjar Dinas Penenjoan",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_telanga",
"nama": "Kepala Dusun Banjar Dinas Telanga",
"deskripsi": "Pimpinan wilayah Banjar Dinas Telanga",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_tengah",
"nama": "Kepala Dusun Banjar Dinas Tengah",
"deskripsi": "Pimpinan wilayah Banjar Dinas Tengah",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_baler_pasar",
"nama": "Kepala Dusun Banjar Dinas Baler Pasar",
"deskripsi": "Pimpinan wilayah Banjar Dinas Baler Pasar",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_bucu",
"nama": "Kepala Dusun Banjar Dinas Bucu",
"deskripsi": "Pimpinan wilayah Banjar Dinas Bucu",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_gulingan",
"nama": "Kepala Dusun Banjar Dinas Gulingan",
"deskripsi": "Pimpinan wilayah Banjar Dinas Gulingan",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_bersih",
"nama": "Kepala Dusun Banjar Dinas Bersih",
"deskripsi": "Pimpinan wilayah Banjar Dinas Bersih",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_umahanyar",
"nama": "Kepala Dusun Banjar Dinas Umahanyar",
"deskripsi": "Pimpinan wilayah Banjar Dinas Umahanyar",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_taman",
"nama": "Kepala Dusun Banjar Dinas Taman",
"deskripsi": "Pimpinan wilayah Banjar Dinas Taman",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "kadus_banjar_dinas_darmasaba",
"nama": "Kepala Dusun Banjar Dinas Darmasaba",
"deskripsi": "Pimpinan wilayah Banjar Dinas Darmasaba",
"hierarki": 3,
"parentId": "sekretaris_desa"
},
{
"id": "staf_desa",
"nama": "Staf Desa",
"deskripsi": "Staf Desa",
"hierarki": 3,
"parentId": "sekretaris_desa"
}
]
]

View File

@@ -1,6 +0,0 @@
[
{
"id" : "1",
"name" : "Struktur PPID"
}
]

View File

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

View File

@@ -95,8 +95,11 @@ model FileStorage {
SDGSDesa SDGSDesa[]
APBDesImage APBDes[] @relation("APBDesImage")
APBDesFile APBDes[] @relation("APBDesFile")
PrestasiDesa PrestasiDesa[]
PrestasiDesa PrestasiDesa[]
DataPerpustakaan DataPerpustakaan[]
PegawaiPPID PegawaiPPID[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -129,8 +132,8 @@ model ProgramInovasi {
model MediaSosial {
id String @id @default(cuid())
name String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
iconUrl String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -145,8 +148,8 @@ model DesaAntiKorupsi {
deskripsi String @db.Text
kategori KategoriDesaAntiKorupsi @relation(fields: [kategoriId], references: [id])
kategoriId String
file FileStorage @relation(fields: [fileId], references: [id])
fileId String
file FileStorage? @relation(fields: [fileId], references: [id])
fileId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -216,18 +219,140 @@ model KategoriPrestasiDesa {
PrestasiDesa PrestasiDesa[]
}
//========================================= INDEKS KEPUASAN MASYARAKAT ========================================= //
// Entitas Survey
model Survey {
id String @id @default(cuid())
title String // Judul survei
totalRespondents Int // Total jumlah responden
averageScore Float // Rata-rata skor
monthlyStats MonthlyStat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// Entitas Statistik Bulanan
model MonthlyStat {
id String @id @default(cuid())
month String // Nama bulan (e.g., "Januari", "Februari")
respondentsCount Int // Jumlah responden per bulan
surveyId String @unique(map: "monthly_stat_survey_id_month_key")
survey Survey @relation(fields: [surveyId], references: [id])
AgeStat AgeStat[]
ResponseStat ResponseStat[]
genderStat genderStat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// Entitas Gender
model genderStat {
id String @id @default(cuid())
laki Int
perempuan Int
percentLaki Float
percentPerempuan Float
total Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
// Entitas Age
model AgeStat {
id String @id @default(cuid())
group String // "18-44", "45+" dll
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
// Entitas Response
model ResponseStat {
id String @id @default(cuid())
label String // BAIK / BURUK
count Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id])
monthlyStatId String?
}
//========================================= MENU PPID ========================================= //
//========================================= STRUKTUR PPID ========================================= //
model StrukturPPID {
id String @id @default(cuid())
name String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
PosisiOrganisasiPPID PosisiOrganisasiPPID? @relation(fields: [posisiOrganisasiPPIDId], references: [id])
posisiOrganisasiPPIDId String?
PegawaiPPID PegawaiPPID? @relation(fields: [pegawaiPPIDId], references: [id])
pegawaiPPIDId String?
}
model PosisiOrganisasiPPID {
id String @id @default(cuid())
nama String @db.VarChar(100)
deskripsi String? @db.Text
hierarki Int
pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String?
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent")
}
model PegawaiPPID {
id String @id @default(cuid())
namaLengkap String @db.VarChar(255)
gelarAkademik String? @db.VarChar(100)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
tanggalMasuk DateTime? @db.Date
email String? @unique @db.VarChar(255)
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
strukturOrganisasi StrukturPPID[] // Relasi balik
}
model StrukturOrganisasiPPID {
id String @id @default(uuid())
posisiOrganisasiId String @db.VarChar(50)
pegawaiId String @db.Uuid
hubunganOrganisasiId String @db.Uuid
posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id])
pegawai Pegawai @relation(fields: [pegawaiId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
}
// ========================================= VISI MISI PPID ========================================= //
@@ -1240,8 +1365,9 @@ model PosisiOrganisasi {
deskripsi String? @db.Text
hierarki Int
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@map("posisi_organisasi")
}
@@ -1266,7 +1392,8 @@ model Pegawai {
sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
@@map("pegawai")
}
@@ -1717,3 +1844,252 @@ model NilaiKonservasiAdat {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU PENDIDIKAN ========================================= //
// ========================================= INFO SEKOLAH & PAUD ========================================= //
model JenjangPendidikan {
id String @id @default(cuid())
nama String // TK/PAUD, SD, SMP, SMA/SMK
lembagas Lembaga[] // Relasi ke lembaga
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Lembaga {
id String @id @default(cuid())
nama String
jenjangPendidikan JenjangPendidikan @relation(fields: [jenjangId], references: [id])
jenjangId String
siswa Siswa[]
pengajar Pengajar[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Siswa {
id String @id @default(cuid())
nama String
lembaga Lembaga @relation(fields: [lembagaId], references: [id])
lembagaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Pengajar {
id String @id @default(cuid())
nama String
lembaga Lembaga @relation(fields: [lembagaId], references: [id])
lembagaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BEASISWA DESA ========================================= //
model KeunggulanProgram {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model BeasiswaPendaftar {
id String @id @default(cuid())
namaLengkap String
nik String @unique
tempatLahir String
tanggalLahir DateTime
jenisKelamin JenisKelamin
kewarganegaraan String
agama Agama
alamatKTP String
alamatDomisili String?
noHp String
email String @unique
statusPernikahan StatusPernikahan
ukuranBaju UkuranBaju?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum JenisKelamin {
LAKI_LAKI
PEREMPUAN
}
enum Agama {
ISLAM
KRISTEN_PROTESTAN
KRISTEN_KATOLIK
HINDU
BUDDHA
KONGHUCU
LAINNYA
}
enum StatusPernikahan {
BELUM_MENIKAH
MENIKAH
JANDA_DUDA
}
enum UkuranBaju {
S
M
L
XL
XXL
LAINNYA
}
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
model TujuanProgram {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramUnggulan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BIMBINGAN BELAJAR DESA ========================================= //
model TujuanBimbinganBelajarDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model LokasiJadwalBimbinganBelajarDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model FasilitasBimbinganBelajarDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENDIDIKAN NON FORMAL ========================================= //
model TujuanPendidikanNonFormal {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model TempatKegiatan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisProgramYangDiselenggarakan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PERPUSTAKAAN ========================================= //
model DataPerpustakaan {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
kategoriId 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 KategoriBuku {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
DataPerpustakaan DataPerpustakaan[]
}
model User {
id String @id @default(cuid())
nama String
email String @unique
password String
role Role @relation(fields: [roleId], references: [id])
roleId String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Role {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
User User[]
}
// ========================================= DATA PENDIDIKAN ========================================= //
model DataPendidikan {
id String @id @default(cuid())
name String
jumlah String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -1,4 +1,7 @@
import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json";
import programInovasi from "./data/landing-page/profile/programInovasi.json";
import mediaSosial from "./data/landing-page/profile/mediaSosial.json";
import categoryPengumuman from "./data/category-pengumuman.json";
import kategoriBerita from "./data/kategori-berita.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
@@ -9,7 +12,6 @@ import potensi from "./data/list-potensi.json";
import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json";
import profilePPID from "./data/ppid/profile-ppid/profilePPid.json";
import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
@@ -21,16 +23,82 @@ 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 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 bentukKonservasiBerdasarkanAdat from './data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json';
import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json';
import profilePejabatDesa from './data/landing-page/profile.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 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 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 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";
(async () => {
// =========== LANDING PAGE ===========
// =========== PROFILE ===========
for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
position: p.position,
},
create: {
id: p.id,
name: p.name,
position: p.position,
},
});
}
console.log(
"✅ profilePejabatDesa seeded without imageId (editable later via UI)"
);
// =========== PROGRAM INOVASI ===========
for (const p of programInovasi) {
await prisma.programInovasi.upsert({
where: { id: p.id },
update: {
name: p.name,
description: p.description,
link: p.link,
},
create: {
id: p.id,
name: p.name,
description: p.description,
link: p.link,
},
});
}
console.log("program inovasi success ...");
// =========== MEDIA SOSIAL ===========
for (const p of mediaSosial) {
await prisma.mediaSosial.upsert({
where: { id: p.id },
update: {
name: p.name,
iconUrl: p.iconUrl,
},
create: {
id: p.id,
name: p.name,
iconUrl: p.iconUrl,
},
});
}
console.log("media sosial success ...");
// =========== LAYANAN ===========
for (const l of layanan) {
await prisma.layanan.upsert({
where: {
@@ -124,7 +192,9 @@ import profilePejabatDesa from './data/landing-page/profile.json';
},
});
}
console.log("✅ profilePerbekel seeded without imageId (editable later via UI)");
console.log(
"✅ profilePerbekel seeded without imageId (editable later via UI)"
);
for (const l of visiMisiDesa) {
await prisma.visiMisiDesa.upsert({
@@ -145,6 +215,64 @@ import profilePejabatDesa from './data/landing-page/profile.json';
console.log("visi misi desa success ...");
// Flatten the nested array structure for posisiOrganisasiPPID
const flattenedPosisiOrganisasiPPID = posisiOrganisasiPPID.flat();
for (const p of flattenedPosisiOrganisasiPPID) {
await prisma.posisiOrganisasiPPID.upsert({
where: {
id: p.id,
},
update: {
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
parentId: p.parentId,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
hierarki: p.hierarki,
parentId: p.parentId,
},
});
}
console.log("posisi organisasi success ...");
// Flatten the nested array structure for pegawaiPPID
const flattenedPegawaiPPID = pegawaiPPID.flat();
for (const p of flattenedPegawaiPPID) {
await prisma.pegawaiPPID.upsert({
where: {
id: p.id,
},
update: {
namaLengkap: p.namaLengkap,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
create: {
id: p.id,
namaLengkap: p.namaLengkap,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
gelarAkademik: p.gelarAkademik,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
});
}
console.log("pegawai success ...");
for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({
where: {
@@ -184,22 +312,6 @@ import profilePejabatDesa from './data/landing-page/profile.json';
}
console.log("pelayanan penduduk non permanen success ...");
for (const s of strukturPPID) {
await prisma.strukturPPID.upsert({
where: {
id: s.id,
},
update: {
name: s.name,
},
create: {
id: s.id,
name: s.name,
},
});
}
console.log("struktur ppid success ...");
for (const p of potensi) {
await prisma.potensi.upsert({
where: {
@@ -369,7 +481,6 @@ import profilePejabatDesa from './data/landing-page/profile.json';
}
console.log("kategori produk success ...");
for (const p of posisiOrganisasi) {
await prisma.posisiOrganisasi.upsert({
where: {
@@ -558,7 +669,6 @@ import profilePejabatDesa from './data/landing-page/profile.json';
console.log("bentuk konservasi berdasarkan adat success ...");
for (const n of nilaiKonservasiAdat) {
await prisma.nilaiKonservasiAdat.upsert({
where: {
@@ -578,22 +688,145 @@ import profilePejabatDesa from './data/landing-page/profile.json';
console.log("nilai konservasi adat success ...");
for (const p of profilePejabatDesa) {
await prisma.pejabatDesa.upsert({
where: { id: p.id },
for (const t of tujuanProgram) {
await prisma.tujuanProgram.upsert({
where: { id: t.id },
update: {
name: p.name,
position: p.position,
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: p.id,
name: p.name,
position: p.position,
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ profilePejabatDesa seeded without imageId (editable later via UI)");
console.log("✅ tujuan program seeded (editable later via UI)");
for (const t of programUnggulan) {
await prisma.programUnggulan.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log("✅ program unggulan seeded (editable later via UI)");
for (const t of tujuanBimbinganBelajarDesa) {
await prisma.tujuanBimbinganBelajarDesa.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log(
"✅ tujuan bimbingan belajar desa seeded (editable later via UI)"
);
for (const t of lokasiJadwalBimbinganBelajarDesa) {
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log(
"✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)"
);
for (const t of fasilitasBimbinganBelajarDesa) {
await prisma.fasilitasBimbinganBelajarDesa.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
for (const t of tujuanProgram2) {
await prisma.tujuanPendidikanNonFormal.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
for (const t of tempatKegiatan) {
await prisma.tempatKegiatan.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
for (const t of jenisProgramYangDiselenggarakan) {
await prisma.jenisProgramYangDiselenggarakan.upsert({
where: { id: t.id },
update: {
judul: t.judul,
deskripsi: t.deskripsi,
},
create: {
id: t.id,
judul: t.judul,
deskripsi: t.deskripsi,
},
});
}
console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -5,176 +5,190 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateJumlahPendudukMiskin = z.object({
year: z.number().min(1, "Data tahun harus diisi"),
totalPoorPopulation: z.number().min(1, "Data total penduduk miskin harus diisi"),
year: z.number().min(1, "Data tahun harus diisi"),
totalPoorPopulation: z
.number()
.min(1, "Data total penduduk miskin harus diisi"),
});
type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: {
id: true;
year: true;
totalPoorPopulation: true;
};
select: {
id: true;
year: true;
totalPoorPopulation: true;
};
}>;
const defaultForm: Omit<JumlahPendudukMiskin, 'id'> & { id?: string } = {
year: 0,
totalPoorPopulation: 0,
const defaultForm: Omit<JumlahPendudukMiskin, "id"> & { id?: string } = {
year: 0,
totalPoorPopulation: 0,
};
const jumlahPendudukMiskin = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateJumlahPendudukMiskin.safeParse(
jumlahPendudukMiskin.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jumlahPendudukMiskin.create.loading = true;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"create"
].post(jumlahPendudukMiskin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
};
jumlahPendudukMiskin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
jumlahPendudukMiskin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true; };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true; };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`
);
if (res.ok) {
const data = await res.json();
jumlahPendudukMiskin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jumlahPendudukMiskin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik jumlah penduduk miskin:", error);
jumlahPendudukMiskin.findUnique.data = null;
}
},
},
update: {
id: "",
form: {...defaultForm},
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJumlahPendudukMiskin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => (v.path as string[]).join(".")).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPendudukMiskin.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data grafik jumlah penduduk miskin:", error);
toast.error("Gagal update data grafik jumlah penduduk miskin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPendudukMiskin.delete.loading = true;
const response = await fetch(`/api/ekonomi/jumlahpendudukmiskin/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik jumlah penduduk miskin berhasil dihapus");
await jumlahPendudukMiskin.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik jumlah penduduk miskin");
}
} catch (error) {
console.error("Gagal delete grafik jumlah penduduk miskin:", error);
toast.error("Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin");
} finally {
jumlahPendudukMiskin.delete.loading = false;
}
},
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateJumlahPendudukMiskin.safeParse(
jumlahPendudukMiskin.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
})
export default jumlahPendudukMiskin
try {
jumlahPendudukMiskin.create.loading = true;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"create"
].post(jumlahPendudukMiskin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
};
jumlahPendudukMiskin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
jumlahPendudukMiskin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/jumlahpendudukmiskin/${id}`);
if (res.ok) {
const data = await res.json();
jumlahPendudukMiskin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jumlahPendudukMiskin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik jumlah penduduk miskin:", error);
jumlahPendudukMiskin.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJumlahPendudukMiskin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => (v.path as string[]).join("."))
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPendudukMiskin.findMany.load();
return result.data;
} catch (error) {
console.error(
"Error update data grafik jumlah penduduk miskin:",
error
);
toast.error("Gagal update data grafik jumlah penduduk miskin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPendudukMiskin.delete.loading = true;
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Grafik jumlah penduduk miskin berhasil dihapus"
);
await jumlahPendudukMiskin.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus grafik jumlah penduduk miskin"
);
}
} catch (error) {
console.error("Gagal delete grafik jumlah penduduk miskin:", error);
toast.error(
"Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin"
);
} finally {
jumlahPendudukMiskin.delete.loading = false;
}
},
},
});
export default jumlahPendudukMiskin;

View File

@@ -23,9 +23,7 @@ const apbdes = proxy({
form: { ...defaultapbdesForm },
loading: false,
async create() {
const cek = templateapbDesaForm.safeParse(
apbdes.create.form
);
const cek = templateapbDesaForm.safeParse(apbdes.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -34,9 +32,7 @@ const apbdes = proxy({
}
try {
apbdes.create.loading = true;
const res = await ApiFetch.api.landingpage.apbdes[
"create"
].post({
const res = await ApiFetch.api.landingpage.apbdes["create"].post({
...apbdes.create.form,
});
@@ -63,9 +59,7 @@ const apbdes = proxy({
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.apbdes[
"find-many"
].get();
const res = await ApiFetch.api.landingpage.apbdes["find-many"].get();
if (res.status === 200) {
apbdes.findMany.data = res.data?.data ?? [];
}
@@ -102,15 +96,12 @@ const apbdes = proxy({
try {
apbdes.delete.loading = true;
const response = await fetch(
`/api/landingpage/apbdes/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const response = await fetch(`/api/landingpage/apbdes/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
@@ -177,9 +168,7 @@ const apbdes = proxy({
},
async update() {
const cek = templateapbDesaForm.safeParse(
apbdes.edit.form
);
const cek = templateapbDesaForm.safeParse(apbdes.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
@@ -189,21 +178,18 @@ const apbdes = proxy({
try {
apbdes.edit.loading = true;
const response = await fetch(
`/api/landingpage/apbdes/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
jumlah: this.form.jumlah,
imageId: this.form.imageId,
fileId: this.form.fileId,
}),
}
);
const response = await fetch(`/api/landingpage/apbdes/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
jumlah: this.form.jumlah,
imageId: this.form.imageId,
fileId: this.form.fileId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
@@ -216,16 +202,12 @@ const apbdes = proxy({
await apbdes.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate apbdes"
);
throw new Error(result.message || "Gagal mengupdate apbdes");
}
} catch (error) {
console.error("Error updating apbdes:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate apbdes"
error instanceof Error ? error.message : "Gagal mengupdate apbdes"
);
return false;
} finally {
@@ -239,4 +221,4 @@ const apbdes = proxy({
},
});
export default apbdes;
export default apbdes;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -54,20 +55,38 @@ const desaAntikorupsi = proxy({
},
},
findMany: {
data: null as Array<
Prisma.DesaAntiKorupsiGetPayload<{
include: {
file: true;
kategori: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.landingpage.desaantikorupsi[
"find-many"
].get();
if (res.status === 200) {
desaAntikorupsi.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
desaAntikorupsi.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.desaantikorupsi[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
desaAntikorupsi.findMany.data = res.data.data || [];
desaAntikorupsi.findMany.total = res.data.total || 0;
desaAntikorupsi.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load media sosial:", res.data?.message);
desaAntikorupsi.findMany.data = [];
desaAntikorupsi.findMany.total = 0;
desaAntikorupsi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading media sosial:", error);
desaAntikorupsi.findMany.data = [];
desaAntikorupsi.findMany.total = 0;
desaAntikorupsi.findMany.totalPages = 1;
} finally {
desaAntikorupsi.findMany.loading = false;
}
},
},
@@ -281,14 +300,38 @@ const kategoriDesaAntiKorupsi = proxy({
},
},
findMany: {
data: null as Array<{
id: string;
name: string;
}> | null,
async load() {
const res = await ApiFetch.api.landingpage.kategoridak["find-many"].get();
if (res.status === 200) {
kategoriDesaAntiKorupsi.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
kategoriDesaAntiKorupsi.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.kategoridak[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
kategoriDesaAntiKorupsi.findMany.data = res.data.data || [];
kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0;
kategoriDesaAntiKorupsi.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load media sosial:", res.data?.message);
kategoriDesaAntiKorupsi.findMany.data = [];
kategoriDesaAntiKorupsi.findMany.total = 0;
kategoriDesaAntiKorupsi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading media sosial:", error);
kategoriDesaAntiKorupsi.findMany.data = [];
kategoriDesaAntiKorupsi.findMany.total = 0;
kategoriDesaAntiKorupsi.findMany.totalPages = 1;
} finally {
kategoriDesaAntiKorupsi.findMany.loading = false;
}
},
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -5,10 +6,10 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateProgramInovasi = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
description: z.string().min(3, "Deskripsi minimal 3 karakter"),
name: z.string().min(1, "Nama minimal 1 karakter"),
description: z.string().min(1, "Deskripsi minimal 1 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
link: z.string().min(3, "Link minimal 3 karakter"),
link: z.string().min(1, "Link minimal 1 karakter"),
});
type ProgramInovasiForm = Prisma.ProgramInovasiGetPayload<{
@@ -59,15 +60,38 @@ const programInovasi = proxy({
},
},
findMany: {
data: null as
| Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.landingpage.programinovasi[
"find-many"
].get();
if (res.status === 200) {
programInovasi.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
programInovasi.findMany.loading = true; // Use the full path to access the property
programInovasi.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.programinovasi[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
programInovasi.findMany.data = res.data.data || [];
programInovasi.findMany.total = res.data.total || 0;
programInovasi.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
programInovasi.findMany.data = [];
programInovasi.findMany.total = 0;
programInovasi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
programInovasi.findMany.data = [];
programInovasi.findMany.total = 0;
programInovasi.findMany.totalPages = 1;
} finally {
programInovasi.findMany.loading = false;
}
},
},
@@ -453,13 +477,38 @@ const mediaSosial = proxy({
},
},
findMany: {
data: null as
| Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.landingpage.mediasosial["find-many"].get();
if (res.status === 200) {
mediaSosial.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
mediaSosial.findMany.loading = true; // Use the full path to access the property
mediaSosial.findMany.page = page;
try {
const res = await ApiFetch.api.landingpage.mediasosial[
"findMany"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
mediaSosial.findMany.data = res.data.data || [];
mediaSosial.findMany.total = res.data.total || 0;
mediaSosial.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load media sosial:", res.data?.message);
mediaSosial.findMany.data = [];
mediaSosial.findMany.total = 0;
mediaSosial.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading media sosial:", error);
mediaSosial.findMany.data = [];
mediaSosial.findMany.total = 0;
mediaSosial.findMany.totalPages = 1;
} finally {
mediaSosial.findMany.loading = false;
}
},
},

View File

@@ -0,0 +1,282 @@
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 templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"),
nik: z.string().min(1, "NIK harus diisi"),
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
kewarganegaraan: z.string().min(1, "Kewarganegaraan harus diisi"),
agama: z.string().min(1, "Agama harus diisi"),
alamatKTP: z.string().min(1, "Alamat KTP harus diisi"),
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
noHp: z.string().min(1, "No HP harus diisi"),
email: z.string().min(1, "Email harus diisi"),
statusPernikahan: z.string().min(1, "Status pernikahan harus diisi"),
ukuranBaju: z.string().min(1, "Ukuran baju harus diisi"),
});
const defaultBeasiswaPendaftar = {
namaLengkap: "",
nik: "",
tempatLahir: "",
tanggalLahir: "",
jenisKelamin: "",
kewarganegaraan: "",
agama: "",
alamatKTP: "",
alamatDomisili: "",
noHp: "",
email: "",
statusPernikahan: "",
ukuranBaju: "",
};
const beasiswaPendaftar = proxy({
create: {
form: { ...defaultBeasiswaPendaftar },
loading: false,
async create() {
const cek = templateBeasiswaPendaftar.safeParse(
beasiswaPendaftar.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
beasiswaPendaftar.create.loading = true;
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"create"
].post(beasiswaPendaftar.create.form);
if (res.status === 200) {
beasiswaPendaftar.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
beasiswaPendaftar.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.BeasiswaPendaftarGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"findMany"
].get();
if (res.status === 200) {
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.BeasiswaPendaftarGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${id}`
);
if (res.ok) {
const data = await res.json();
beasiswaPendaftar.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
beasiswaPendaftar.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
beasiswaPendaftar.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
beasiswaPendaftar.delete.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Beasiswa berhasil dihapus");
await beasiswaPendaftar.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus beasiswa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus beasiswa");
} finally {
beasiswaPendaftar.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultBeasiswaPendaftar },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${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 = {
namaLengkap: data.namaLengkap,
nik: data.nik,
tempatLahir: data.tempatLahir,
tanggalLahir: data.tanggalLahir,
jenisKelamin: data.jenisKelamin,
kewarganegaraan: data.kewarganegaraan,
agama: data.agama,
alamatKTP: data.alamatKTP,
alamatDomisili: data.alamatDomisili,
noHp: data.noHp,
email: data.email,
statusPernikahan: data.statusPernikahan,
ukuranBaju: data.ukuranBaju,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading beasiswa pendaftar:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateBeasiswaPendaftar.safeParse(
beasiswaPendaftar.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
beasiswaPendaftar.update.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
namaLengkap: this.form.namaLengkap,
nik: this.form.nik,
tanggalLahir: this.form.tanggalLahir,
jenisKelamin: this.form.jenisKelamin,
kewarganegaraan: this.form.kewarganegaraan,
agama: this.form.agama,
alamatKTP: this.form.alamatKTP,
alamatDomisili: this.form.alamatDomisili,
noHp: this.form.noHp,
email: this.form.email,
statusPernikahan: this.form.statusPernikahan,
ukuranBaju: this.form.ukuranBaju,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update beasiswa pendaftar");
await beasiswaPendaftar.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update beasiswa pendaftar");
}
} catch (error) {
console.error("Error updating beasiswa pendaftar:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update beasiswa pendaftar"
);
return false;
} finally {
beasiswaPendaftar.update.loading = false;
}
},
reset() {
beasiswaPendaftar.update.id = "";
beasiswaPendaftar.update.form = { ...defaultBeasiswaPendaftar };
},
},
});
const beasiswaDesaState = proxy({
beasiswaPendaftar,
});
export default beasiswaDesaState;

View File

@@ -0,0 +1,260 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PROGRAM ========================================= //
const templateTujuanProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanProgramForm = Prisma.TujuanBimbinganBelajarDesaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanProgram = proxy({
findById: {
data: null as TujuanProgramForm | null,
loading: false,
initialize() {
stateTujuanProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanProgramForm;
},
async load(id: string) {
try {
stateTujuanProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.tujuanprogram[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tujuan program");
} finally {
stateTujuanProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanProgramForm) {
const cek = templateTujuanProgramForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTujuanProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.tujuanprogram[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan program berhasil diubah");
await stateTujuanProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tujuan program");
} finally {
stateTujuanProgram.update.loading = false;
}
},
},
});
// ========================================= LOKASI DAN JADWAL ========================================= //
const templateLokasiDanJadwalForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type LokasiDanJadwalForm = Prisma.LokasiJadwalBimbinganBelajarDesaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const lokasiDanJadwalState = proxy({
findById: {
data: null as LokasiDanJadwalForm | null,
loading: false,
initialize() {
lokasiDanJadwalState.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as LokasiDanJadwalForm;
},
async load(id: string) {
try {
lokasiDanJadwalState.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.lokasidanjadwal[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
lokasiDanJadwalState.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data lokasi dan jadwal");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data lokasi dan jadwal");
} finally {
lokasiDanJadwalState.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: LokasiDanJadwalForm) {
const cek = templateLokasiDanJadwalForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
lokasiDanJadwalState.update.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.lokasidanjadwal[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data lokasi dan jadwal berhasil diubah");
await lokasiDanJadwalState.findById.load(data.id);
} else {
toast.error("Gagal mengubah data lokasi dan jadwal");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data lokasi dan jadwal");
} finally {
lokasiDanJadwalState.update.loading = false;
}
},
},
});
// ========================================= FASILITAS YANG DISEDIAKAN ========================================= //
const templateFasilitasYangDisediakanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type FasilitasYangDisediakanForm = Prisma.FasilitasBimbinganBelajarDesaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const fasilitasYangDisediakanState = proxy({
findById: {
data: null as FasilitasYangDisediakanForm | null,
loading: false,
initialize() {
fasilitasYangDisediakanState.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as FasilitasYangDisediakanForm;
},
async load(id: string) {
try {
fasilitasYangDisediakanState.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.fasilitasyangdisediakan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
fasilitasYangDisediakanState.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data fasilitas yang disediakan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data fasilitas yang disediakan");
} finally {
fasilitasYangDisediakanState.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: FasilitasYangDisediakanForm) {
const cek = templateFasilitasYangDisediakanForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
fasilitasYangDisediakanState.update.loading = true;
const res =
await ApiFetch.api.pendidikan.bimbinganbelajardesa.fasilitasyangdisediakan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data fasilitas yang disediakan berhasil diubah");
await fasilitasYangDisediakanState.findById.load(data.id);
} else {
toast.error("Gagal mengubah data fasilitas yang disediakan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data fasilitas yang disediakan");
} finally {
fasilitasYangDisediakanState.update.loading = false;
}
},
},
});
const stateBimbinganBelajarDesa = proxy({
stateTujuanProgram,
lokasiDanJadwalState,
fasilitasYangDisediakanState,
});
export default stateBimbinganBelajarDesa;

View File

@@ -0,0 +1,178 @@
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 templateDataPendidikan = z.object({
name: z.string().min(1, "Data nama harus diisi"),
jumlah: z.string().min(1, "Data jumlah harus diisi"),
});
type DataPendidikan = Prisma.DataPendidikanGetPayload<{
select: {
id: true;
name: true;
jumlah: true;
};
}>;
const defaultForm: Omit<DataPendidikan, "id"> & { id?: string } = {
name: "",
jumlah: "",
};
const dataPendidikan = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateDataPendidikan.safeParse(dataPendidikan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
dataPendidikan.create.loading = true;
const res = await ApiFetch.api.pendidikan.datapendidikan["create"].post(
dataPendidikan.create.form
);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
dataPendidikan.create.form = {
name: "",
jumlah: "",
};
dataPendidikan.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
dataPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DataPendidikanGetPayload<{
select: { id: true; name: true; jumlah: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.datapendidikan[
"findMany"
].get();
if (res.status === 200) {
dataPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DataPendidikanGetPayload<{
select: { id: true; name: true; jumlah: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/datapendidikan/${id}`);
if (res.ok) {
const data = await res.json();
dataPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
dataPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error loading data pendidikan:", error);
dataPendidikan.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateDataPendidikan.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => (v.path as string[]).join("."))
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/pendidikan/datapendidikan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await dataPendidikan.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data data pendidikan:", error);
toast.error("Gagal update data data pendidikan");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/datapendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data berhasil dihapus");
await dataPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus data");
}
} catch (error) {
console.error("Gagal delete data pendidikan:", error);
toast.error("Terjadi kesalahan saat menghapus data pendidikan");
} finally {
dataPendidikan.delete.loading = false;
}
},
},
});
export default dataPendidikan;

View File

@@ -0,0 +1,998 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= JENJANG PENDIDIKAN ========================================= //
const jenjangPendidikanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const jenjangPendidikanDefaultForm = {
nama: "",
};
const jenjangPendidikan = proxy({
create: {
form: { ...jenjangPendidikanDefaultForm },
loading: false,
async create() {
const cek = jenjangPendidikanForm.safeParse(
jenjangPendidikan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jenjangPendidikan.create.loading = true;
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
"create"
].post(jenjangPendidikan.create.form);
if (res.status === 200) {
jenjangPendidikan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
jenjangPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
"find-many"
].get();
if (res.status === 200) {
jenjangPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.JenjangPendidikanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/${id}`
);
if (res.ok) {
const data = await res.json();
jenjangPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenjangPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
jenjangPendidikan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenjangPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "jenjang pendidikan berhasil dihapus"
);
await jenjangPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus jenjang pendidikan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenjang pendidikan");
} finally {
jenjangPendidikan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...jenjangPendidikanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading jenjang pendidikan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = jenjangPendidikanForm.safeParse(jenjangPendidikan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
jenjangPendidikan.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/jenjangpendidikan/${jenjangPendidikan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: jenjangPendidikan.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate jenjang pendidikan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui jenjang pendidikan"
);
await jenjangPendidikan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate jenjang pendidikan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating jenjang pendidikan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate jenjang pendidikan"
);
return false;
} finally {
jenjangPendidikan.edit.loading = false;
}
},
reset() {
jenjangPendidikan.edit.id = "";
jenjangPendidikan.edit.form = { ...jenjangPendidikanDefaultForm };
},
},
});
// ========================================= LEMBAGA PENDIDIKAN ========================================= //
const lembagaPendidikanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
jenjangId: z.string().min(1, "Jenjang pendidikan minimal 1"),
});
const lembagaPendidikanDefaultForm = {
nama: "",
jenjangId: "",
};
const lembagaPendidikan = proxy({
create: {
form: { ...lembagaPendidikanDefaultForm },
loading: false,
async create() {
const cek = lembagaPendidikanForm.safeParse(
lembagaPendidikan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
lembagaPendidikan.create.loading = true;
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
"create"
].post(lembagaPendidikan.create.form);
if (res.status === 200) {
lembagaPendidikan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
lembagaPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.LembagaGetPayload<{
include: {
jenjangPendidikan: true;
siswa: true;
pengajar: true;
};
}>
> | null,
async load() {
const res =
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
"find-many"
].get();
if (res.status === 200) {
lembagaPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.LembagaGetPayload<{
include: {
jenjangPendidikan: true;
siswa: true;
pengajar: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/${id}`
);
if (res.ok) {
const data = await res.json();
lembagaPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
lembagaPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
lembagaPendidikan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
lembagaPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "lembaga pendidikan berhasil dihapus"
);
await lembagaPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus lembaga pendidikan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus lembaga pendidikan");
} finally {
lembagaPendidikan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...lembagaPendidikanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
jenjangId: data.jenjangId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading lembaga pendidikan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = lembagaPendidikanForm.safeParse(lembagaPendidikan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
lembagaPendidikan.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/lembagapendidikan/${lembagaPendidikan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: lembagaPendidikan.edit.form.nama,
jenjangId: lembagaPendidikan.edit.form.jenjangId,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate lembaga pendidikan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui lembaga pendidikan"
);
await lembagaPendidikan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate lembaga pendidikan"
);
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating lembaga pendidikan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate lembaga pendidikan"
);
return false;
} finally {
lembagaPendidikan.edit.loading = false;
}
},
reset() {
lembagaPendidikan.edit.id = "";
lembagaPendidikan.edit.form = { ...lembagaPendidikanDefaultForm };
},
},
});
// ========================================= SISWA ========================================= //
const siswaForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
lembagaId: z.string().min(1, "lembaga pendidikan minimal 1"),
});
const siswaDefaultForm = {
nama: "",
lembagaId: "",
};
const siswa = proxy({
create: {
form: { ...siswaDefaultForm },
loading: false,
async create() {
const cek = siswaForm.safeParse(siswa.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
siswa.create.loading = true;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"create"
].post(siswa.create.form);
if (res.status === 200) {
siswa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
siswa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.SiswaGetPayload<{
include: {
lembaga: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
"find-many"
].get();
if (res.status === 200) {
siswa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.SiswaGetPayload<{
include: {
lembaga: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/infosekolahpaud/siswa/${id}`);
if (res.ok) {
const data = await res.json();
siswa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
siswa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
siswa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
siswa.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/siswa/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "siswa berhasil dihapus");
await siswa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus siswa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus siswa");
} finally {
siswa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...siswaDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/siswa/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
lembagaId: data.lembagaId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading siswa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = siswaForm.safeParse(siswa.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
siswa.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/siswa/${siswa.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: siswa.edit.form.nama,
lembagaId: siswa.edit.form.lembagaId,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message || `Gagal mengupdate siswa (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui siswa");
await siswa.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate siswa");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating siswa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate siswa"
);
return false;
} finally {
siswa.edit.loading = false;
}
},
reset() {
siswa.edit.id = "";
siswa.edit.form = { ...siswaDefaultForm };
},
},
});
// ========================================= PENGAJAR ========================================= //
const pengajarForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
lembagaId: z.string().min(1, "lembaga pendidikan minimal 1"),
});
const pengajarDefaultForm = {
nama: "",
lembagaId: "",
};
const pengajar = proxy({
create: {
form: { ...pengajarDefaultForm },
loading: false,
async create() {
const cek = pengajarForm.safeParse(pengajar.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pengajar.create.loading = true;
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"create"
].post(pengajar.create.form);
if (res.status === 200) {
pengajar.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
pengajar.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PengajarGetPayload<{
include: {
lembaga: true;
};
}>
> | null,
async load() {
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
"find-many"
].get();
if (res.status === 200) {
pengajar.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PengajarGetPayload<{
include: {
lembaga: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`);
if (res.ok) {
const data = await res.json();
pengajar.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pengajar.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pengajar.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengajar.delete.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "pengajar berhasil dihapus");
await pengajar.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengajar");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengajar");
} finally {
pengajar.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...pengajarDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
lembagaId: data.lembagaId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading siswa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = pengajarForm.safeParse(pengajar.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pengajar.edit.loading = true;
const response = await fetch(
`/api/pendidikan/infosekolahpaud/pengajar/${pengajar.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: pengajar.edit.form.nama,
lembagaId: pengajar.edit.form.lembagaId,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message || `Gagal mengupdate pengajar (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui pengajar");
await pengajar.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pengajar");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating pengajar:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pengajar"
);
return false;
} finally {
pengajar.edit.loading = false;
}
},
reset() {
pengajar.edit.id = "";
pengajar.edit.form = { ...pengajarDefaultForm };
},
},
});
const infoSekolahPaud = proxy({
jenjangPendidikan,
lembagaPendidikan,
siswa,
pengajar,
});
export default infoSekolahPaud;

View File

@@ -0,0 +1,267 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PENDIDIKAN NON FORMAL ========================================= //
const templateTujuanPendidikanNonFormalForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanPendidikanNonFormalForm =
Prisma.TujuanPendidikanNonFormalGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanPendidikanNonFormal = proxy({
findById: {
data: null as TujuanPendidikanNonFormalForm | null,
loading: false,
initialize() {
stateTujuanPendidikanNonFormal.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanPendidikanNonFormalForm;
},
async load(id: string) {
try {
stateTujuanPendidikanNonFormal.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanPendidikanNonFormal.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan pendidikan non formal");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data tujuan pendidikan non formal"
);
} finally {
stateTujuanPendidikanNonFormal.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanPendidikanNonFormalForm) {
const cek = templateTujuanPendidikanNonFormalForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTujuanPendidikanNonFormal.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tujuanpendidikannonformal[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan pendidikan non formal berhasil diubah");
await stateTujuanPendidikanNonFormal.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan pendidikan non formal");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data tujuan pendidikan non formal"
);
} finally {
stateTujuanPendidikanNonFormal.update.loading = false;
}
},
},
});
// ========================================= TEMPAT KEGIATAN ========================================= //
const templateTempatKegiatanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TempatKegiatanForm = Prisma.TempatKegiatanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTempatKegiatan = proxy({
findById: {
data: null as TempatKegiatanForm | null,
loading: false,
initialize() {
stateTempatKegiatan.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TempatKegiatanForm;
},
async load(id: string) {
try {
stateTempatKegiatan.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTempatKegiatan.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tempat kegiatan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tempat kegiatan");
} finally {
stateTempatKegiatan.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TempatKegiatanForm) {
const cek = templateTempatKegiatanForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTempatKegiatan.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.tempatkegiatan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tempat kegiatan berhasil diubah");
await stateTempatKegiatan.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tempat kegiatan");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tempat kegiatan");
} finally {
stateTempatKegiatan.update.loading = false;
}
},
},
});
// ========================================= JENIS PROGRAM YANG DISELENGGARAKAN ========================================= //
const templateJenisProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type JenisProgramForm = Prisma.JenisProgramYangDiselenggarakanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateJenisProgram = proxy({
findById: {
data: null as JenisProgramForm | null,
loading: false,
initialize() {
stateJenisProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as JenisProgramForm;
},
async load(id: string) {
try {
stateJenisProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateJenisProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data jenis program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data jenis program");
} finally {
stateJenisProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: JenisProgramForm) {
const cek = templateJenisProgramForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateJenisProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.pendidikannonformal.jenisprogramyangdiselenggarakan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data jenis program berhasil diubah");
await stateJenisProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data jenis program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data jenis program");
} finally {
stateJenisProgram.update.loading = false;
}
},
},
});
const pendidikanNonFormalState = proxy({
stateTujuanPendidikanNonFormal,
stateTempatKegiatan,
stateJenisProgram,
});
export default pendidikanNonFormalState;

View File

@@ -0,0 +1,478 @@
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 templateDataPerpustakaan = z.object({
judul: z.string().min(1, "Judul harus diisi"),
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
imageId: z.string().min(1, "Image ID harus diisi"),
kategoriId: z.string().min(1, "Kategori ID harus diisi"),
});
const defaultDataPerpustakaan = {
judul: "",
deskripsi: "",
imageId: "",
kategoriId: "",
};
const dataPerpustakaan = proxy({
create: {
form: { ...defaultDataPerpustakaan },
loading: false,
async create() {
const cek = templateDataPerpustakaan.safeParse(
dataPerpustakaan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
dataPerpustakaan.create.loading = true;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"create"
].post(dataPerpustakaan.create.form);
if (res.status === 200) {
dataPerpustakaan.findMany.load();
return toast.success("Data Data Perpustakaan Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
dataPerpustakaan.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.DataPerpustakaanGetPayload<{
include: {
kategori: true;
image: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
"findMany"
].get();
if (res.status === 200) {
dataPerpustakaan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {
kategori: true;
image: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/${id}`
);
if (res.ok) {
const data = await res.json();
dataPerpustakaan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
dataPerpustakaan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
dataPerpustakaan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataPerpustakaan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data Perpustakaan berhasil dihapus");
await dataPerpustakaan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data Perpustakaan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Perpustakaan");
} finally {
dataPerpustakaan.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultDataPerpustakaan },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
judul: data.judul,
deskripsi: data.deskripsi,
imageId: data.imageId,
kategoriId: data.kategoriId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading perpustakaan digital:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateDataPerpustakaan.safeParse(
dataPerpustakaan.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
dataPerpustakaan.update.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/dataperpustakaan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
kategoriId: this.form.kategoriId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data perpustakaan digital");
await dataPerpustakaan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal update data perpustakaan digital"
);
}
} catch (error) {
console.error("Error updating data perpustakaan digital:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data perpustakaan digital"
);
return false;
} finally {
dataPerpustakaan.update.loading = false;
}
},
reset() {
dataPerpustakaan.update.id = "";
dataPerpustakaan.update.form = { ...defaultDataPerpustakaan };
},
},
});
const templateKategoriBuku = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultKategoriBuku = {
name: "",
};
const kategoriBuku = proxy({
create: {
form: { ...defaultKategoriBuku },
loading: false,
async create() {
const cek = templateKategoriBuku.safeParse(kategoriBuku.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriBuku.create.loading = true;
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"create"
].post(kategoriBuku.create.form);
if (res.status === 200) {
kategoriBuku.findMany.load();
return toast.success("Data Kategori Buku Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
kategoriBuku.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.KategoriBukuGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
"findMany"
].get();
if (res.status === 200) {
kategoriBuku.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriBukuGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/${id}`
);
if (res.ok) {
const data = await res.json();
kategoriBuku.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriBuku.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriBuku.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriBuku.delete.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data Kategori Buku berhasil dihapus"
);
await kategoriBuku.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data Kategori Buku");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data Kategori Buku");
} finally {
kategoriBuku.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultKategoriBuku },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/${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,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori buku:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKategoriBuku.safeParse(kategoriBuku.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriBuku.update.loading = true;
const response = await fetch(
`/api/pendidikan/perpustakaandigital/kategoribuku/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data kategori buku");
await kategoriBuku.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data kategori buku");
}
} catch (error) {
console.error("Error updating data kategori buku:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data kategori buku"
);
return false;
} finally {
kategoriBuku.update.loading = false;
}
},
reset() {
kategoriBuku.update.id = "";
kategoriBuku.update.form = { ...defaultKategoriBuku };
},
},
});
const perpustakaanDigitalState = proxy({
dataPerpustakaan,
kategoriBuku,
});
export default perpustakaanDigitalState;

View File

@@ -0,0 +1,181 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// ========================================= TUJUAN PROGRAM ========================================= //
const templateTujuanProgramForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanProgramForm = Prisma.TujuanProgramGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanProgram = proxy({
findById: {
data: null as TujuanProgramForm | null,
loading: false,
initialize() {
stateTujuanProgram.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as TujuanProgramForm;
},
async load(id: string) {
try {
stateTujuanProgram.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.tujuanprogram[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateTujuanProgram.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tujuan program");
} finally {
stateTujuanProgram.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanProgramForm) {
const cek = templateTujuanProgramForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTujuanProgram.update.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.tujuanprogram[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data tujuan program berhasil diubah");
await stateTujuanProgram.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan program");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tujuan program");
} finally {
stateTujuanProgram.update.loading = false;
}
},
},
});
// ========================================= PROGRAM UNGGULAN ========================================= //
const templateProgramUnggulanForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type ProgramUnggulanForm = Prisma.ProgramUnggulanGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const programUnggulanState = proxy({
findById: {
data: null as ProgramUnggulanForm | null,
loading: false,
initialize() {
programUnggulanState.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as ProgramUnggulanForm;
},
async load(id: string) {
try {
programUnggulanState.findById.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.programunggulan[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
programUnggulanState.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data program pendidikan anak");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data program pendidikan anak"
);
} finally {
programUnggulanState.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: ProgramUnggulanForm) {
const cek = templateProgramUnggulanForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
programUnggulanState.update.loading = true;
const res =
await ApiFetch.api.pendidikan.programpendidikananak.programunggulan[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data program pendidikan anak berhasil diubah");
await programUnggulanState.findById.load(data.id);
} else {
toast.error("Gagal mengubah data program pendidikan anak");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data program pendidikan anak"
);
} finally {
programUnggulanState.update.loading = false;
}
},
},
});
const stateProgramPendidikanAnak = proxy({
stateTujuanProgram,
programUnggulanState,
});
export default stateProgramPendidikanAnak;

View File

@@ -1,169 +1,683 @@
/* 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 templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
})
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const defaultForm = {
name: "",
imageId: "",
name: "",
imageId: "",
};
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
};
}>;
const stateStrukturPPID = proxy({
struktur: {
data: null as StrukturPPIDForm | null,
loading: false,
error: null as string | null,
const stateStruktur = proxy({
struktur: {
data: null as StrukturPPIDForm | null,
loading: false,
error: null as string | null,
async load(id: string) {
if(!id) {
toast.warn("ID tidak valid")
return null
}
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
this.loading = true;
this.error = null;
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${id}`);
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
try {
const response = await fetch(`/api/ppid/strukturppid/${id}`);
const result = await response.json();
if(result.success) {
this.data = result.data;
return result.data
} else {
throw new Error(result.message || "Gagal mengambil data struktur")
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
this.error = null;
this.loading = false;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
const result = await response.json();
initialize(strukturData: StrukturPPIDForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update struktur");
await stateStrukturPPID.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
if (result.success) {
this.data = result.data;
return result.data;
} else {
throw new Error(result.message || "Gagal mengambil data struktur");
}
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
return strukturData;
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
this.data = null;
this.error = null;
this.loading = false;
},
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
initialize(strukturData: StrukturPPIDForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update struktur");
await stateStruktur.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
})
return strukturData;
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
},
});
const templatePosisiOrganisasi = z.object({
nama: z.string().min(1, "Nama harus diisi"),
deskripsi: z.string().optional(),
hierarki: z.number().int().positive("Hierarki harus angka positif"),
});
const posisiOrganisasiDefaultForm = {
nama: "",
deskripsi: "",
hierarki: 0,
};
const posisiOrganisasi = proxy({
create: {
form: { ...posisiOrganisasiDefaultForm },
loading: false,
async submit() {
const cek = templatePosisiOrganisasi.safeParse(this.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join("\n");
return toast.error(err);
}
try {
this.loading = true;
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"create"
].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal menambahkan posisi");
}
} catch (error) {
console.error("Create error:", error);
toast.error("Terjadi kesalahan saat menambahkan posisi");
} finally {
this.loading = false;
}
},
reset() {
this.form = { ...posisiOrganisasiDefaultForm };
},
},
findUnique: {
data: null as Prisma.StrukturOrganisasiPPIDGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ppid/strukturppid/posisiorganisasi/${id}`
);
if (res.ok) {
const data = await res.json();
posisiOrganisasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
posisiOrganisasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching posisiOrganisasi:", error);
posisiOrganisasi.findUnique.data = null;
}
},
},
edit: {
id: "",
form: { ...posisiOrganisasiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/ppid/strukturppid/posisiorganisasi/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
deskripsi: data.deskripsi,
hierarki: data.hierarki,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading posisi organisasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePosisiOrganisasi.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
this.loading = true;
const response = await fetch(
`/api/ppid/strukturppid/posisiorganisasi/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
deskripsi: this.form.deskripsi,
hierarki: this.form.hierarki,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update posisi organisasi");
await posisiOrganisasi.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate posisi organisasi"
);
}
} catch (error) {
console.error("Error updating posisi organisasi:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate posisi organisasi"
);
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...posisiOrganisasiDefaultForm };
},
},
findMany: {
data: [] as Array<{
id: string;
nama: string;
deskripsi: string | null;
hierarki: number;
}>,
async load() {
try {
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
"find-many"
].get();
if (res.status === 200) {
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? [];
}
} catch (error) {
console.error("Find many error:", error);
this.data = [];
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
posisiOrganisasi.delete.loading = true;
const response = await fetch(
`/api/ppid/strukturppid/posisiorganisasi/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Posisi organisasi berhasil dihapus");
await posisiOrganisasi.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus posisi organisasi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
} finally {
posisiOrganisasi.delete.loading = false;
}
},
},
});
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().min(1, "Telepom wajib diisi"),
alamat: z.string().min(1, "Alamat wajib diisi"),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
const pegawaiDefaultForm = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
const pegawai = proxy({
create: {
form: { ...pegawaiDefaultForm },
loading: false,
async submit() {
const cek = templatePegawai.safeParse(pegawai.create.form);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"create"
].post(pegawai.create.form);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah pegawai");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
} finally {
pegawai.create.loading = false;
}
},
},
// In struktur-organisasi.ts
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pegawai.findMany.data = res.data.data || [];
pegawai.findMany.total = res.data.total || 0;
pegawai.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
async load(id: string) {
const res = await fetch(`/api/ppid/strukturppid/pegawai/${id}`);
if (res.ok) {
const json = await res.json();
pegawai.findUnique.data = json.data
? {
...json.data,
isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada data
}
: null;
} else {
pegawai.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(
`/api/ppid/strukturppid/pegawai/del/${id}`,
{
method: "DELETE",
}
);
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
await pegawai.findMany.load();
} else {
toast.error(json.message ?? "Gagal hapus pegawai");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
pegawai.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/ppid/strukturppid/pegawai/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pegawai.edit.loading = true;
// Format tanggalMasuk to ISO string if it exists
const formattedTanggalMasuk = this.form.tanggalMasuk
? new Date(this.form.tanggalMasuk).toISOString()
: undefined;
const response = await fetch(
`/api/ppid/strukturppid/pegawai/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: this.id,
namaLengkap: this.form.namaLengkap,
gelarAkademik: this.form.gelarAkademik,
imageId: this.form.imageId || null,
tanggalMasuk: formattedTanggalMasuk,
email: this.form.email,
telepon: this.form.telepon,
alamat: this.form.alamat,
posisiId: this.form.posisiId,
isActive: this.form.isActive,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pegawai");
await pegawai.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pegawai"
);
return false;
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
});
const stateStrukturPPID = proxy({
stateStruktur,
posisiOrganisasi,
pegawai
});
export default stateStrukturPPID;

View File

@@ -0,0 +1,456 @@
import { proxy } from 'valtio'
import { toast } from 'react-toastify'
import ApiFetch from '@/lib/api-fetch'
import { Prisma } from '@prisma/client'
import { z } from 'zod'
// 1. Validasi Zod
const userSchema = z.object({
nama: z.string().min(1, 'Nama harus diisi'),
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Password minimal 6 karakter'),
roleId: z.string().optional(),
})
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
// 2. State Valtio
const userState = proxy({
// Register
register: {
form: { ...defaultForm },
loading: false,
async submit() {
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(', ')
return toast.error(err)
}
try {
userState.register.loading = true
const res = await ApiFetch.api.user.register.post(userState.register.form)
if (res.status === 200) {
toast.success('Registrasi berhasil, silakan login')
userState.register.form = { ...defaultForm } // reset
} else {
toast.error(res.data?.message || 'Gagal registrasi')
}
} catch (e) {
console.error(e)
toast.error('Terjadi kesalahan saat registrasi')
} finally {
userState.register.loading = false
}
},
},
// Login
login: {
form: { email: '', password: '' },
loading: false,
async submit() {
try {
userState.login.loading = true
const res = await ApiFetch.api.user.login.post(userState.login.form)
if (res.status === 200) {
toast.success('Login berhasil')
const token = res.data?.data?.token
if (typeof token === 'string') {
localStorage.setItem('token', token)
// Optional: simpan user role untuk otorisasi
const user = res.data?.data?.user
localStorage.setItem('user', JSON.stringify(user))
}
} else {
toast.error(res.data?.message || 'Login gagal')
}
} catch (e) {
console.error(e)
toast.error('Terjadi kesalahan saat login')
} finally {
userState.login.loading = false
}
},
},
// CRUD User (untuk admin)
create: {
form: { ...defaultForm },
loading: false,
async create(isAdmin = false) {
const valid = userSchema.safeParse(userState.create.form)
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(', ')
return toast.error(err)
}
try {
userState.create.loading = true
const endpoint = isAdmin ? 'create' : 'register'
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
if (res.status === 200) {
toast.success('User berhasil dibuat')
userState.findMany.load() // refresh list
userState.create.form = { ...defaultForm } // reset form
} else {
toast.error(res.data?.message || 'Gagal membuat user')
}
} catch (e) {
console.error(e)
toast.error('Gagal membuat user')
} finally {
userState.create.loading = false
}
},
},
// Find Many
findMany: {
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
loading: false,
async load() {
this.loading = true
try {
const res = await ApiFetch.api.user.findMany.get()
if (res.status === 200) {
this.data = res.data?.data || []
}
} catch (e) {
console.error(e)
toast.error('Gagal muat data user')
} finally {
this.loading = false
}
},
},
// Find Unique
findUnique: {
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
loading: false,
async load(id: string) {
this.loading = true
try {
const res = await fetch(`/api/user/findUnique/${id}`)
const data = await res.json()
if (res.status === 200) {
this.data = data.data
} else {
toast.error(data.message)
}
} catch (e) {
console.error(e)
toast.error('Gagal ambil data user')
} finally {
this.loading = false
}
},
},
// Update
update: {
id: '',
form: { ...defaultForm },
loading: false,
async load(id: string) {
this.loading = true
try {
const res = await fetch(`/api/user/findUnique/${id}`)
const data = await res.json()
if (res.status === 200) {
const user = data.data
this.id = user.id
this.form = {
nama: user.nama,
email: user.email,
password: '', // jangan kirim password lama
roleId: user.roleId,
}
}
} catch (e) {
console.error(e)
toast.error('Gagal muat data')
} finally {
this.loading = false
}
},
async submit() {
this.loading = true
try {
const res = await fetch(`/api/user/update/${this.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form),
})
const data = await res.json()
if (res.status === 200) {
toast.success('Update berhasil')
userState.findMany.load()
} else {
toast.error(data.message || 'Gagal update')
}
} catch (e) {
console.error(e)
toast.error('Gagal update user')
} finally {
this.loading = false
}
},
},
// Delete (Soft Delete)
delete: {
loading: false,
async submit(id: string) {
this.loading = true
try {
const res = await fetch(`/api/user/del/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
if (res.status === 200) {
toast.success('User dinonaktifkan')
userState.findMany.load()
} else {
toast.error(data.message || 'Gagal hapus')
}
} catch (e) {
console.error(e)
toast.error('Gagal hapus user')
} finally {
this.loading = false
}
},
},
})
const templateRole = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultRole = {
name: "",
};
const roleState = proxy({
create: {
form: { ...defaultRole },
loading: false,
async create() {
const cek = templateRole.safeParse(roleState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
roleState.create.loading = true;
const res =
await ApiFetch.api.role[
"create"
].post(roleState.create.form);
if (res.status === 200) {
roleState.findMany.load();
return toast.success("Data role Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
roleState.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.RoleGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.role[
"findMany"
].get();
if (res.status === 200) {
roleState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.RoleGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/role/${id}`
);
if (res.ok) {
const data = await res.json();
roleState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
roleState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
roleState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
roleState.delete.loading = true;
const response = await fetch(
`/api/role/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data role berhasil dihapus"
);
await roleState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data role");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data role");
} finally {
roleState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultRole },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/role/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading role:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateRole.safeParse(roleState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
roleState.update.loading = true;
const response = await fetch(
`/api/role/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data role");
await roleState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data role");
}
} catch (error) {
console.error("Error updating data role:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data role"
);
return false;
} finally {
roleState.update.loading = false;
}
},
reset() {
roleState.update.id = "";
roleState.update.form = { ...defaultRole };
},
},
});
const user = proxy({
userState,
roleState,
})
export default user

View File

@@ -148,7 +148,6 @@ function Page() {
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box w={{ base: '100%', md: '50%' }}>
<Dropzone
onDrop={(files) => {
const newImages = files.map((file) => ({
file,

View File

@@ -12,17 +12,17 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi"
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi"
},
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai"
},
{
label: "Hubungan Organisasi",
value: "hubunganorganisasi",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi"
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/hubungan-organisasi"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -126,7 +126,7 @@ export default function EditPegawai() {
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);

View File

@@ -25,7 +25,7 @@ function DetailPegawai() {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai")
}
}
@@ -120,7 +120,7 @@ function DetailPegawai() {
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.findUnique.data}

View File

@@ -64,7 +64,7 @@ function CreatePegawai() {
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");

View File

@@ -78,7 +78,7 @@ function ListPegawai({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
@@ -131,7 +131,7 @@ function ListPegawai({ search }: { search: string }) {
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>

View File

@@ -68,7 +68,7 @@ function EditPosisiOrganisasi() {
const success = await stateOrganisasi.edit.update();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi");
}
} catch (error) {
console.error("Error updating posisi organisasi:", error);

View File

@@ -27,7 +27,7 @@ function CreatePosisiOrganisasi() {
const handleSubmit = async () => {
await stateOrganisasi.create.submit();
resetForm();
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi")
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi")
};
return (

View File

@@ -67,7 +67,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Posisi Organisasi'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
@@ -91,7 +91,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`);
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/${item.id}`);
}
}}
>

View File

@@ -21,7 +21,7 @@ function KeamananLingkungan() {
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKeamananLingkungan search={search}/>
<ListKeamananLingkungan search={search} />
</Box>
);
}

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
@@ -34,6 +34,14 @@ function ListKategoriKegiatan({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = stateKategori.findMany;
const handleHapus = () => {
if (selectedId) {
stateKategori.delete.byId(selectedId)
@@ -42,23 +50,51 @@ function ListKategoriKegiatan({ search }: { search: string }) {
}
}
useShallowEffect(() => {
stateKategori.findMany.load()
}, [])
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (stateKategori.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
if (!stateKategori.findMany.data) {
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={300} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Kategori Kegiatan'
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
@@ -68,36 +104,50 @@ function ListKategoriKegiatan({ search }: { search: string }) {
title='List Kategori Kegiatan'
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
@@ -29,27 +29,65 @@ function DesaAntiKorupsi() {
function ListDesaAntiKorupsi({ search }: { search: string }) {
const listState = useProxy(korupsiState.desaAntikorupsi)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useEffect(() => {
listState.findMany.load()
}, [])
load(page, 10);
}, [page]);
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
});
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => b.createdAt - a.createdAt);
}, [data, search]);
if (!listState.findMany.data) {
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={300} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Desa Anti Korupsi'
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -77,7 +115,9 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Box w={200}>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>{item.kategori?.name}</TableTd>
<TableTd>
@@ -92,6 +132,18 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
)
}

View File

@@ -0,0 +1,67 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Survey",
value: "listSurvey",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey"
},
{
label: "List Bulanan",
value: "listBulanan",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan"
},
{
label: "List Gender Stat",
value: "listGenderStat",
href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat"
}
];
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 LayoutTabs;

View File

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

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditListBulanan() {
const editState = useProxy(indeksKepuasanState.monthlyStatState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
month: editState.edit.form.month || '',
respondentsCount: editState.edit.form.respondentsCount || 0,
surveyId: editState.edit.form.surveyId || '',
})
useEffect(() => {
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
month: data.month,
respondentsCount: data.respondentsCount,
surveyId: data.surveyId,
});
}
} catch (error) {
console.error("Error loading list bulanan:", error);
toast.error("Gagal memuat data list bulanan");
}
};
indeksKepuasanState.surveyState.findMany.load();
loadSurvey();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
month: formData.month,
respondentsCount: formData.respondentsCount,
surveyId: formData.surveyId,
};
await editState.edit.update();
toast.success("list bulanan berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan");
} catch (error) {
console.error("Error updating list bulanan:", error);
toast.error("Terjadi kesalahan saat memperbarui list bulanan");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
value={formData.month}
onChange={(val) => {
setFormData({
...formData,
month: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Bulan</Text>}
placeholder='Masukkan bulan'
/>
<TextInput
value={formData.respondentsCount}
onChange={(val) => {
setFormData({
...formData,
respondentsCount: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Survey</Text>}
placeholder="Pilih survey"
value={formData.surveyId}
onChange={(value) => {
if (value) setFormData({
...formData,
surveyId: value
})
}}
data={
indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({
value: survey.id,
label: `${survey.title} (${survey.totalRespondents} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListBulanan;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailListBulanan() {
const detailState = useProxy(indeksKepuasanState.monthlyStatState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Bulan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.month}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.respondentsCount}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Survey</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.survey.title }</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus list bulanan ini?'
/>
</Box>
);
}
export default DetailListBulanan;

View File

@@ -0,0 +1,84 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListBulanan() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.monthlyStatState)
useEffect(() => {
stateCreate.findMany.load();
indeksKepuasanState.surveyState.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
month: "",
respondentsCount: 0,
surveyId: "",
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan")
}
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 List Bulanan</Title>
<TextInput
value={stateCreate.create.form.month}
onChange={(val) => {
stateCreate.create.form.month = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Bulan</Text>}
placeholder='Masukkan bulan'
/>
<TextInput
value={stateCreate.create.form.respondentsCount}
onChange={(val) => {
stateCreate.create.form.respondentsCount = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Survey</Text>}
placeholder="Pilih survey"
value={stateCreate.create.form.surveyId}
onChange={(value) => {
if (value) stateCreate.create.form.surveyId = value;
}}
data={
indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({
value: survey.id,
label: `${survey.title} (${survey.totalRespondents} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListBulanan;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } 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 indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
import JudulList from '../../../_com/judulList';
function ListBulananLandingPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Bulanan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBUlanan search={search} />
</Box>
);
}
function ListBUlanan({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.monthlyStatState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.month.toLowerCase().includes(keyword) ||
item.respondentsCount.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Bulanan'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Bulan</TableTh>
<TableTh>Total Responden</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.month}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.respondentsCount}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListBulananLandingPage;

View File

@@ -0,0 +1,186 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditListSurvey() {
const editState = useProxy(indeksKepuasanState.genderStatState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
laki: editState.edit.form.laki || 0,
perempuan: editState.edit.form.perempuan || 0,
monthlyStatId: editState.edit.form.monthlyStatId || '',
total: editState.edit.form.total || 0,
percentLaki: editState.edit.form.percentLaki || 0,
percentPerempuan: editState.edit.form.percentPerempuan || 0,
})
useEffect(() => {
indeksKepuasanState.monthlyStatState.findMany.load();
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
laki: data.laki || 0,
perempuan: data.perempuan || 0,
monthlyStatId: data.monthlyStatId || '',
total: data.total || 0,
percentLaki: data.percentLaki || 0,
percentPerempuan: data.percentPerempuan || 0,
});
}
} catch (error) {
console.error("Error loading list survey:", error);
toast.error("Gagal memuat data list survey");
}
};
loadSurvey();
}, [params?.id]);
// Hitung total dan persentase saat nilai laki atau perempuan berubah
useEffect(() => {
const total = formData.laki + formData.perempuan;
const percentLaki = total > 0 ? Math.round((formData.laki / total) * 100) : 0;
const percentPerempuan = 100 - percentLaki;
setFormData(prev => ({
...prev,
total,
percentLaki,
percentPerempuan
}));
}, [formData.laki, formData.perempuan]);
const handleSubmit = async () => {
if (!formData.monthlyStatId) {
return toast.error("Silakan pilih bulan terlebih dahulu");
}
if (formData.laki < 0 || formData.perempuan < 0) {
return toast.error("Nilai tidak boleh negatif");
}
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
laki: formData.laki,
perempuan: formData.perempuan,
monthlyStatId: formData.monthlyStatId,
total: formData.total,
percentLaki: formData.percentLaki,
percentPerempuan: formData.percentPerempuan,
};
await editState.edit.update();
toast.success("Data gender berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat");
} catch (error) {
console.error("Error updating list gender:", error);
toast.error("Terjadi kesalahan saat memperbarui data gender");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
type="number"
value={formData.laki}
onChange={(val) => {
setFormData({
...formData,
laki: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Laki - Laki</Text>}
placeholder='Masukkan laki - laki'
/>
<TextInput
type="number"
value={formData.perempuan}
onChange={(val) => {
setFormData({
...formData,
perempuan: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Perempuan</Text>}
placeholder='Masukkan perempuan'
/>
<TextInput
type="number"
value={formData.total}
label={<Text fw="bold" fz="sm">Total</Text>}
disabled
/>
<TextInput
type="number"
value={formData.percentLaki}
label={<Text fw="bold" fz="sm">Persentase Laki Laki</Text>}
disabled
/>
<TextInput
type="number"
value={formData.percentPerempuan}
label={<Text fw="bold" fz="sm">Persentase Perempuan</Text>}
disabled
/>
<Select
label={"Pilih Bulan"}
placeholder="Pilih bulanan"
value={formData.monthlyStatId}
onChange={(value) => {
setFormData(prev => ({
...prev,
monthlyStatId: value || ''
}));
}}
data={
indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({
value: monthlyStat.id,
label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`,
})) || []
}
required
error={!formData.monthlyStatId ? 'Bulan harus dipilih' : undefined}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={editState.edit.loading}
>
{editState.edit.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListSurvey;

View File

@@ -0,0 +1,113 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailGenderStat() {
const detailState = useProxy(indeksKepuasanState.genderStatState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Laki Laki</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.laki}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Perempuan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.perempuan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Persentase Laki Laki</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.percentLaki}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Persentase Perempuan</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.percentPerempuan}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus list gender stat ini?'
/>
</Box>
);
}
export default DetailGenderStat;

View File

@@ -0,0 +1,103 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListGenderStat() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.genderStatState)
useEffect(() => {
stateCreate.findMany.load();
indeksKepuasanState.monthlyStatState.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
laki: 0,
perempuan: 0,
monthlyStatId: "",
total: 0,
percentLaki: 0,
percentPerempuan: 0,
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat")
}
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 List Gender Stat</Title>
<TextInput
value={stateCreate.create.form.laki}
onChange={(val) => {
stateCreate.create.form.laki = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Laki Laki</Text>}
placeholder='Masukkan laki laki'
/>
<TextInput
value={stateCreate.create.form.perempuan}
onChange={(val) => {
stateCreate.create.form.perempuan = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Perempuan</Text>}
placeholder='Masukkan perempuan'
/>
<TextInput
value={stateCreate.create.form.total}
label={<Text fw="bold" fz="sm">Total</Text>}
disabled
/>
<TextInput
value={stateCreate.create.form.percentLaki}
label={<Text fw="bold" fz="sm">Persentase Laki Laki</Text>}
disabled
/>
<TextInput
value={stateCreate.create.form.percentPerempuan}
label={<Text fw="bold" fz="sm">Persentase Perempuan</Text>}
disabled
/>
<Select
label={<Text fw="bold" fz="sm">Pilih Bulan</Text>}
placeholder="Pilih bulanan"
value={stateCreate.create.form.monthlyStatId}
onChange={(value) => {
if (value) stateCreate.create.form.monthlyStatId = value;
}}
data={
indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({
value: monthlyStat.id,
label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListGenderStat;

View File

@@ -0,0 +1,106 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } 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 indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
function ListGenderStat() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Gender Stat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGender search={search} />
</Box>
);
}
function ListGender({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.genderStatState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.laki.toString().includes(keyword) ||
item.perempuan.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Gender Stat'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Laki Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Persentase Laki Laki</TableTh>
<TableTh>Persentase Perempuan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.laki}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.perempuan}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.percentLaki}%</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.percentPerempuan}%</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListGenderStat;

View File

@@ -0,0 +1,122 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditListSurvey() {
const editState = useProxy(indeksKepuasanState.surveyState)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState({
title: editState.edit.form.title || '',
totalRespondents: editState.edit.form.totalRespondents || 0,
averageScore: editState.edit.form.averageScore || 0,
})
useEffect(() => {
const loadSurvey = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
title: data.title || '',
totalRespondents: data.totalRespondents || 0,
averageScore: data.averageScore || 0,
});
}
} catch (error) {
console.error("Error loading list survey:", error);
toast.error("Gagal memuat data list survey");
}
};
loadSurvey();
}, [params?.id]);
const handleSubmit = async () => {
try {
// edit global state with form data
editState.edit.form = {
...editState.edit.form,
title: formData.title,
totalRespondents: formData.totalRespondents,
averageScore: formData.averageScore,
};
await editState.edit.update();
toast.success("list survey berhasil diperbarui!");
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey");
} catch (error) {
console.error("Error updating list survey:", error);
toast.error("Terjadi kesalahan saat memperbarui list survey");
}
};
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"}>Edit List Survey</Text>
<Paper>
<Stack gap={"xs"}>
<TextInput
value={formData.title}
onChange={(val) => {
setFormData({
...formData,
title: val.target.value
})
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
value={formData.totalRespondents}
onChange={(val) => {
setFormData({
...formData,
totalRespondents: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan total responden'
/>
<TextInput
value={formData.averageScore}
onChange={(val) => {
setFormData({
...formData,
averageScore: Number(val.target.value)
})
}}
label={<Text fw={"bold"} fz={"sm"}>Skor Rata-rata</Text>}
placeholder='Masukkan skor'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
</Box>
);
}
export default EditListSurvey;

View File

@@ -0,0 +1,109 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
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 { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailKegiatanDesa() {
const detailState = useProxy(indeksKepuasanState.surveyState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail List Survey</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.title}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Total Responden</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.totalRespondents}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Rata-rata Skor</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.averageScore}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus list survey ini?'
/>
</Box>
);
}
export default DetailKegiatanDesa;

View File

@@ -0,0 +1,78 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateListSurvey() {
const router = useRouter();
const stateCreate = useProxy(indeksKepuasanState.surveyState)
useEffect(() => {
stateCreate.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
title: "",
totalRespondents: 0,
averageScore: 0,
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey")
}
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 List Survey</Title>
<TextInput
value={stateCreate.create.form.title}
onChange={(val) => {
stateCreate.create.form.title = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<TextInput
value={stateCreate.create.form.totalRespondents}
onChange={(val) => {
stateCreate.create.form.totalRespondents = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Total Responden</Text>}
placeholder='Masukkan tahun'
/>
<TextInput
value={stateCreate.create.form.averageScore}
onChange={(val) => {
stateCreate.create.form.averageScore = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Skor</Text>}
placeholder='Masukkan skor'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateListSurvey;

View File

@@ -0,0 +1,103 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } 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 indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat';
import JudulList from '../../../_com/judulList';
function ListSurveyLandingPage() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='List Survey'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListSurvey search={search} />
</Box>
);
}
function ListSurvey({ search }: { search: string }) {
const listState = useProxy(indeksKepuasanState.surveyState)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.title.toLowerCase().includes(keyword) ||
item.totalRespondents.toString().includes(keyword) ||
item.averageScore.toString().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Survey'
href='/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Total Responden</TableTh>
<TableTh>Skor Rata-rata</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.title}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.totalRespondents}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.averageScore}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/list-survey/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListSurveyLandingPage;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
Indeks Kepuasan MAsyarakat
</div>
);
}
export default Page;

View File

@@ -68,35 +68,37 @@ function ListKategoriPrestasi({ search }: { search: string }) {
title='List Kategori Prestasi Desa'
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
@@ -30,24 +30,61 @@ function ListMediaSosial({ search }: { search: string }) {
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
const router = useRouter();
useShallowEffect(() => {
stateMediaSosial.findMany.load()
}, [])
const {
data,
page,
totalPages,
loading,
load,
} = stateMediaSosial.findMany;
const filteredData = (stateMediaSosial.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.iconUrl?.toLowerCase().includes(keyword)
);
});
useEffect(() => {
load(page, 10)
}, [page])
if (!stateMediaSosial.findMany.data) {
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.iconUrl?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={300} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
<TableTh>Image</TableTh>
<TableTh>Icon URL / No Telephone</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
@@ -57,35 +94,55 @@ function ListMediaSosial({ search }: { search: string }) {
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
<TableTh>Image</TableTh>
<TableTh>Icon URL / No Telephone</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box w={50} h={50}>
<Image src={item.image?.link} alt={item.name} />
</Box>
</TableTd>
<TableTd>{item.iconUrl}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
<TableTh>Image</TableTh>
<TableTh>Icon URL / No Telephone</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box w={50} h={50}>
<Image src={item.image?.link} alt={item.name} />
</Box>
</TableTd>
<TableTd>
<Box w={250}>
<a style={{color: "black"}} href={item.iconUrl} target="_blank" rel="noopener noreferrer">
<Text truncate fz={'sm'}>{item.iconUrl}</Text>
</a>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -59,7 +59,13 @@ function DetailProgramInovasi() {
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Link</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.link}</Text>
<a
href={stateProgramInovasi.findUnique.data?.link || "#"}
target="_blank"
rel="noopener noreferrer"
>
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"}
</a>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
@@ -30,27 +30,62 @@ function ListProgramInovasi({ search }: { search: string }) {
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
const router = useRouter();
useShallowEffect(() => {
stateProgramInovasi.findMany.load()
}, [])
const {
data,
page,
totalPages,
loading,
load,
} = stateProgramInovasi.findMany;
const filteredData = (stateProgramInovasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.description?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword)
);
});
useEffect(() => {
load(page, 10);
}, [page]);
if (!stateProgramInovasi.findMany.data) {
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.description?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={300} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -58,31 +93,51 @@ function ListProgramInovasi({ search }: { search: string }) {
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>{item.link}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd w={200}>{item.description}</TableTd>
<TableTd>
<Box w={250}>
<a style={{ color: "black" }} href={item.link} target="_blank" rel="noopener noreferrer">
<Text truncate fz={'sm'}>{item.link}</Text>
</a>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Beasiswa Pendaftar",
value: "beasiswa-pendaftar",
href: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
},
{
label: "Keunggulan Program",
value: "keunggulan-program",
href: "/admin/pendidikan/beasiswa-desa/keunggulan-program"
},
];
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}>Beasiswa Desa</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 LayoutTabs;

View File

@@ -0,0 +1,137 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailBeasiswaPendaftar() {
const state = useProxy(beasiswaDesaState.beasiswaPendaftar)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex justify="space-between" gap={"xs"}>
<Text fz={"xl"} fw={"bold"}>Detail Beasiswa Pendaftar</Text>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
</Flex>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Lengkap</Text>
<Text fz={"lg"}>{state.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text>
<Text fz={"lg"}>{state.findUnique.data?.nik}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tempat Lahir</Text>
<Text fz={"lg"}>{state.findUnique.data?.tempatLahir}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Lahir</Text>
<Text fz={"lg"}>{state.findUnique.data?.tanggalLahir ? new Date(state.findUnique.data.tanggalLahir).toLocaleDateString() : '-'}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
<Text fz={"lg"}>{state.findUnique.data?.jenisKelamin}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kewarganegaraan</Text>
<Text fz={"lg"}>{state.findUnique.data?.kewarganegaraan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Agama</Text>
<Text fz={"lg"}>{state.findUnique.data?.agama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat KTP</Text>
<Text fz={"lg"}>{state.findUnique.data?.alamatKTP}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat Domisili</Text>
<Text fz={"lg"}>{state.findUnique.data?.alamatDomisili}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>No HP</Text>
<Text fz={"lg"}>{state.findUnique.data?.noHp}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Email</Text>
<Text fz={"lg"}>{state.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Status Pernikahan</Text>
<Text fz={"lg"}>{state.findUnique.data?.statusPernikahan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Ukuran Baju</Text>
<Text fz={"lg"}>{state.findUnique.data?.ukuranBaju}</Text>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus beasiswa desa ini?'
/>
</Box>
);
}
export default DetailBeasiswaPendaftar;

View File

@@ -0,0 +1,107 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import HeaderSearch from '../../../_com/header';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
function BeasiswaPendaftar() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Beasiswa Pendaftar'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBeasiswaPendaftar search={search} />
</Box>
);
}
function ListBeasiswaPendaftar({ search }: { search: string }) {
const listDataState = useProxy(beasiswaDesaState.beasiswaPendaftar)
const router = useRouter();
useEffect(() => {
listDataState.findMany.load()
}, [])
const filteredData = (listDataState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.namaLengkap.toLowerCase().includes(keyword) ||
item.alamatKTP.toLowerCase().includes(keyword) ||
(item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '').toLowerCase().includes(keyword) ||
item.jenisKelamin.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<Title order={4}>List Beasiswa Pendaftar</Title>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Lengkap</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Tanggal Lahir</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.namaLengkap}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.alamatKTP}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '-'}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.jenisKelamin}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default BeasiswaPendaftar;

View File

@@ -3,13 +3,14 @@ import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import HeaderSearch from '../../_com/header';
import HeaderSearch from '../../../_com/header';
function BeasiswaDesa() {
return (
<Box>
<HeaderSearch
title='Beasiswa Desa'
title='Keunggulan Program'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>

View File

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

View File

@@ -0,0 +1,99 @@
'use client'
import { Button, Stack } from '@mantine/core';
import { Link, RichTextEditor } from '@mantine/tiptap';
import Highlight from '@tiptap/extension-highlight';
import SubScript from '@tiptap/extension-subscript';
import Superscript from '@tiptap/extension-superscript';
import TextAlign from '@tiptap/extension-text-align';
import Underline from '@tiptap/extension-underline';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect } from 'react';
export function BimbinganBelajarDesaTextEditor({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
onSubmit?: (val: string) => void,
onChange: (val: string) => void,
showSubmit?: boolean,
initialContent?: string }) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
immediatelyRender: false,
content: initialContent,
onUpdate : ({editor}) => {
onChange(editor.getHTML())
}
});
useEffect(() => {
if (editor && initialContent !== editor.getHTML()) {
editor.commands.setContent(initialContent || '<p></p>');
}
}, [initialContent, editor]);
return (
<Stack >
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
{showSubmit && (
<Button onClick={() => {
if (!editor) return
onSubmit?.(editor?.getHTML())
}}>Submit</Button>
)}
</Stack>
);
}

View File

@@ -0,0 +1,68 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Tujuan Program",
value: "tujuan-program",
href: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program"
},
{
label: "Lokasi dan Jadwal",
value: "lokasi-dan-jadwal",
href: "/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal"
},
{
label: "Fasilitas yang disediakan",
value: "fasilitas-yang-disediakan",
href: "/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan"
},
];
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}>Bimbingan Belajar Desa</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 LayoutTabs;

View File

@@ -0,0 +1,86 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const BimbinganBelajarDesaTextEditor = dynamic(() => import('../../_lib/bimbinganBelajarDesaTextEditor').then(mod => mod.BimbinganBelajarDesaTextEditor), {
ssr: false,
});
function EditTujuanProgram() {
const router = useRouter()
const editState = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!editState.findById.data) {
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (editState.findById.data) {
setJudul(editState.findById.data.judul ?? '')
setContent(editState.findById.data.deskripsi ?? '')
}
}, [editState.findById.data])
const submit = () => {
if (editState.findById.data) {
editState.findById.data.judul = judul;
editState.findById.data.deskripsi = content;
editState.update.save(editState.findById.data)
}
router.push('/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Fasilitas Yang Disediakan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
value={judul}
onChange={(e) => setJudul(e.target.value)}
/>
<Text fw={"bold"}>Deskripsi</Text>
<BimbinganBelajarDesaTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={editState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditTujuanProgram;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateBimbinganBelajarDesa from '../../../_state/pendidikan/bimbingan-belajar-desa';
function Page() {
const router = useRouter()
const listPreview = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState)
useShallowEffect(() => {
listPreview.findById.load('edit')
}, [])
if (!listPreview.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Fasilitas Yang Disediakan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

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

View File

@@ -0,0 +1,86 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const BimbinganBelajarDesaTextEditor = dynamic(() => import('../../_lib/bimbinganBelajarDesaTextEditor').then(mod => mod.BimbinganBelajarDesaTextEditor), {
ssr: false,
});
function EditTujuanProgram() {
const router = useRouter()
const editState = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!editState.findById.data) {
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (editState.findById.data) {
setJudul(editState.findById.data.judul ?? '')
setContent(editState.findById.data.deskripsi ?? '')
}
}, [editState.findById.data])
const submit = () => {
if (editState.findById.data) {
editState.findById.data.judul = judul;
editState.findById.data.deskripsi = content;
editState.update.save(editState.findById.data)
}
router.push('/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Lokasi Dan Jadwal</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
value={judul}
onChange={(e) => setJudul(e.target.value)}
/>
<Text fw={"bold"}>Deskripsi</Text>
<BimbinganBelajarDesaTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={editState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditTujuanProgram;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateBimbinganBelajarDesa from '../../../_state/pendidikan/bimbingan-belajar-desa';
function Page() {
const router = useRouter()
const listPreview = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState)
useShallowEffect(() => {
listPreview.findById.load('edit')
}, [])
if (!listPreview.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Lokasi Dan Jadwal</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -1,74 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
function BimbinganBelajarDesa() {
return (
<Box>
<HeaderSearch
title='Bimbingan Belajar Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListBimbinganBelajarDesa/>
</Box>
);
}
function ListBimbinganBelajarDesa() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Bimbingan Belajar Desa'
href='/admin/pendidikan/bimbingan-belajar-desa/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Nomor</TableTh>
<TableTh>Nama Lengkap</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>1</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Email</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default BimbinganBelajarDesa;

View File

@@ -0,0 +1,86 @@
'use client'
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const BimbinganBelajarDesaTextEditor = dynamic(() => import('../../_lib/bimbinganBelajarDesaTextEditor').then(mod => mod.BimbinganBelajarDesaTextEditor), {
ssr: false,
});
function EditTujuanProgram() {
const router = useRouter()
const editState = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!editState.findById.data) {
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (editState.findById.data) {
setJudul(editState.findById.data.judul ?? '')
setContent(editState.findById.data.deskripsi ?? '')
}
}, [editState.findById.data])
const submit = () => {
if (editState.findById.data) {
editState.findById.data.judul = judul;
editState.findById.data.deskripsi = content;
editState.update.save(editState.findById.data)
}
router.push('/admin/pendidikan/bimbingan-belajar-desa/tujuan-program')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Tujuan Program</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
value={judul}
onChange={(e) => setJudul(e.target.value)}
/>
<Text fw={"bold"}>Deskripsi</Text>
<BimbinganBelajarDesaTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={editState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditTujuanProgram;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateBimbinganBelajarDesa from '../../../_state/pendidikan/bimbingan-belajar-desa';
function Page() {
const router = useRouter()
const listPreview = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram)
useShallowEffect(() => {
listPreview.findById.load('edit')
}, [])
if (!listPreview.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Tujuan Program</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/tujuan-program/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import dataPendidikan from '../../../_state/pendidikan/data-pendidikan';
function EditDataPendidikan() {
const router = useRouter()
const params = useParams() as { id: string }
const stateDPM = useProxy(dataPendidikan)
const id = params.id
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateDPM.findUnique.load(id).then(() => {
const data = stateDPM.findUnique.data
if (data) {
stateDPM.update.form = {
name: data.name || "",
jumlah: data.jumlah || "",
}
}
})
}
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
stateDPM.update.id = id;
await stateDPM.update.submit();
router.push('/admin/pendidikan/data-pendidikan')
}
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 Data Pendidikan</Title>
<TextInput
label="Nama"
placeholder="masukkan nama"
value={stateDPM.update.form.name}
onChange={(val) => {
stateDPM.update.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah"
type="number"
placeholder="masukkan jumlah"
value={stateDPM.update.form.jumlah}
onChange={(val) => {
stateDPM.update.form.jumlah = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditDataPendidikan;

View File

@@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import dataPendidikan from '../../../_state/pendidikan/data-pendidikan';
function CreateDataPendidikan() {
const stateDPM = useProxy(dataPendidikan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
stateDPM.create.form = {
name: "",
jumlah: "",
}
}
const handleSubmit = async () => {
const id = await stateDPM.create.create();
if (id) {
const idStr = String(id);
await stateDPM.findUnique.load(idStr);
if (stateDPM.findUnique.data) {
setChartData([stateDPM.findUnique.data]);
}
}
resetForm();
router.push("/admin/pendidikan/data-pendidikan");
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Data Pendidikan</Title>
<Stack gap={"xs"}>
<TextInput
label="Nama"
type="text"
value={stateDPM.create.form.name}
placeholder="Masukkan nama"
onChange={(val) => {
stateDPM.create.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah"
type="number"
value={stateDPM.create.form.jumlah}
placeholder="Masukkan jumlah"
onChange={(val) => {
stateDPM.create.form.jumlah = val.currentTarget.value;
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateDataPendidikan;

View File

@@ -1,74 +1,169 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import dataPendidikan from '../../_state/pendidikan/data-pendidikan';
function DataPendidikan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Data Pendidikan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
title='Data Pendidikan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDataPendidikan/>
<ListDataPendidikan search={search} />
</Box>
);
}
function ListDataPendidikan() {
function ListDataPendidikan({ search }: { search: string }) {
type DPMrafik = {
id: string;
name: string;
jumlah: number;
}
const stateDPM = useProxy(dataPendidikan);
const [chartData, setChartData] = useState<DPMrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const handleDelete = () => {
if (selectedId) {
stateDPM.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateDPM.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateDPM.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (stateDPM.findMany.data) {
setChartData(stateDPM.findMany.data.map((item) => ({
id: item.id,
name: item.name,
jumlah: Number(item.jumlah)
})));
}
}, [stateDPM.findMany.data]);
const filteredData = (stateDPM.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.jumlah.toLowerCase().includes(keyword)
);
});
if (!stateDPM.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Data Pendidikan'
href='/admin/pendidikan/data-pendidikan/create'
<Stack gap={'xs'}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Data Pendidikan'
href='/admin/pendidikan/data-pendidikan/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Nomor</TableTh>
<TableTh>Nama Lengkap</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Jumlah</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.jumlah}</TableTd>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>1</Text>
</Box>
<Button color='green' onClick={() => router.push(`/admin/pendidikan/data-pendidikan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Email</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/data-pendidikan/detail')}>
<IconDeviceImacCog size={25} />
<Button
color='red'
disabled={stateDPM.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Data Pendidikan</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
</Stack>
</Paper>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Data Pendidikan</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Data Pendidikan" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik data pendidikan ini?'
/>
</Box>
)
);
}
export default DataPendidikan;

View File

@@ -0,0 +1,72 @@
/* 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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Jenjang Pendidikan",
value: "jenjangPendidikan",
href: "/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan"
},
{
label: "Lembaga",
value: "lembaga",
href: "/admin/pendidikan/info-sekolah-paud/lembaga"
},
{
label: "Siswa",
value: "siswa",
href: "/admin/pendidikan/info-sekolah-paud/siswa"
},
{
label: "Pengajar",
value: "pengajar",
href: "/admin/pendidikan/info-sekolah-paud/pengajar"
},
];
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}>Info Sekolah & PAUD</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 LayoutTabs;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditJenjangPendidikan() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan);
const [formData, setFormData] = useState({
nama: "",
});
useEffect(() => {
const loadJenjangPendidikan = async () => {
if (!id) return;
try {
const data = await stateJenjang.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateJenjang.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading jenjang pendidikan:", error);
toast.error("Gagal memuat data jenjang pendidikan");
}
};
loadJenjangPendidikan();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama jenjang pendidikan tidak boleh kosong');
return;
}
stateJenjang.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateJenjang.edit.id) {
stateJenjang.edit.id = id; // fallback
}
const success = await stateJenjang.edit.update();
if (success) {
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan");
}
} catch (error) {
console.error("Error updating jenjang pendidikan:", error);
// toast akan ditampilkan dari fungsi update
}
};
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 Jenjang Pendidikan</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenjang Pendidikan</Text>}
placeholder='Masukkan nama jenjang pendidikan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditJenjangPendidikan;

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import infoSekolahPaud from '../../../../_state/pendidikan/info-sekolah-paud';
function CreateJenjangPendidikan() {
const router = useRouter();
const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan)
useEffect(() => {
stateJenjang.findMany.load();
}, []);
const resetForm = () => {
stateJenjang.create.form = {
nama: "",
};
}
const handleSubmit = async () => {
await stateJenjang.create.create();
resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan")
}
return (
<Box>
<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 Jenjang Pendidikan</Title>
<TextInput
value={stateJenjang.create.form.nama}
onChange={(val) => {
stateJenjang.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenjang Pendidikan</Text>}
placeholder='Masukkan nama jenjang pendidikan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateJenjangPendidikan;

View File

@@ -0,0 +1,112 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import infoSekolahPaud from '../../../_state/pendidikan/info-sekolah-paud';
function JenjangPendidikan() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Jenjang Pendidikan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListJenjangPendidikan search={search} />
</Box>
);
}
function ListJenjangPendidikan({ search }: { search: string }) {
const stateList = useProxy(infoSekolahPaud.jenjangPendidikan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const handleHapus = () => {
if (selectedId) {
stateList.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
useShallowEffect(() => {
stateList.findMany.load()
}, [])
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!stateList.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jenjang Pendidikan'
href='/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenjang Pendidikan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus jenjang pendidikan ini?'
/>
</Box>
);
}
export default JenjangPendidikan;

View File

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

View File

@@ -0,0 +1,89 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Box, Button, Paper, Select, Stack, TextInput, Title } from '@mantine/core';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { IconArrowBack } from '@tabler/icons-react';
export default function EditLembaga() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const state = useProxy(infoSekolahPaud.lembagaPendidikan);
const jenjangPendidikanList = infoSekolahPaud.jenjangPendidikan.findMany.data;
const [form, setForm] = useState({
nama: '',
jenjangId: '',
});
useEffect(() => {
infoSekolahPaud.jenjangPendidikan.findMany.load();
if (id) {
state.edit.load(id).then(data => {
if (data) {
setForm({
nama: data.nama,
jenjangId: data.jenjangId,
});
}
});
}
}, [id]);
const handleSubmit = async () => {
if (!form.nama || !form.jenjangId) {
toast.warn("Nama dan jenjang pendidikan harus diisi");
return;
}
state.edit.id = id;
state.edit.form = form;
const result = await state.edit.update();
if (result) {
toast.success("Data berhasil diperbarui");
router.push('/admin/pendidikan/info-sekolah-paud/lembaga');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Edit Lembaga</Title>
<TextInput
label="Nama Lembaga"
placeholder="Masukkan nama lembaga"
value={form.nama}
onChange={(e) => setForm({ ...form, nama: e.currentTarget.value })}
/>
<Select
label="Jenjang Pendidikan"
placeholder="Pilih jenjang pendidikan"
searchable
data={jenjangPendidikanList?.map(p => ({
value: p.id,
label: p.nama,
})) || []}
value={form.jenjangId}
onChange={(val) => setForm({ ...form, jenjangId: val || '' })}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,115 @@
'use client'
import { useProxy } from 'valtio/utils';
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 colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
function DetailLembaga() {
const detailState = useProxy(infoSekolahPaud.lembagaPendidikan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/lembaga")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Lembaga</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenjang Pendidikan</Text>
<Text fz={"lg"} >{detailState.findUnique.data?.jenjangPendidikan?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jumlah Siswa</Text>
<Text fz={"lg"} >{detailState.findUnique.data?.siswa?.length}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jumlah Pengajar</Text>
<Text fz={"lg"} >{detailState.findUnique.data?.pengajar?.length}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus lembaga ini?'
/>
</Box>
);
}
export default DetailLembaga;

View File

@@ -0,0 +1,76 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import infoSekolahPaud from '../../../../_state/pendidikan/info-sekolah-paud';
function CreateLembaga() {
const router = useRouter();
const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan)
useEffect(() => {
stateLembaga.findMany.load();
infoSekolahPaud.jenjangPendidikan.findMany.load();
}, []);
const resetForm = () => {
stateLembaga.create.form = {
nama: "",
jenjangId: "",
};
}
const handleSubmit = async () => {
await stateLembaga.create.create();
resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/lembaga")
}
return (
<Box>
<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 Lembaga</Title>
<TextInput
value={stateLembaga.create.form.nama}
onChange={(val) => {
stateLembaga.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Lembaga</Text>}
placeholder='Masukkan nama lembaga'
/>
<Select
label="Jenjang Pendidikan"
placeholder="Pilih jenjang pendidikan"
searchable
data={infoSekolahPaud.jenjangPendidikan.findMany.data?.map(j => ({
value: j.id,
label: j.nama,
})) || []}
value={stateLembaga.create.form.jenjangId}
onChange={(val) => {
stateLembaga.create.form.jenjangId = val || '';
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateLembaga;

View File

@@ -0,0 +1,89 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } 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 JudulList from '../../../_com/judulList';
import infoSekolahPaud from '../../../_state/pendidikan/info-sekolah-paud';
function Lembaga() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Lembaga'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListLembaga search={search} />
</Box>
);
}
function ListLembaga({ search }: { search: string }) {
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
const router = useRouter()
useShallowEffect(() => {
stateList.findMany.load()
}, [])
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.jenjangPendidikan?.nama.toLowerCase().includes(keyword)
);
});
if (!stateList.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Lembaga'
href='/admin/pendidikan/info-sekolah-paud/lembaga/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Lembaga</TableTh>
<TableTh>Jenjang Pendidikan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.jenjangPendidikan?.nama}</TableTd>
<TableTd>
<Button color="blue" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default Lembaga;

View File

@@ -1,69 +0,0 @@
'use client'
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function InfoSekolahPaud() {
return (
<Box>
<HeaderSearch
title='Info Sekolah & PAUD'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListInfoSekolahPaud/>
</Box>
);
}
function ListInfoSekolahPaud() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Info Sekolah & PAUD'
href='/admin/pendidikan/info-sekolah-paud/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Nama Sekolah PAUD</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Sekolah PAUD</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/info-sekolah-paud/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default InfoSekolahPaud;

View File

@@ -0,0 +1,100 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Button, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormPengajar {
nama: string;
lembagaId: string;
}
function EditPengajar() {
const pengajarState = useProxy(infoSekolahPaud.pengajar)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState<FormPengajar>({
nama: '',
lembagaId: ''
})
useEffect(() => {
const loadPengajar = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await pengajarState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
lembagaId: data.lembagaId || '',
});
}
} catch (error) {
console.error("Error loading pengajar:", error);
toast.error("Gagal memuat data pengajar");
}
}
infoSekolahPaud.lembagaPendidikan.findMany.load()
loadPengajar();
}, [params?.id]);
const handleSubmit = async () => {
try {
pengajarState.edit.form = {
...pengajarState.edit.form,
nama: formData.nama.trim(),
lembagaId: formData.lembagaId.trim(),
}
await pengajarState.edit.update()
router.push("/admin/pendidikan/info-sekolah-paud/pengajar");
} catch (error) {
console.error("Error updating pengajar:", error);
toast.error("Terjadi kesalahan saat memperbarui pengajar");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Pengajar</Title>
<TextInput
value={formData.nama}
label={<Text fz="sm" fw="bold">Nama Pengajar</Text>}
placeholder="masukkan nama siswa"
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
/>
<Select
label="Lembaga Pendidikan"
data={infoSekolahPaud.lembagaPendidikan.findMany.data?.map(k => ({
value: k.id,
label: k.nama
})) ?? []}
value={formData.lembagaId}
onChange={(val) => setFormData({ ...formData, lembagaId: val ?? '' })}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPengajar;

View File

@@ -0,0 +1,107 @@
'use client'
import { useProxy } from 'valtio/utils';
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 colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
function DetailPengajar() {
const detailState = useProxy(infoSekolahPaud.pengajar)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
detailState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
detailState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/pengajar")
}
}
if (!detailState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Pengajar</Text>
{detailState.findUnique.data ? (
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{detailState.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lembaga</Text>
<Text fz={"lg"} >{detailState.findUnique.data?.lembaga?.nama}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (detailState.findUnique.data) {
setSelectedId(detailState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={detailState.delete.loading || !detailState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/pengajar/${detailState.findUnique.data.id}/edit`);
}
}}
disabled={!detailState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengajar ini?'
/>
</Box>
);
}
export default DetailPengajar;

View File

@@ -0,0 +1,75 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreatePengajar() {
const router = useRouter();
const stateCreate = useProxy(infoSekolahPaud.pengajar)
useEffect(() => {
stateCreate.findMany.load();
infoSekolahPaud.lembagaPendidikan.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
nama: "",
lembagaId: "",
};
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/pengajar")
}
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 Pengajar</Title>
<TextInput
value={stateCreate.create.form.nama}
onChange={(val) => {
stateCreate.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>}
placeholder='Masukkan nama'
/>
<Select
value={stateCreate.create.form.lembagaId}
onChange={(val) => {
stateCreate.create.form.lembagaId = val ?? "";
}}
label={<Text fw={"bold"} fz={"sm"}>Lembaga</Text>}
placeholder="Pilih lembaga"
data={
infoSekolahPaud.lembagaPendidikan.findMany.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePengajar;

View File

@@ -0,0 +1,90 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } 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 infoSekolahPaud from '../../../_state/pendidikan/info-sekolah-paud';
function Pengajar() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Pengajar'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPengajar search={search} />
</Box>
);
}
function ListPengajar({ search }: { search: string }) {
const listState = useProxy(infoSekolahPaud.pengajar)
const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.lembaga.nama.toLowerCase().includes(keyword)
);
});
if (!listState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Pengajar'
href='/admin/pendidikan/info-sekolah-paud/pengajar/create'
/>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama Pengajar</TableTh>
<TableTh>Lembaga</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/pengajar/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default Pengajar;

View File

@@ -0,0 +1,100 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
import colors from '@/con/colors';
import { Box, Button, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormSiswa {
nama: string;
lembagaId: string;
}
function EditSiswa() {
const siswaState = useProxy(infoSekolahPaud.siswa)
const params = useParams()
const router = useRouter()
const [formData, setFormData] = useState<FormSiswa>({
nama: '',
lembagaId: ''
})
useEffect(() => {
const loadSiswa = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await siswaState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
lembagaId: data.lembagaId || '',
});
}
} catch (error) {
console.error("Error loading siswa:", error);
toast.error("Gagal memuat data siswa");
}
}
infoSekolahPaud.lembagaPendidikan.findMany.load()
loadSiswa();
}, [params?.id]);
const handleSubmit = async () => {
try {
siswaState.edit.form = {
...siswaState.edit.form,
nama: formData.nama.trim(),
lembagaId: formData.lembagaId.trim(),
}
await siswaState.edit.update()
router.push("/admin/pendidikan/info-sekolah-paud/siswa");
} catch (error) {
console.error("Error updating siswa:", error);
toast.error("Terjadi kesalahan saat memperbarui siswa");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Siswa</Title>
<TextInput
value={formData.nama}
label={<Text fz="sm" fw="bold">Nama Siswa</Text>}
placeholder="masukkan nama siswa"
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
/>
<Select
label="Lembaga Pendidikan"
data={infoSekolahPaud.lembagaPendidikan.findMany.data?.map(k => ({
value: k.id,
label: k.nama
})) ?? []}
value={formData.lembagaId}
onChange={(val) => setFormData({ ...formData, lembagaId: val ?? '' })}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditSiswa;

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