diff --git a/.gitignore b/.gitignore
index fb70e68f..ebd64b35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,9 @@ next-env.d.ts
# uploads
/uploads
+# download
+/download
+
# cache
/cache
diff --git a/bun.lockb b/bun.lockb
index b7d17ba0..e30c0976 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 791da768..e4b3bc0b 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
"version": "0.1.5",
"private": true,
"scripts": {
- "dev": "bun --bun next dev --hostname 0.0.0.0",
- "build": "bun --bun next build",
- "start": "bun --bun next start"
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
},
"prisma": {
"seed": "bun run prisma/seed.ts"
@@ -19,6 +19,7 @@
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
+ "@emotion/react": "^11.14.0",
"@mantine/carousel": "^7.16.2",
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
@@ -26,6 +27,7 @@
"@mantine/dropzone": "^8.1.1",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",
+ "@mantine/modals": "^8.3.6",
"@mantine/tiptap": "^7.17.4",
"@paljs/types": "^8.1.0",
"@prisma/client": "^6.3.1",
@@ -39,19 +41,27 @@
"@tiptap/pm": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
+ "@types/adm-zip": "^0.5.7",
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
+ "@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
+ "adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"classnames": "^2.5.1",
+ "colors": "^1.4.0",
+ "date-fns": "^4.1.0",
"dayjs": "^1.11.13",
+ "dotenv": "^17.2.3",
"elysia": "^1.3.5",
- "embla-carousel-autoplay": "^8.5.2",
- "embla-carousel-react": "^7.1.0",
+ "embla-carousel": "^8.6.0",
+ "embla-carousel-autoplay": "^8.6.0",
+ "embla-carousel-react": "^8.6.0",
+ "extract-zip": "^2.0.1",
"form-data": "^4.0.2",
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
@@ -67,17 +77,20 @@
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
+ "nodemailer": "^7.0.10",
"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-exif-orientation-img": "^0.1.5",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
+ "react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"sharp": "^0.34.3",
diff --git a/prisma/data/kategori-berita.json b/prisma/data/desa/berita/kategori-berita.json
similarity index 87%
rename from prisma/data/kategori-berita.json
rename to prisma/data/desa/berita/kategori-berita.json
index ee0a53b0..4d777965 100644
--- a/prisma/data/kategori-berita.json
+++ b/prisma/data/desa/berita/kategori-berita.json
@@ -1,5 +1,4 @@
[
- { "name": "Semua" },
{ "name": "Pemerintahan" },
{ "name": "Pembangunan" },
{ "name": "Ekonomi" },
diff --git a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json
index 2259bea4..d39b6e98 100644
--- a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json
+++ b/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json
@@ -1,6 +1,6 @@
[
{
- "id": "1",
+ "id": "edit",
"name": "Pelayanan Penduduk Non-Permanent",
"deskripsi": "
Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.
"
}
diff --git a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json
index 8df36cf0..42e7ab7a 100644
--- a/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json
+++ b/prisma/data/desa/layanan/pelayananPerizinanBerusaha.json
@@ -1,6 +1,6 @@
[
{
- "id": "1",
+ "id": "edit",
"name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
"deskripsi": "Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.
",
"link" : "https://oss.go.id/"
diff --git a/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json b/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json
deleted file mode 100644
index 0f0e8271..00000000
--- a/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json
+++ /dev/null
@@ -1,8 +0,0 @@
-[
- {
- "id": "650e8400-e29b-41d4-a716-446655440001",
- "atasanId": "550e8400-e29b-41d4-a716-446655440001",
- "bawahanId": "550e8400-e29b-41d4-a716-446655440002",
- "tipe": "Langsung Melapor"
- }
-]
diff --git a/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json b/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json
new file mode 100644
index 00000000..0976a812
--- /dev/null
+++ b/prisma/data/ekonomi/struktur-organisasi/pegawai-bumdes.json
@@ -0,0 +1,91 @@
+[
+ {
+ "id": "cmgewz4gt000704ib91i3f169",
+ "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": "cmgewxfvw000004ibee5013f4",
+ "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": "cmgewxvqw000104ibgm5l8fzs",
+ "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": "cmgewy1g9000204ib2n7hbx0i",
+ "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": "cmgewybah000304ibgqhn1gm2",
+ "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": "cmgewygqz000404ib20sv8nvg",
+ "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": "cmgewyos1000504ibcu8o2gyk",
+ "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": "cmgewyxk7000604ib8djs3i6c",
+ "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
+ }
+
+]
\ No newline at end of file
diff --git a/prisma/data/ekonomi/struktur-organisasi/pegawai.json b/prisma/data/ekonomi/struktur-organisasi/pegawai.json
deleted file mode 100644
index 9d7a6437..00000000
--- a/prisma/data/ekonomi/struktur-organisasi/pegawai.json
+++ /dev/null
@@ -1,24 +0,0 @@
-[
- {
- "id": "550e8400-e29b-41d4-a716-446655440001",
- "namaLengkap": "Budi Santoso",
- "gelarAkademik": "S.IP",
- "tanggalMasuk": "2020-01-01T00:00:00.000Z",
- "email": "budi@desa.id",
- "telepon": "081234567891",
- "alamat": "Jl. Raya Desa No. 1",
- "posisiId": "kepala_desa",
- "isActive": true
- },
- {
- "id": "550e8400-e29b-41d4-a716-446655440002",
- "namaLengkap": "Ani Lestari",
- "gelarAkademik": "S.Pd",
- "tanggalMasuk": "2020-02-01T00:00:00.000Z",
- "email": "ani@desa.id",
- "telepon": "081234567892",
- "alamat": "Jl. Raya Desa No. 2",
- "posisiId": "sekretaris_desa",
- "isActive": true
- }
- ]
\ No newline at end of file
diff --git a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json
new file mode 100644
index 00000000..4a1699d7
--- /dev/null
+++ b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json
@@ -0,0 +1,159 @@
+[
+ [
+ {
+ "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"
+ }
+ ]
+ ]
+
\ No newline at end of file
diff --git a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json
deleted file mode 100644
index 7596e168..00000000
--- a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json
+++ /dev/null
@@ -1,27 +0,0 @@
-[
- {
- "id": "kepala_desa",
- "nama": "Kepala Desa",
- "deskripsi": "Kepala Desa",
- "hierarki": 1
- },
- {
- "id": "sekretaris_desa",
- "nama": "Sekretaris Desa",
- "deskripsi": "Sekretaris Desa",
- "hierarki": 2
- },
- {
- "id": "bendahara_desa",
- "nama": "Bendahara Desa",
- "deskripsi": "Bendahara Desa",
- "hierarki": 3
- },
- {
- "id": "staff_umum",
- "nama": "Staff Umum",
- "deskripsi": "Staff Umum",
- "hierarki": 4
- }
- ]
-
\ No newline at end of file
diff --git a/prisma/data/landing-page/profile/mediaSosial.json b/prisma/data/landing-page/profile/mediaSosial.json
index e6df4799..9af092a0 100644
--- a/prisma/data/landing-page/profile/mediaSosial.json
+++ b/prisma/data/landing-page/profile/mediaSosial.json
@@ -1,16 +1,4 @@
[
- {
- "id": "cmds8w2q60002vnbe6i8qhkuo",
- "name": "Telephone Desa Darmasaba",
- "iconUrl": "081239580000",
- "imageId": "cmff3nv180003vn6h5jvedidq"
- },
- {
- "id": "cmds8z7u20005vnbegyyvnbk0",
- "name": "Email Desa Darmasaba",
- "iconUrl": "desadarmasaba@badungkab.go.id",
- "imageId": "cmff3ll130001vn6hkhls3f5y"
- },
{
"id": "cmds9023u0008vnbe3oxmhwyf",
"name": "Desa Darmasaba",
diff --git a/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json
new file mode 100644
index 00000000..874e2e32
--- /dev/null
+++ b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json
@@ -0,0 +1,6 @@
+[
+ { "nama": "Kebersihan" },
+ { "nama": "Infrastruktur" },
+ { "nama": "Sosial" },
+ { "nama": "Lingkungan" }
+ ]
\ No newline at end of file
diff --git a/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json b/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json
new file mode 100644
index 00000000..a2d63947
--- /dev/null
+++ b/prisma/data/pendidikan/info-sekolah/jenjang-pendidikan.json
@@ -0,0 +1,9 @@
+[
+ { "id": "cmghqwjs4000404l8c5uvc300", "nama": "PAUD" },
+ { "id": "cmghqwjs4000404l8c5uvc301", "nama": "TK" },
+ { "id": "cmghqwjs4000404l8c5uvc302", "nama": "SD" },
+ { "id": "cmghqwjs4000404l8c5uvc303", "nama": "SMP" },
+ { "id": "cmghqwjs4000404l8c5uvc304", "nama": "SMA" },
+ { "id": "cmghqwjs4000404l8c5uvc305", "nama": "SMK" }
+ ]
+
\ No newline at end of file
diff --git a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json
index e7a7a9cf..713cb799 100644
--- a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json
+++ b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json
@@ -1,6 +1,6 @@
[
{
- "id": "550e8400-e29b-41d4-a716-446655440001",
+ "id": "cmgewz4gt000704ib91i3f169",
"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",
@@ -11,7 +11,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440002",
+ "id": "cmgewxfvw000004ibee5013f4",
"namaLengkap": "I Ketut Suwanta",
"gelarAkademik": "S.Pt",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -22,7 +22,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440006",
+ "id": "cmgewxvqw000104ibgm5l8fzs",
"namaLengkap": "Ni Wayan Supardiati",
"gelarAkademik": "S.Pd",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -33,7 +33,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440011",
+ "id": "cmgewy1g9000204ib2n7hbx0i",
"namaLengkap": "I Wayan Agus Juni Artha Saputra",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -44,7 +44,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440012",
+ "id": "cmgewybah000304ibgqhn1gm2",
"namaLengkap": "I Wayan Sueca",
"gelarAkademik": "S.H.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -55,7 +55,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440017",
+ "id": "cmgewygqz000404ib20sv8nvg",
"namaLengkap": "Si Gede Ketut Astawa",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -66,7 +66,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440018",
+ "id": "cmgewyos1000504ibcu8o2gyk",
"namaLengkap": "I Kadek Arya Minarta",
"gelarAkademik": "S.T.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
@@ -77,7 +77,7 @@
"isActive": true
},
{
- "id": "550e8400-e29b-41d4-a716-446655440021",
+ "id": "cmgewyxk7000604ib8djs3i6c",
"namaLengkap": "I Gede Andika Pradnya Diputra",
"gelarAkademik": "S.E.",
"tanggalMasuk": "2020-02-01T00:00:00.000Z",
diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json
index 79da3188..4d2a1046 100644
--- a/prisma/data/user/roles.json
+++ b/prisma/data/user/roles.json
@@ -1,29 +1,32 @@
[
- {
- "id": "1",
- "name": "ADMIN DESA",
- "description": "Administrator Desa",
- "permissions": ["manage_users", "manage_content", "view_reports"],
- "isActive": true,
- "createdAt": "2025-09-01T00:00:00.000Z",
- "updatedAt": "2025-09-01T00:00:00.000Z"
- },
- {
- "id": "2",
- "name": "ADMIN KESEHATAN",
- "description": "Administrator Bidang Kesehatan",
- "permissions": ["manage_health_data", "view_reports"],
- "isActive": true,
- "createdAt": "2025-09-01T00:00:00.000Z",
- "updatedAt": "2025-09-01T00:00:00.000Z"
- },
- {
- "id": "3",
- "name": "ADMIN SEKOLAH",
- "description": "Administrator Sekolah",
- "permissions": ["manage_school_data", "view_reports"],
- "isActive": true,
- "createdAt": "2025-09-01T00:00:00.000Z",
- "updatedAt": "2025-09-01T00:00:00.000Z"
- }
- ]
\ No newline at end of file
+ {
+ "id": "0",
+ "name": "DEVELOPER",
+ "description": "Developer",
+ "isActive": true
+ },
+ {
+ "id": "1",
+ "name": "SUPER ADMIN",
+ "description": "Administrator",
+ "isActive": true
+ },
+ {
+ "id": "2",
+ "name": "ADMIN DESA",
+ "description": "Administrator Desa",
+ "isActive": true
+ },
+ {
+ "id": "3",
+ "name": "ADMIN KESEHATAN",
+ "description": "Administrator Bidang Kesehatan",
+ "isActive": true
+ },
+ {
+ "id": "4",
+ "name": "ADMIN PENDIDIKAN",
+ "description": "Administrator Bidang Pendidikan",
+ "isActive": true
+ }
+]
diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json
index 2f44c667..733aeba4 100644
--- a/prisma/data/user/users.json
+++ b/prisma/data/user/users.json
@@ -1,32 +1,10 @@
[
- {
- "id": "1",
- "nama": "Admin Desa",
- "nomor": "089647037426",
- "roleId": "1",
- "isActive": true,
- "lastLogin": "2025-08-31T10:00:00.000Z",
- "createdAt": "2025-09-01T00:00:00.000Z",
- "updatedAt": "2025-09-01T00:00:00.000Z"
- },
- {
- "id": "2",
- "nama": "Admin Kesehatan",
- "nomor": "082339004198",
- "roleId": "2",
- "isActive": true,
- "lastLogin": null,
- "createdAt": "2025-09-01T00:00:00.000Z",
- "updatedAt": "2025-09-01T00:00:00.000Z"
- },
- {
- "id": "3",
- "nama": "Admin Sekolah",
- "nomor": "085237157222",
- "roleId": "3",
- "isActive": true,
- "lastLogin": null,
- "createdAt": "2025-09-01T00:00:00.000Z",
- "updatedAt": "2025-09-01T00:00:00.000Z"
- }
- ]
\ No newline at end of file
+ {
+ "id": "cmie1o0zh0002vn132vtzg7hh",
+ "username": "SuperAdmin-Nico",
+ "nomor": "6289647037426",
+ "roleId": 0,
+ "isActive": true,
+ "sessionInvalid": false
+ }
+]
diff --git a/prisma/migrations/20251119062255_add_unique_username/migration.sql b/prisma/migrations/20251119062255_add_unique_username/migration.sql
new file mode 100644
index 00000000..d8d9e087
--- /dev/null
+++ b/prisma/migrations/20251119062255_add_unique_username/migration.sql
@@ -0,0 +1,1127 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `kelahiranKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `kematianBayi` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `kematianKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `tahun` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
+ - You are about to drop the column `jumlah` on the `GrafikKepuasan` table. All the data in the column will be lost.
+ - You are about to drop the column `label` on the `GrafikKepuasan` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KolaborasiInovasi` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
+ - You are about to drop the column `imageId` on the `KontakItem` table. All the data in the column will be lost.
+ - You are about to drop the column `kategoriId` on the `KontakItem` table. All the data in the column will be lost.
+ - You are about to drop the column `programKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost.
+ - You are about to drop the column `tipsKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost.
+ - You are about to drop the column `videoKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost.
+ - You are about to drop the column `kategori` on the `PotensiDesa` table. All the data in the column will be lost.
+ - You are about to drop the column `ikonUrl` on the `ProgramKemiskinan` table. All the data in the column will be lost.
+ - You are about to drop the `ProgramKeamanan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `TipsKeamanan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `VideoKeamanan` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `hubungan_organisasi` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `pegawai` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `posisi_organisasi` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `struktur_organisasi` table. If the table is not empty, all the data it contains will be lost.
+ - Added the required column `kelahiranId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `kematianId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `category` to the `FileStorage` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `alamat` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `jenisKelamin` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `nama` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `penyakit` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `tanggal` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `whatsapp` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `icon` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `kategoriId` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `icon` to the `KontakItem` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `notelp` to the `LowonganPekerjaan` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `name` to the `MediaSosial` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `kontak` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `deskripsi` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `deskripsiSingkat` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `judul` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `linkVideo` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `jadwalPelayanan` to the `Posyandu` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `icon` to the `ProgramKemiskinan` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- CreateEnum
+CREATE TYPE "JenisKelamin" AS ENUM ('LAKI_LAKI', 'PEREMPUAN');
+
+-- CreateEnum
+CREATE TYPE "Agama" AS ENUM ('ISLAM', 'KRISTEN_PROTESTAN', 'KRISTEN_KATOLIK', 'HINDU', 'BUDDHA', 'KONGHUCU', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "StatusPernikahan" AS ENUM ('BELUM_MENIKAH', 'MENIKAH', 'JANDA_DUDA');
+
+-- CreateEnum
+CREATE TYPE "UkuranBaju" AS ENUM ('S', 'M', 'L', 'XL', 'XXL', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "StatusPeminjaman" AS ENUM ('Dipinjam', 'Dikembalikan', 'Terlambat', 'Dibatalkan');
+
+-- DropForeignKey
+ALTER TABLE "JadwalKegiatan" DROP CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KolaborasiInovasi" DROP CONSTRAINT "KolaborasiInovasi_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KontakDaruratKeamanan" DROP CONSTRAINT "KontakDaruratKeamanan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "MediaSosial" DROP CONSTRAINT "MediaSosial_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PelayananSuratKeterangan" DROP CONSTRAINT "PelayananSuratKeterangan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "Penghargaan" DROP CONSTRAINT "Penghargaan_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_atasanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_bawahanId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_imageId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_posisiId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_pegawaiId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey";
+
+-- AlterTable
+ALTER TABLE "ArtikelKesehatan" ADD COLUMN "imageId" TEXT;
+
+-- AlterTable
+ALTER TABLE "DataKematian_Kelahiran" DROP COLUMN "kelahiranKasar",
+DROP COLUMN "kematianBayi",
+DROP COLUMN "kematianKasar",
+DROP COLUMN "tahun",
+ADD COLUMN "kelahiranId" TEXT NOT NULL,
+ADD COLUMN "kematianId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "FileStorage" ADD COLUMN "category" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "GrafikKepuasan" DROP COLUMN "jumlah",
+DROP COLUMN "label",
+ADD COLUMN "alamat" TEXT NOT NULL,
+ADD COLUMN "jenisKelamin" TEXT NOT NULL,
+ADD COLUMN "nama" TEXT NOT NULL,
+ADD COLUMN "penyakit" TEXT NOT NULL,
+ADD COLUMN "tanggal" TIMESTAMP(3) NOT NULL;
+
+-- AlterTable
+ALTER TABLE "JadwalKegiatan" ALTER COLUMN "pendaftaranJadwalKegiatanId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KolaborasiInovasi" DROP COLUMN "imageId";
+
+-- AlterTable
+ALTER TABLE "KontakDarurat" ADD COLUMN "whatsapp" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "imageId",
+ADD COLUMN "deletedAt" TIMESTAMP(3),
+ADD COLUMN "icon" TEXT NOT NULL,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "kategoriId" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "KontakItem" DROP COLUMN "imageId",
+DROP COLUMN "kategoriId",
+ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "icon" TEXT NOT NULL,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
+
+-- AlterTable
+ALTER TABLE "LaporanPublik" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
+ALTER COLUMN "status" SET DEFAULT 'Proses';
+
+-- AlterTable
+ALTER TABLE "LowonganPekerjaan" ADD COLUMN "notelp" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "MediaSosial" ADD COLUMN "name" TEXT NOT NULL,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PasarDesa" ADD COLUMN "kontak" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PelayananSuratKeterangan" ADD COLUMN "image2Id" TEXT,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PencegahanKriminalitas" DROP COLUMN "programKeamananId",
+DROP COLUMN "tipsKeamananId",
+DROP COLUMN "videoKeamananId",
+ADD COLUMN "deskripsi" TEXT NOT NULL,
+ADD COLUMN "deskripsiSingkat" TEXT NOT NULL,
+ADD COLUMN "judul" TEXT NOT NULL,
+ADD COLUMN "linkVideo" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Penghargaan" ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Posyandu" ADD COLUMN "jadwalPelayanan" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "PotensiDesa" DROP COLUMN "kategori",
+ADD COLUMN "kategoriId" TEXT,
+ALTER COLUMN "imageId" DROP NOT NULL;
+
+-- AlterTable
+ALTER TABLE "ProgramKemiskinan" DROP COLUMN "ikonUrl",
+ADD COLUMN "icon" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "StrukturPPID" ADD COLUMN "pegawaiPPIDId" TEXT,
+ADD COLUMN "posisiOrganisasiPPIDId" TEXT;
+
+-- DropTable
+DROP TABLE "ProgramKeamanan";
+
+-- DropTable
+DROP TABLE "TipsKeamanan";
+
+-- DropTable
+DROP TABLE "VideoKeamanan";
+
+-- DropTable
+DROP TABLE "hubungan_organisasi";
+
+-- DropTable
+DROP TABLE "pegawai";
+
+-- DropTable
+DROP TABLE "posisi_organisasi";
+
+-- DropTable
+DROP TABLE "struktur_organisasi";
+
+-- CreateTable
+CREATE TABLE "DesaAntiKorupsi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "fileId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DesaAntiKorupsi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriDesaAntiKorupsi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriDesaAntiKorupsi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SdgsDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "jumlah" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "SdgsDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "APBDes" (
+ "id" TEXT NOT NULL,
+ "tahun" INTEGER,
+ "name" TEXT,
+ "deskripsi" TEXT,
+ "jumlah" TEXT,
+ "imageId" TEXT,
+ "fileId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "APBDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "APBDesItem" (
+ "id" TEXT NOT NULL,
+ "kode" TEXT NOT NULL,
+ "uraian" TEXT NOT NULL,
+ "anggaran" DOUBLE PRECISION NOT NULL,
+ "realisasi" DOUBLE PRECISION NOT NULL,
+ "selisih" DOUBLE PRECISION NOT NULL,
+ "persentase" DOUBLE PRECISION NOT NULL,
+ "tipe" TEXT,
+ "level" INTEGER NOT NULL,
+ "parentId" TEXT,
+ "apbdesId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "APBDesItem_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PrestasiDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PrestasiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriPrestasiDesa" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriPrestasiDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Responden" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "tanggal" DATE NOT NULL,
+ "jenisKelaminId" TEXT NOT NULL,
+ "ratingId" TEXT NOT NULL,
+ "kelompokUmurId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Responden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisKelaminResponden" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenisKelaminResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PilihanRatingResponden" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PilihanRatingResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UmurResponden" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "UmurResponden_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PosisiOrganisasiPPID" (
+ "id" TEXT NOT NULL,
+ "nama" VARCHAR(100) NOT NULL,
+ "deskripsi" TEXT,
+ "hierarki" INTEGER NOT NULL,
+ "parentId" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PosisiOrganisasiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PegawaiPPID" (
+ "id" TEXT NOT NULL,
+ "namaLengkap" VARCHAR(255) NOT NULL,
+ "gelarAkademik" VARCHAR(100),
+ "imageId" TEXT,
+ "tanggalMasuk" DATE,
+ "email" VARCHAR(255),
+ "telepon" VARCHAR(20),
+ "alamat" TEXT,
+ "posisiId" VARCHAR(50) NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PegawaiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StrukturOrganisasiPPID" (
+ "id" TEXT NOT NULL,
+ "posisiOrganisasiId" VARCHAR(50) NOT NULL,
+ "pegawaiId" TEXT NOT NULL,
+ "hubunganOrganisasiId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "StrukturOrganisasiPPID_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PerbekelDariMasaKeMasa" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "periode" TEXT NOT NULL,
+ "imageId" TEXT,
+ "daerah" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PerbekelDariMasaKeMasa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriPotensi" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriPotensi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AjukanPermohonan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "nik" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "nomorKk" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "AjukanPermohonan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Kelahiran" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "tanggal" TIMESTAMP(3) NOT NULL,
+ "jenisKelamin" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Kelahiran_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Kematian" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "tanggal" TIMESTAMP(3) NOT NULL,
+ "jenisKelamin" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "penyebab" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Kematian_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KontakDaruratToItem" (
+ "id" TEXT NOT NULL,
+ "kontakDaruratId" TEXT NOT NULL,
+ "kontakItemId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "KontakDaruratToItem_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StrukturBumDes" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "posisiOrganisasiBumDesId" TEXT,
+ "pegawaiBumDesId" TEXT,
+
+ CONSTRAINT "StrukturBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PosisiOrganisasiBumDes" (
+ "id" TEXT NOT NULL,
+ "nama" VARCHAR(100) NOT NULL,
+ "deskripsi" TEXT,
+ "hierarki" INTEGER NOT NULL,
+ "parentId" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PosisiOrganisasiBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PegawaiBumDes" (
+ "id" TEXT NOT NULL,
+ "namaLengkap" VARCHAR(255) NOT NULL,
+ "gelarAkademik" VARCHAR(100),
+ "imageId" TEXT,
+ "tanggalMasuk" DATE,
+ "email" VARCHAR(255),
+ "telepon" VARCHAR(20),
+ "alamat" TEXT,
+ "posisiId" VARCHAR(50) NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "PegawaiBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "StrukturOrganisasiBumDes" (
+ "id" TEXT NOT NULL,
+ "posisiOrganisasiId" VARCHAR(50) NOT NULL,
+ "pegawaiId" TEXT NOT NULL,
+ "hubunganOrganisasiId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "StrukturOrganisasiBumDes_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "MitraKolaborasi" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "imageId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "MitraKolaborasi_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenjangPendidikan" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenjangPendidikan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Lembaga" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "jenjangId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Lembaga_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Siswa" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "lembagaId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Siswa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Pengajar" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "lembagaId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "Pengajar_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KeunggulanProgram" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KeunggulanProgram_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "BeasiswaPendaftar" (
+ "id" TEXT NOT NULL,
+ "namaLengkap" TEXT NOT NULL,
+ "nis" TEXT,
+ "kelas" TEXT,
+ "jenisKelamin" "JenisKelamin" NOT NULL,
+ "alamatDomisili" TEXT,
+ "tempatLahir" TEXT NOT NULL,
+ "tanggalLahir" TIMESTAMP(3) NOT NULL,
+ "namaOrtu" TEXT,
+ "nik" TEXT NOT NULL,
+ "pekerjaanOrtu" TEXT,
+ "penghasilan" TEXT,
+ "noHp" TEXT NOT NULL,
+ "kewarganegaraan" TEXT,
+ "agama" "Agama",
+ "alamatKTP" TEXT,
+ "email" TEXT,
+ "statusPernikahan" "StatusPernikahan",
+ "ukuranBaju" "UkuranBaju",
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "BeasiswaPendaftar_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanProgram" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanProgram_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ProgramUnggulan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "ProgramUnggulan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanBimbinganBelajarDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanBimbinganBelajarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "LokasiJadwalBimbinganBelajarDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "LokasiJadwalBimbinganBelajarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "FasilitasBimbinganBelajarDesa" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "FasilitasBimbinganBelajarDesa_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TujuanPendidikanNonFormal" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TujuanPendidikanNonFormal_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "TempatKegiatan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "TempatKegiatan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "JenisProgramYangDiselenggarakan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "JenisProgramYangDiselenggarakan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataPerpustakaan" (
+ "id" TEXT NOT NULL,
+ "judul" TEXT NOT NULL,
+ "deskripsi" TEXT NOT NULL,
+ "kategoriId" TEXT NOT NULL,
+ "imageId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DataPerpustakaan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KategoriBuku" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "KategoriBuku_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PeminjamanBuku" (
+ "id" TEXT NOT NULL,
+ "nama" TEXT NOT NULL,
+ "noTelp" TEXT NOT NULL,
+ "alamat" TEXT NOT NULL,
+ "bukuId" TEXT NOT NULL,
+ "tanggalPinjam" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "batasKembali" TIMESTAMP(3) NOT NULL,
+ "tanggalKembali" TIMESTAMP(3),
+ "status" "StatusPeminjaman" NOT NULL DEFAULT 'Dipinjam',
+ "catatan" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "PeminjamanBuku_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "username" TEXT NOT NULL,
+ "nomor" TEXT NOT NULL,
+ "roleId" TEXT NOT NULL DEFAULT '1',
+ "instansi" TEXT,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "lastLogin" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "roles" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "permissions" JSONB NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+
+ CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "KodeOtp" (
+ "id" TEXT NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "nomor" TEXT NOT NULL,
+ "otp" INTEGER NOT NULL,
+
+ CONSTRAINT "KodeOtp_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "permissions" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "UserSession" (
+ "id" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expires" TIMESTAMP(3),
+ "active" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "userId" TEXT NOT NULL,
+
+ CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DataPendidikan" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "jumlah" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+
+ CONSTRAINT "DataPendidikan_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DesaAntiKorupsi_name_key" ON "DesaAntiKorupsi"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KategoriDesaAntiKorupsi_name_key" ON "KategoriDesaAntiKorupsi"("name");
+
+-- CreateIndex
+CREATE INDEX "APBDesItem_kode_idx" ON "APBDesItem"("kode");
+
+-- CreateIndex
+CREATE INDEX "APBDesItem_level_idx" ON "APBDesItem"("level");
+
+-- CreateIndex
+CREATE INDEX "APBDesItem_apbdesId_idx" ON "APBDesItem"("apbdesId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "KategoriPrestasiDesa_name_key" ON "KategoriPrestasiDesa"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Responden_name_key" ON "Responden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "JenisKelaminResponden_name_key" ON "JenisKelaminResponden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PilihanRatingResponden_name_key" ON "PilihanRatingResponden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UmurResponden_name_key" ON "UmurResponden"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PegawaiPPID_email_key" ON "PegawaiPPID"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PegawaiBumDes_email_key" ON "PegawaiBumDes"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "BeasiswaPendaftar_nik_key" ON "BeasiswaPendaftar"("nik");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "BeasiswaPendaftar_email_key" ON "BeasiswaPendaftar"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_nomor_key" ON "User"("nomor");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserSession_userId_key" ON "UserSession"("userId");
+
+-- AddForeignKey
+ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriDesaAntiKorupsi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SdgsDesa" ADD CONSTRAINT "SdgsDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "APBDesItem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_apbdesId_fkey" FOREIGN KEY ("apbdesId") REFERENCES "APBDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPrestasiDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Responden" ADD CONSTRAINT "Responden_jenisKelaminId_fkey" FOREIGN KEY ("jenisKelaminId") REFERENCES "JenisKelaminResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Responden" ADD CONSTRAINT "Responden_ratingId_fkey" FOREIGN KEY ("ratingId") REFERENCES "PilihanRatingResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Responden" ADD CONSTRAINT "Responden_kelompokUmurId_fkey" FOREIGN KEY ("kelompokUmurId") REFERENCES "UmurResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_posisiOrganisasiPPIDId_fkey" FOREIGN KEY ("posisiOrganisasiPPIDId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_pegawaiPPIDId_fkey" FOREIGN KEY ("pegawaiPPIDId") REFERENCES "PegawaiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PosisiOrganisasiPPID" ADD CONSTRAINT "PosisiOrganisasiPPID_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PerbekelDariMasaKeMasa" ADD CONSTRAINT "PerbekelDariMasaKeMasa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_image2Id_fkey" FOREIGN KEY ("image2Id") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AjukanPermohonan" ADD CONSTRAINT "AjukanPermohonan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "PelayananSuratKeterangan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey" FOREIGN KEY ("pendaftaranJadwalKegiatanId") REFERENCES "PendaftaranJadwalKegiatan"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kematianId_fkey" FOREIGN KEY ("kematianId") REFERENCES "Kematian"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kelahiranId_fkey" FOREIGN KEY ("kelahiranId") REFERENCES "Kelahiran"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakDaruratId_fkey" FOREIGN KEY ("kontakDaruratId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakItemId_fkey" FOREIGN KEY ("kontakItemId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_posisiOrganisasiBumDesId_fkey" FOREIGN KEY ("posisiOrganisasiBumDesId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_pegawaiBumDesId_fkey" FOREIGN KEY ("pegawaiBumDesId") REFERENCES "PegawaiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PosisiOrganisasiBumDes" ADD CONSTRAINT "PosisiOrganisasiBumDes_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "MitraKolaborasi" ADD CONSTRAINT "MitraKolaborasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Lembaga" ADD CONSTRAINT "Lembaga_jenjangId_fkey" FOREIGN KEY ("jenjangId") REFERENCES "JenjangPendidikan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Siswa" ADD CONSTRAINT "Siswa_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Pengajar" ADD CONSTRAINT "Pengajar_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriBuku"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PeminjamanBuku" ADD CONSTRAINT "PeminjamanBuku_bukuId_fkey" FOREIGN KEY ("bukuId") REFERENCES "DataPerpustakaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/safeseedUnique.ts b/prisma/safeseedUnique.ts
new file mode 100644
index 00000000..92d16071
--- /dev/null
+++ b/prisma/safeseedUnique.ts
@@ -0,0 +1,30 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// helpers/safeSeedUnique.ts
+import { PrismaClient } from "@prisma/client";
+
+const prisma = new PrismaClient();
+
+/**
+ * Helper generic buat seed dengan upsert aman
+ */
+export async function safeSeedUnique(
+ model: T,
+ where: Record,
+ data: Record
+) {
+ const m = prisma[model];
+
+ if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`);
+
+ try {
+ // @ts-expect-error upsert dynamic
+ await m.upsert({
+ where,
+ update: data,
+ create: { ...where, ...data },
+ });
+ console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`);
+ } catch (err) {
+ console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err);
+ }
+}
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 54166bfc..1e73563b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -81,7 +81,7 @@ model FileStorage {
PelayananSuratKeteranganImage PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage")
PelayananSuratKeteranganImage2 PelayananSuratKeterangan[] @relation("PelayananSuratKeteranganImage2")
PasarDesa PasarDesa[]
- Pegawai Pegawai[]
+ PegawaiBumDes PegawaiBumDes[]
DesaDigital DesaDigital[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
@@ -101,6 +101,7 @@ model FileStorage {
MitraKolaborasi MitraKolaborasi[]
ArtikelKesehatan ArtikelKesehatan[]
+ StrukturBumDes StrukturBumDes[]
}
//========================================= MENU LANDING PAGE ========================================= //
@@ -142,7 +143,7 @@ model MediaSosial {
isActive Boolean @default(true)
}
-//========================================= PROFILE ========================================= //
+//========================================= DESA ANTI KORUPSI ========================================= //
model DesaAntiKorupsi {
id String @id @default(cuid())
name String @unique
@@ -183,18 +184,46 @@ model SdgsDesa {
//========================================= APBDes ========================================= //
model APBDes {
id String @id @default(cuid())
- name String
- jumlah String
+ tahun Int?
+ name String? // misalnya: "APBDes Tahun 2025"
+ deskripsi String?
+ jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items)
+ items APBDesItem[]
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String?
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
fileId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime? // opsional, tidak perlu default now()
isActive Boolean @default(true)
}
+model APBDesItem {
+ id String @id @default(cuid())
+ kode String // contoh: "4", "4.1", "4.1.2"
+ uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
+ anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
+ realisasi Float
+ selisih Float // realisasi - anggaran
+ persentase Float
+ tipe String? // (realisasi / anggaran) * 100
+ level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
+ parentId String? // untuk relasi hierarki
+ parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
+ children APBDesItem[] @relation("APBDesItemParent")
+ apbdesId String
+ apbdes APBDes @relation(fields: [apbdesId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
+
+ @@index([kode])
+ @@index([level])
+ @@index([apbdesId])
+}
+
//========================================= PRESTASI DESA ========================================= //
model PrestasiDesa {
id String @id @default(cuid())
@@ -286,49 +315,51 @@ model StrukturPPID {
}
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?
- isActive Boolean @default(true)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
- children PosisiOrganisasiPPID[] @relation("Parent")
+ id String @id @default(cuid())
+ nama String @db.VarChar(100)
+ deskripsi String? @db.Text
+ hierarki Int
+ pegawai PegawaiPPID[]
+ strukturOrganisasi StrukturPPID[] // Relasi balik
+ parentId String?
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
+ children PosisiOrganisasiPPID[] @relation("Parent")
+ StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
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
+ 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
+ StrukturOrganisasiPPID StrukturOrganisasiPPID[]
}
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
+ id String @id @default(uuid())
+ posisiOrganisasiId String @db.VarChar(50)
+ pegawaiId String
+ hubunganOrganisasiId String
+ posisiOrganisasi PosisiOrganisasiPPID @relation(fields: [posisiOrganisasiId], references: [id])
+ pegawai PegawaiPPID @relation(fields: [pegawaiId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
deletedAt DateTime?
- isActive Boolean @default(true)
+ isActive Boolean @default(true)
}
// ========================================= VISI MISI PPID ========================================= //
@@ -850,7 +881,7 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
- pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
+ pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1167,6 +1198,7 @@ model KontakDarurat {
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
+ whatsapp String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1340,6 +1372,7 @@ model PasarDesa {
harga Int
rating Float
alamatUsaha String
+ kontak String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1382,6 +1415,7 @@ model LowonganPekerjaan {
gaji String
deskripsi String
kualifikasi String
+ notelp String
tanggalPosting DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@ -1391,79 +1425,67 @@ model LowonganPekerjaan {
// ========================================= STRUKTUR ORGANISASI ========================================= //
-model PosisiOrganisasi {
- id String @id @default(uuid()) @db.VarChar(50)
- nama String @db.VarChar(100)
- deskripsi String? @db.Text
- hierarki Int
-
- pegawai Pegawai[]
- strukturOrganisasi StrukturOrganisasi[] // Relasi balik
- StrukturOrganisasiPPID StrukturOrganisasiPPID[]
- isActive Boolean @default(true)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- @@map("posisi_organisasi")
+model StrukturBumDes {
+ 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)
+ PosisiOrganisasiBumDes PosisiOrganisasiBumDes? @relation(fields: [posisiOrganisasiBumDesId], references: [id])
+ posisiOrganisasiBumDesId String?
+ PegawaiBumDes PegawaiBumDes? @relation(fields: [pegawaiBumDesId], references: [id])
+ pegawaiBumDesId String?
}
-model Pegawai {
- id String @id @default(uuid()) @db.Uuid
- 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 PosisiOrganisasi @relation(fields: [posisiId], references: [id])
-
- sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan")
- sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan")
-
- strukturOrganisasi StrukturOrganisasi[] // Relasi balik
- StrukturOrganisasiPPID StrukturOrganisasiPPID[]
-
- @@map("pegawai")
+model PosisiOrganisasiBumDes {
+ id String @id @default(cuid())
+ nama String @db.VarChar(100)
+ deskripsi String? @db.Text
+ hierarki Int
+ pegawai PegawaiBumDes[]
+ strukturOrganisasi StrukturBumDes[] // Relasi balik
+ parentId String?
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ parent PosisiOrganisasiBumDes? @relation("Parent", fields: [parentId], references: [id])
+ children PosisiOrganisasiBumDes[] @relation("Parent")
+ StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
-model HubunganOrganisasi {
- id String @id @default(uuid()) @db.Uuid
- atasanId String @db.Uuid
- bawahanId String @db.Uuid
- tipe String? @db.VarChar(50)
-
- atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id])
- bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id])
-
- strukturOrganisasi StrukturOrganisasi[] // Relasi balik
-
- @@unique([atasanId, bawahanId])
- @@map("hubungan_organisasi")
+model PegawaiBumDes {
+ 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 PosisiOrganisasiBumDes @relation(fields: [posisiId], references: [id])
+ strukturOrganisasi StrukturBumDes[] // Relasi balik
+ StrukturOrganisasiBumDes StrukturOrganisasiBumDes[]
}
-model StrukturOrganisasi {
- 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])
- hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id])
-
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime?
- isActive Boolean @default(true)
-
- @@map("struktur_organisasi")
+model StrukturOrganisasiBumDes {
+ id String @id @default(uuid())
+ posisiOrganisasiId String @db.VarChar(50)
+ pegawaiId String
+ hubunganOrganisasiId String
+ posisiOrganisasi PosisiOrganisasiBumDes @relation(fields: [posisiOrganisasiId], references: [id])
+ pegawai PegawaiBumDes @relation(fields: [pegawaiId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime?
+ isActive Boolean @default(true)
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
@@ -1612,7 +1634,7 @@ model Pembiayaan {
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
-// ========================================= INOVASI ========================================= //
+// ========================================= MENU INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
@@ -1948,23 +1970,28 @@ model KeunggulanProgram {
}
model BeasiswaPendaftar {
- id String @id @default(cuid())
+ id String @id @default(cuid())
namaLengkap String
- nik String @unique
+ nis String?
+ kelas String?
+ jenisKelamin JenisKelamin
+ alamatDomisili String?
tempatLahir String
tanggalLahir DateTime
- jenisKelamin JenisKelamin
- kewarganegaraan String
- agama Agama
- alamatKTP String
- alamatDomisili String?
+ namaOrtu String?
+ nik String @unique
+ pekerjaanOrtu String?
+ penghasilan String?
noHp String
- email String @unique
- statusPernikahan StatusPernikahan
+ kewarganegaraan String?
+ agama Agama?
+ alamatKTP String?
+ email String? @unique
+ statusPernikahan StatusPernikahan?
ukuranBaju UkuranBaju?
- isActive Boolean @default(true)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
}
enum JenisKelamin {
@@ -2093,6 +2120,9 @@ model DataPerpustakaan {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
+
+ // relasi baru ke peminjaman
+ peminjamanBuku PeminjamanBuku[]
}
model KategoriBuku {
@@ -2105,28 +2135,56 @@ model KategoriBuku {
DataPerpustakaan DataPerpustakaan[]
}
+model PeminjamanBuku {
+ id String @id @default(cuid())
+ nama String
+ noTelp String
+ alamat String
+ bukuId String
+ tanggalPinjam DateTime @default(now())
+ batasKembali DateTime // tenggat waktu pengembalian
+ tanggalKembali DateTime? // diisi saat dikembalikan
+ status StatusPeminjaman @default(Dipinjam)
+ catatan String? // opsional, misal: kondisi buku
+ buku DataPerpustakaan @relation(fields: [bukuId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ deletedAt DateTime @default(now())
+ isActive Boolean @default(true)
+}
+
+enum StatusPeminjaman {
+ Dipinjam
+ Dikembalikan
+ Terlambat
+ Dibatalkan
+}
+
// ========================================= USER ========================================= //
model User {
- id String @id @default(cuid())
- username String
- nomor String @unique
- role Role @relation(fields: [roleId], references: [id])
- roleId String @default("1")
- instansi String?
- UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll)
- isActive Boolean @default(true)
- lastLogin DateTime?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- deletedAt DateTime?
+ id String @id @default(cuid())
+ username String
+ nomor String @unique
+ roleId String @default("2")
+ isActive Boolean @default(false)
+ sessionInvalid Boolean @default(false)
+ lastLogin DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
+ permissions Json?
+ sessions UserSession[] // ✅ Relasi one-to-many
+ role Role @relation(fields: [roleId], references: [id])
+ menuAccesses UserMenuAccess[]
+
+ @@map("users")
}
model Role {
id String @id @default(cuid())
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
description String?
- permissions Json // Menyimpan permission dalam format JSON
+ permissions Json?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -2145,26 +2203,32 @@ model KodeOtp {
otp Int
}
-// Tabel untuk menyimpan permission
-model Permission {
- id String @id @default(cuid())
- name String @unique
- description String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+model UserSession {
+ id String @id @default(cuid())
+ token String @db.Text // ✅ JWT bisa panjang
+ expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten)
+ active Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now()) @updatedAt
- @@map("permissions")
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ userId String // ✅ HAPUS @unique - user bisa punya multiple sessions
+
+ @@index([userId]) // ✅ Index untuk query cepat
+ @@index([token]) // ✅ Index untuk verify cepat
+ @@map("user_sessions")
}
-model UserSession {
- id String @id @default(cuid())
- token String
- expires DateTime?
- active Boolean @default(true)
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @updatedAt
- User User @relation(fields: [userId], references: [id])
- userId String @unique
+model UserMenuAccess {
+ id String @id @default(cuid())
+ userId String
+ menuId String // ID menu (misal: "Landing Page", "Kesehatan")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id])
+
+ @@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali
}
// ========================================= DATA PENDIDIKAN ========================================= //
diff --git a/prisma/seed.ts b/prisma/seed.ts
index ab793335..bb52224d 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import prisma from "@/lib/prisma";
import profilePejabatDesa from "./data/landing-page/profile/profile.json";
@@ -31,14 +32,14 @@ import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
-import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
-import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
-import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
-import kategoriBerita from "./data/kategori-berita.json";
+import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
+import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
+import kategoriBerita from "./data/desa/berita/kategori-berita.json";
import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
+import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json";
import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json";
@@ -54,89 +55,110 @@ import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-progr
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
import roles from "./data/user/roles.json";
-import users from "./data/user/users.json";
import fileStorage from "./data/file-storage.json";
+import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json";
+import seedAssets from "./seed_assets";
+import users from "./data/user/users.json";
+import { safeSeedUnique } from "./safeseedUnique";
(async () => {
- // =========== USER & ROLE ===========
- // In your seed.ts
- // =========== ROLES ===========
console.log("🔄 Seeding roles...");
- for (const r of roles) {
- await prisma.role.upsert({
- where: { id: r.id },
- update: {
- name: r.name,
- description: r.description,
- permissions: r.permissions,
- isActive: r.isActive,
- },
- create: {
- id: r.id,
- name: r.name,
- description: r.description,
- permissions: r.permissions,
- isActive: r.isActive,
- },
- });
- }
- console.log("✅ Roles seeded");
- // =========== USERS ===========
+ for (const r of roles) {
+ try {
+ // ✅ Destructure to remove permissions if exists
+ const { permissions, ...roleData } = r as any;
+
+ await safeSeedUnique(
+ "role",
+ { name: roleData.name },
+ {
+ id: roleData.id,
+ name: roleData.name,
+ description: roleData.description,
+ permissions: roleData.permissions || {}, // ✅ Include permissions
+ isActive: roleData.isActive,
+ }
+ );
+ console.log(`✅ Seeded role -> ${roleData.name}`);
+ } catch (error: any) {
+ if (error.code === "P2002") {
+ console.warn(`⚠️ Role already exists (skipping): ${r.name}`);
+ } else {
+ console.error(`❌ Failed to seed role ${r.name}:`, error.message);
+ }
+ }
+ }
+ console.log("✅ Roles seeding completed");
+
+ // =========== USER ===========
console.log("🔄 Seeding users...");
for (const u of users) {
- // First verify the role exists
- const roleExists = await prisma.role.findUnique({
- where: { id: u.roleId },
- });
+ try {
+ // Verify role exists first
+ const roleExists = await prisma.role.findUnique({
+ where: { id: u.roleId.toString() },
+ select: { id: true }, // Only select id to minimize query
+ });
- if (!roleExists) {
- console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`);
- continue;
+ if (!roleExists) {
+ console.error(
+ `❌ Role with id ${u.roleId} not found for user ${u.username}`
+ );
+ continue;
+ }
+
+ await safeSeedUnique(
+ "user",
+ { id: u.id },
+ {
+ username: u.username,
+ nomor: u.nomor,
+ roleId: u.roleId.toString(),
+ isActive: u.isActive,
+ sessionInvalid: false,
+ }
+ );
+ console.log(`✅ Seeded user -> ${u.username}`);
+ } catch (error: any) {
+ if (error.code === "P2003") {
+ console.error(
+ `❌ Foreign key constraint failed for user ${u.username}: Role ${u.roleId} does not exist`
+ );
+ } else {
+ console.error(`❌ Failed to seed user ${u.username}:`, error.message);
+ }
}
-
- await prisma.user.upsert({
- where: { id: u.id },
- update: {
- username: u.nama,
- nomor: u.nomor,
- roleId: u.roleId,
- isActive: u.isActive,
- },
- create: {
- id: u.id,
- username: u.nama,
- nomor: u.nomor,
- roleId: u.roleId,
- isActive: u.isActive,
- },
- });
}
- console.log("✅ Users seeded");
+ console.log("✅ Users seeding completed");
// =========== FILE STORAGE ===========
console.log("🔄 Seeding file storage...");
for (const f of fileStorage) {
- await prisma.fileStorage.upsert({
- where: { id: f.id },
- update: {
- name: f.name,
- realName: f.realName,
- path: f.path,
- mimeType: f.mimeType,
- link: f.link,
- category: f.category,
- },
- create: {
- id: f.id,
- name: f.name,
- realName: f.realName,
- path: f.path,
- mimeType: f.mimeType,
- link: f.link,
- category: f.category,
- },
- });
+ try {
+ await prisma.fileStorage.upsert({
+ where: { id: f.id },
+ update: {
+ name: f.name,
+ realName: f.realName,
+ path: f.path,
+ mimeType: f.mimeType,
+ link: f.link,
+ category: f.category,
+ },
+ create: {
+ id: f.id,
+ name: f.name,
+ realName: f.realName,
+ path: f.path,
+ mimeType: f.mimeType,
+ link: f.link,
+ category: f.category,
+ },
+ });
+ } catch (error: any) {
+ console.error(`❌ Failed to seed file storage ${f.name}:`, error.message);
+ }
}
console.log("✅ File storage seeded");
// =========== LANDING PAGE ===========
@@ -364,6 +386,7 @@ import fileStorage from "./data/file-storage.json";
jumlah: l.jumlah,
},
create: {
+ id: l.id,
name: l.name,
jumlah: l.jumlah,
},
@@ -554,15 +577,40 @@ import fileStorage from "./data/file-storage.json";
console.log("posisi organisasi berhasil");
// =========== PEGAWAI PPID ===========
+ console.log("🔄 Seeding pegawai PPID...");
const flattenedPegawai = pegawaiPPID.flat();
+
+ // Check for duplicate emails
+ const emails = new Set();
for (const p of flattenedPegawai) {
- await prisma.pegawaiPPID.upsert({
- where: { id: p.id },
- update: p,
- create: p,
- });
+ if (emails.has(p.email)) {
+ console.warn(`⚠️ Duplicate email found in pegawaiPPID: ${p.email}`);
+ }
+ emails.add(p.email);
}
- console.log("pegawai berhasil");
+
+ for (const p of flattenedPegawai) {
+ try {
+ await prisma.pegawaiPPID.upsert({
+ where: { id: p.id },
+ update: p,
+ create: p,
+ });
+ console.log(`✅ Seeded pegawai PPID -> ${p.namaLengkap}`);
+ } catch (error: any) {
+ if (error.code === "P2002") {
+ console.warn(
+ `⚠️ Pegawai PPID with duplicate email (skipping): ${p.email}`
+ );
+ } else {
+ console.error(
+ `❌ Failed to seed pegawai PPID ${p.namaLengkap}:`,
+ error.message
+ );
+ }
+ }
+ }
+ console.log("✅ pegawai PPID seeding completed");
// =========== SUBMENU VISI MISI PPID ===========
@@ -823,28 +871,36 @@ import fileStorage from "./data/file-storage.json";
}
console.log("kategori produk success ...");
- for (const p of posisiOrganisasi) {
- await prisma.posisiOrganisasi.upsert({
- where: {
- id: p.id,
- },
- update: {
- nama: p.nama,
- deskripsi: p.deskripsi,
- hierarki: p.hierarki,
- },
- create: {
- id: p.id,
- nama: p.nama,
- deskripsi: p.deskripsi,
- hierarki: p.hierarki,
- },
+ const flattenedPosisiBumdes = posisiOrganisasi.flat();
+
+ // ✅ Urutkan berdasarkan hierarki
+ const sortedPosisiBumdes = flattenedPosisiBumdes.sort(
+ (a, b) => a.hierarki - b.hierarki
+ );
+
+ for (const p of sortedPosisiBumdes) {
+ console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
+
+ if (p.parentId) {
+ const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
+ if (!parentExists) {
+ console.warn(
+ `⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`
+ );
+ continue;
+ }
+ }
+
+ await prisma.posisiOrganisasiBumDes.upsert({
+ where: { id: p.id },
+ update: p,
+ create: p,
});
}
- console.log("posisi organisasi success ...");
+ console.log("posisi organisasi berhasil");
for (const p of pegawai) {
- await prisma.pegawai.upsert({
+ await prisma.pegawaiBumDes.upsert({
where: {
id: p.id,
},
@@ -873,26 +929,6 @@ import fileStorage from "./data/file-storage.json";
}
console.log("pegawai success ...");
- for (const p of hubunganOrganisasi) {
- await prisma.hubunganOrganisasi.upsert({
- where: {
- atasanId_bawahanId: {
- atasanId: p.atasanId,
- bawahanId: p.bawahanId,
- },
- },
- update: {
- tipe: p.tipe,
- },
- create: {
- atasanId: p.atasanId,
- bawahanId: p.bawahanId,
- tipe: p.tipe,
- },
- });
- }
- console.log("hubungan organisasi success ...");
-
for (const d of detailDataPengangguran) {
await prisma.detailDataPengangguran.upsert({
where: {
@@ -916,6 +952,30 @@ import fileStorage from "./data/file-storage.json";
}
console.log("📊 detailDataPengangguran success ...");
+ // =========== KATEGORI GOTONG ROYONG ===========
+ // Add IDs to the kategoriKegiatan data
+ const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({
+ ...k,
+ id: `kategori-${index + 1}`,
+ }));
+
+ for (const k of kategoriKegiatan) {
+ await prisma.kategoriKegiatan.upsert({
+ where: {
+ id: k.id,
+ },
+ update: {
+ nama: k.nama,
+ },
+ create: {
+ id: k.id,
+ nama: k.nama,
+ },
+ });
+ }
+
+ console.log("kategori kegiatan success ...");
+
for (const e of tujuanEdukasiLingkungan) {
await prisma.tujuanEdukasiLingkungan.upsert({
where: {
@@ -1169,6 +1229,25 @@ import fileStorage from "./data/file-storage.json";
console.log(
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)"
);
+
+ for (const j of jenjangPendidikan) {
+ await prisma.jenjangPendidikan.upsert({
+ where: {
+ id: j.id || undefined,
+ },
+ update: {
+ nama: j.nama,
+ },
+ create: {
+ nama: j.nama,
+ },
+ });
+ }
+
+ console.log("✅ Jenjang Pendidikan seeded successfully");
+
+ // seed assets
+ await seedAssets();
})()
.then(() => prisma.$disconnect())
.catch((e) => {
diff --git a/prisma/seed_assets.ts b/prisma/seed_assets.ts
new file mode 100644
index 00000000..f92c0d36
--- /dev/null
+++ b/prisma/seed_assets.ts
@@ -0,0 +1,118 @@
+// prisma/seedAssets.ts
+import fs from "fs/promises";
+import path from "path";
+import sharp from "sharp";
+import fetch from "node-fetch";
+import AdmZip from "adm-zip";
+import prisma from "@/lib/prisma";
+
+const UPLOADS_DIR =
+ process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads");
+
+// --- Helper: deteksi kategori file ---
+function detectCategory(filename: string): "image" | "document" | "other" {
+ const ext = path.extname(filename).toLowerCase();
+ if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image";
+ if ([".pdf", ".doc", ".docx"].includes(ext)) return "document";
+ return "other";
+}
+
+// --- Helper: recursive walk dir ---
+async function walkDir(dir: string, fileList: string[] = []): Promise {
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ if (entry.name === "__MACOSX") continue; // skip folder sampah
+ await walkDir(fullPath, fileList);
+ } else {
+ if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah
+ fileList.push(fullPath);
+ }
+ }
+
+ return fileList;
+}
+
+export default async function seedAssets() {
+ console.log("🚀 Seeding assets...");
+
+ // 1. Download zip
+ const url =
+ "https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1";
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`);
+ const buffer = Buffer.from(await res.arrayBuffer());
+
+ // 2. Extract zip ke folder tmp
+ const extractDir = path.join(process.cwd(), "tmp_assets");
+ await fs.rm(extractDir, { recursive: true, force: true });
+ await fs.mkdir(extractDir, { recursive: true });
+
+ const zip = new AdmZip(buffer);
+ zip.extractAllTo(extractDir, true);
+
+ // 3. Cari semua file valid (recursive)
+ const files = await walkDir(extractDir);
+
+ // 4. Loop tiap file & simpan
+ for (const filePath of files) {
+ const entryName = path.basename(filePath);
+ const category = detectCategory(entryName);
+
+ let finalName = entryName;
+ let mimeType = "application/octet-stream";
+ let targetPath = "";
+
+ if (category === "image") {
+ const fileBaseName = path.parse(entryName).name;
+ finalName = `${fileBaseName}.webp`;
+ targetPath = path.join(UPLOADS_DIR, "images", finalName);
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await sharp(filePath).webp({ quality: 80 }).toFile(targetPath);
+ mimeType = "image/webp";
+ } else if (category === "document") {
+ targetPath = path.join(UPLOADS_DIR, "documents", entryName);
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.copyFile(filePath, targetPath);
+ mimeType = "application/pdf";
+ } else {
+ targetPath = path.join(UPLOADS_DIR, "other", entryName);
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
+ await fs.copyFile(filePath, targetPath);
+ }
+
+ // 5. Simpan ke DB
+ await prisma.fileStorage.create({
+ data: {
+ name: finalName,
+ realName: entryName,
+ path: targetPath,
+ mimeType,
+ link: `/uploads/${category}/${finalName}`,
+ category,
+ },
+ });
+
+ console.log(`📂 saved: ${category}/${finalName}`);
+ }
+
+ // 6. Cleanup
+ await fs.rm(extractDir, { recursive: true, force: true });
+
+ console.log("✅ Selesai seed assets!");
+}
+
+// --- Auto run kalau dipanggil langsung ---
+if (import.meta.main) {
+ seedAssets()
+ .catch((err) => {
+ console.error("❌ Error seeding assets:", err);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ });
+}
diff --git a/public/beasiswa-siswa.png b/public/beasiswa-siswa.png
new file mode 100644
index 00000000..ed40216d
Binary files /dev/null and b/public/beasiswa-siswa.png differ
diff --git a/public/perbekel.png b/public/perbekel.png
index 7da92111..a940365b 100644
Binary files a/public/perbekel.png and b/public/perbekel.png differ
diff --git a/src/app/admin/(dashboard)/_com/createEditor.tsx b/src/app/admin/(dashboard)/_com/createEditor.tsx
index 7878e59a..86cf608c 100644
--- a/src/app/admin/(dashboard)/_com/createEditor.tsx
+++ b/src/app/admin/(dashboard)/_com/createEditor.tsx
@@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
+import { useEffect } from 'react';
type CreateEditorProps = {
value: string;
@@ -32,6 +33,13 @@ export default function CreateEditor({ value, onChange }: CreateEditorProps) {
},
});
+ // 👇 Tambahkan efek untuk sinkronisasi value dari luar (resetForm)
+ useEffect(() => {
+ if (editor && value !== editor.getHTML()) {
+ editor.commands.setContent(value || '');
+ }
+ }, [value, editor]);
+
return (
diff --git a/src/app/admin/(dashboard)/_com/editEditor.tsx b/src/app/admin/(dashboard)/_com/editEditor.tsx
index 66317cc2..004a0dc0 100644
--- a/src/app/admin/(dashboard)/_com/editEditor.tsx
+++ b/src/app/admin/(dashboard)/_com/editEditor.tsx
@@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) {
editor.off('update', updateHandler);
};
}, [editor, onChange]);
+
return (
diff --git a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx
index 093a41df..c221672f 100644
--- a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx
+++ b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx
@@ -1,6 +1,6 @@
-'use client'
+'use client';
-import { Box, rem, Select } from '@mantine/core';
+import { Box, Group, rem, Select, SelectProps } from '@mantine/core';
import {
IconAmbulance,
IconCash,
@@ -25,7 +25,7 @@ import {
IconTrophy,
IconTruckFilled,
IconBuilding,
- IconAlertTriangle
+ IconAlertTriangle,
} from '@tabler/icons-react';
const iconMap = {
@@ -38,26 +38,26 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
- lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
- sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
- ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
- mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
- rumah: {label: 'Rumah', icon: IconHome},
- pohon: {label: 'Pohon', icon: IconTree},
- air: {label: 'Air', icon: IconDroplet},
- bantuan: {label: 'Bantuan', icon: IconCash},
- pelatihan: {label: 'Pelatihan', icon: IconSchool},
- subsidi: {label: 'Subsidi', icon: IconShoppingCart},
- layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
- polisi: {label: 'Polisi', icon: IconShieldFilled},
- ambulans: {label: 'Ambulans', icon: IconAmbulance},
- pemadam: {label: 'Pemadam', icon: IconFiretruck},
- rumahSakit: {label: 'Rumah Sakit', icon: IconHospital},
- bangunan: {label: 'Bangunan', icon: IconBuilding},
- darurat: {label: 'Darurat', icon: IconAlertTriangle},
+ lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
+ sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
+ ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
+ mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
+ rumah: { label: 'Rumah', icon: IconHome },
+ pohon: { label: 'Pohon', icon: IconTree },
+ air: { label: 'Air', icon: IconDroplet },
+ bantuan: { label: 'Bantuan', icon: IconCash },
+ pelatihan: { label: 'Pelatihan', icon: IconSchool },
+ subsidi: { label: 'Subsidi', icon: IconShoppingCart },
+ layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
+ polisi: { label: 'Polisi', icon: IconShieldFilled },
+ ambulans: { label: 'Ambulans', icon: IconAmbulance },
+ pemadam: { label: 'Pemadam', icon: IconFiretruck },
+ rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
+ bangunan: { label: 'Bangunan', icon: IconBuilding },
+ darurat: { label: 'Darurat', icon: IconAlertTriangle },
};
-type IconKey = keyof typeof iconMap;
+export type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
@@ -67,44 +67,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({
export default function SelectIconProgramEdit({
onChange,
value,
+ ...props
}: {
- onChange: (value: IconKey) => void;
- value: IconKey;
-}) {
- const IconComponent = iconMap[value]?.icon || null;
-
+ onChange: (value: IconKey | '') => void;
+ value: IconKey | '';
+} & Omit) {
return (
{
- if (value) onChange(value as IconKey);
+ value={value || ''}
+ onChange={(val: string | null) => {
+ if (val) {
+ onChange(val as IconKey);
+ } else {
+ onChange('');
+ }
}}
data={iconList}
+ renderOption={({ option }) => {
+ const Icon = iconMap[option.value as IconKey]?.icon;
+ return (
+
+ {Icon && }
+ {option.label}
+
+ );
+ }}
leftSection={
- IconComponent && (
-
-
+ value && iconMap[value as IconKey] ? (
+
+ {(() => {
+ const Icon = iconMap[value as IconKey].icon;
+ return ;
+ })()}
- )
+ ) : null
}
- withCheckIcon={false}
- searchable={false}
- rightSectionWidth={0}
+ searchable
styles={{
input: {
- textAlign: 'left',
- fontSize: rem(16),
paddingLeft: 40,
- },
- section: {
- left: 10,
- right: 'auto',
+ fontSize: rem(16),
},
}}
+ {...props}
/>
);
-}
-
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/desa/berita.ts b/src/app/admin/(dashboard)/_state/desa/berita.ts
index 2105e58f..0a7dc17e 100644
--- a/src/app/admin/(dashboard)/_state/desa/berita.ts
+++ b/src/app/admin/(dashboard)/_state/desa/berita.ts
@@ -75,17 +75,18 @@ const berita = proxy({
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", kategori = "") => {
- berita.findMany.loading = true; // ✅ Akses langsung via nama path
+ const startTime = Date.now();
+ berita.findMany.loading = true;
berita.findMany.page = page;
berita.findMany.search = search;
-
+
try {
const query: any = { page, limit };
if (search) query.search = search;
if (kategori) query.kategori = kategori;
-
+
const res = await ApiFetch.api.desa.berita["find-many"].get({ query });
-
+
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
@@ -98,9 +99,16 @@ const berita = proxy({
berita.findMany.data = [];
berita.findMany.totalPages = 1;
} finally {
- berita.findMany.loading = false;
+ // pastikan minimal 300ms sebelum loading = false (biar UX smooth)
+ const elapsed = Date.now() - startTime;
+ const minDelay = 300;
+ const delay = elapsed < minDelay ? minDelay - elapsed : 0;
+
+ setTimeout(() => {
+ berita.findMany.loading = false;
+ }, delay);
}
- },
+ },
},
findUnique: {
diff --git a/src/app/admin/(dashboard)/_state/desa/layananDesa.ts b/src/app/admin/(dashboard)/_state/desa/layananDesa.ts
index b11b67a8..a0a39410 100644
--- a/src/app/admin/(dashboard)/_state/desa/layananDesa.ts
+++ b/src/app/admin/(dashboard)/_state/desa/layananDesa.ts
@@ -581,33 +581,24 @@ const pelayananPerizinanBerusaha = proxy({
findById: {
data: null as pelayananPerizinanBerusahaForm | null,
loading: false,
- initialize() {
- pelayananPerizinanBerusaha.findById.data = {
- id: "",
- name: "",
- deskripsi: "",
- link: "",
- } as pelayananPerizinanBerusahaForm;
- },
async load(id: string) {
try {
- pelayananPerizinanBerusaha.findById.loading = true;
- const res = await fetch(
- `/api/desa/layanan/pelayananperizinanberusaha/${id}`
- );
- if (res.ok) {
- const data = await res.json();
- pelayananPerizinanBerusaha.findById.data = data.data ?? null;
- } else {
- console.error(
- "Failed to fetch pelayanan perizinan berusaha:",
- res.statusText
- );
- pelayananPerizinanBerusaha.findById.data = null;
+ this.loading = true;
+ const response = await fetch(`/api/desa/layanan/pelayananperizinanberusaha/${id}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
}
+ const result = await response.json();
+ if (result?.success) {
+ this.data = result.data; // Make sure this matches your API response structure
+ }
+ return result?.data || null;
} catch (error) {
- console.error("Error fetching pelayanan perizinan berusaha:", error);
- pelayananPerizinanBerusaha.findById.data = null;
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data');
+ return null;
+ } finally {
+ this.loading = false;
}
},
},
diff --git a/src/app/admin/(dashboard)/_state/desa/penghargaan.ts b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts
index 68be0ba7..20921427 100644
--- a/src/app/admin/(dashboard)/_state/desa/penghargaan.ts
+++ b/src/app/admin/(dashboard)/_state/desa/penghargaan.ts
@@ -39,7 +39,7 @@ const penghargaanState = proxy({
);
if (res.status === 200) {
penghargaanState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts
index 09320003..dd289918 100644
--- a/src/app/admin/(dashboard)/_state/desa/pengumuman.ts
+++ b/src/app/admin/(dashboard)/_state/desa/pengumuman.ts
@@ -287,7 +287,7 @@ const pengumuman = proxy({
);
if (res.status === 200) {
pengumuman.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts
index fd3dd897..6b09c7e5 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts
@@ -101,6 +101,38 @@ const ApbDesa = proxy({
}
},
},
+ findFirst: {
+ data: null as Prisma.ApbDesaGetPayload<{
+ include: { pendapatan: true; belanja: true; pembiayaan: true };
+ }> | null,
+ loading: false,
+ async load(params?: Record) {
+ try {
+ this.loading = true;
+
+ // ✅ request ke endpoint find-first
+ const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
+ "find-first"
+ ].get({ query: params || {} });
+
+ if (res.status === 200 && res.data?.success) {
+ this.data = res.data.data ?? null;
+ } else {
+ this.data = null;
+ toast.error(res.data?.message || "Gagal memuat data pertama APB Desa");
+ }
+ } catch (error) {
+ console.error("Error findFirst APB Desa:", error);
+ toast.error("Gagal memuat data APB Desa pertama");
+ this.data = null;
+ } finally {
+ this.loading = false;
+ }
+ },
+ reset() {
+ this.data = null;
+ },
+ },
update: {
id: "",
form: { ...ApbDesaDefaultForm },
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts b/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts
index bf29247b..3f4da69b 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan.ts
@@ -49,7 +49,7 @@ const demografiPekerjaan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
demografiPekerjaan.create.form = { ...defaultForm };
demografiPekerjaan.findMany.load();
return id;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts
index 2eb11a03..5acad685 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin.ts
@@ -47,7 +47,7 @@ const jumlahPendudukMiskin = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts
index c45ac24a..4fe9939a 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran.ts
@@ -89,7 +89,7 @@ const jumlahPengangguran = proxy({
if (res.status === 200) {
const id = res.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
jumlahPengangguran.findMany.load();
return id;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts
index 0481bbca..1cf5941b 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts
@@ -13,6 +13,7 @@ const templateForm = z.object({
gaji: z.string(),
deskripsi: z.string(),
kualifikasi: z.string(),
+ notelp: z.string(),
});
const defaultForm = {
@@ -23,6 +24,7 @@ const defaultForm = {
gaji: "",
deskripsi: "",
kualifikasi: "",
+ notelp: "",
};
const lowonganKerjaState = proxy({
@@ -45,7 +47,7 @@ const lowonganKerjaState = proxy({
);
if (res.status === 200) {
lowonganKerjaState.create.loading = false;
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
@@ -179,6 +181,7 @@ const lowonganKerjaState = proxy({
gaji: data.gaji,
deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi,
+ notelp: data.notelp,
};
return data;
} else {
@@ -218,6 +221,7 @@ const lowonganKerjaState = proxy({
gaji: this.form.gaji,
deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi,
+ notelp: this.form.notelp,
}),
});
if (!response.ok) {
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts b/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts
index e5cfb7e0..21d8e7b4 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa.ts
@@ -12,6 +12,7 @@ const templatePasarDesaForm = z.object({
imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
+ kontak: z.string().min(1, "Kontak wajib diisi"),
});
const defaultPasarDesaForm = {
@@ -21,6 +22,7 @@ const defaultPasarDesaForm = {
imageId: "",
rating: 0,
kategoriId: [] as string[],
+ kontak: "",
};
const pasarDesa = proxy({
@@ -188,6 +190,7 @@ const pasarDesa = proxy({
imageId: data.imageId,
rating: data.rating,
kategoriId: data.kategoriId,
+ kontak: data.kontak,
};
return data;
} else {
@@ -225,6 +228,7 @@ const pasarDesa = proxy({
imageId: this.form.imageId,
rating: this.form.rating,
kategoriId: this.form.kategoriId,
+ kontak: this.form.kontak,
}),
});
if (!response.ok) {
@@ -336,6 +340,40 @@ const kategoriProduk = proxy({
}
},
},
+ // ✅ Versi findManyAll (ambil semua tanpa pagination)
+ findManyAll: {
+ data: null as
+ | Prisma.KategoriProdukGetPayload<{
+ omit: { isActive: true };
+ }>[]
+ | null,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ kategoriProduk.findManyAll.loading = true;
+ kategoriProduk.findManyAll.search = search;
+
+ try {
+ const query: any = {};
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many-all"].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ kategoriProduk.findManyAll.data = res.data.data ?? [];
+ } else {
+ kategoriProduk.findManyAll.data = [];
+ }
+ } catch (err) {
+ console.error("Gagal fetch kategori produk (all):", err);
+ kategoriProduk.findManyAll.data = [];
+ } finally {
+ kategoriProduk.findManyAll.loading = false;
+ }
+ },
+ },
findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts b/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts
index 1332bbea..8de0655e 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan.ts
@@ -45,7 +45,7 @@ const programKemiskinanState = proxy({
);
if (res.status === 200) {
programKemiskinanState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts b/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts
index 341398f0..0c78ec15 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa.ts
@@ -46,7 +46,7 @@ const grafikSektorUnggulan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
grafikSektorUnggulan.create.form = {
name: "",
description: "",
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts b/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts
index 9fc870d2..cef5c3fa 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts
@@ -1,9 +1,173 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { proxy } from "valtio";
-import { z } from "zod";
-import { toast } from "react-toastify";
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"),
+});
+
+const defaultForm = {
+ name: "",
+ imageId: "",
+};
+
+type StrukturBumDesForm = Prisma.StrukturBumDesGetPayload<{
+ select: {
+ id: true;
+ name: true;
+ imageId: true;
+ image?: {
+ select: {
+ link: true;
+ };
+ };
+ };
+}>;
+
+const stateStruktur = proxy({
+ struktur: {
+ data: null as StrukturBumDesForm | null,
+ loading: false,
+ error: null as string | null,
+
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const response = await fetch(`/api/ekonomi/struktur-organisasi/${id}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ 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;
+ },
+ },
+
+ editStruktur: {
+ id: "",
+ form: { ...defaultForm },
+ loading: false,
+ error: null as string | null,
+ isReadOnly: false,
+
+ initialize(strukturData: StrukturBumDesForm) {
+ 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/ekonomi/struktur-organisasi/${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"),
@@ -30,9 +194,7 @@ const posisiOrganisasi = proxy({
try {
this.loading = true;
- const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
- "posisi-organisasi"
- ]["create"].post(this.form);
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['create'].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();
@@ -52,6 +214,29 @@ const posisiOrganisasi = proxy({
},
},
+ findUnique: {
+ data: null as Prisma.StrukturOrganisasiBumDesGetPayload<{
+ omit: { isActive: true };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/ekonomi/struktur-organisasi/posisi-organisasi/${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 },
@@ -165,17 +350,17 @@ const posisiOrganisasi = proxy({
totalPages: 1,
loading: false,
search: "",
- load: async (page = 1, limit = 10, search = "") => {
- posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
+ load: async (page = 1, limit?: number, search = "") => {
+ const appliedLimit = limit ?? 10;
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
-
+
try {
- const query: any = { page, limit };
+ const query: any = { page, limit: appliedLimit };
if (search) query.search = search;
-
- const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
-
+
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many'].get({ query });
+
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
@@ -192,7 +377,42 @@ const posisiOrganisasi = proxy({
}
},
},
+ findManyAll: {
+ data: [] as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string | null;
+ hierarki: number;
+ }>,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ // Change to arrow function
+ posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
+ posisiOrganisasi.findManyAll.search = search;
+ try {
+ const query: any = { search };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi']['posisi-organisasi']['find-many-all'].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ posisiOrganisasi.findManyAll.data = res.data.data || [];
+
+ } else {
+ console.error("Failed to load posisiOrganisasi:", res.data?.message);
+ posisiOrganisasi.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading posisiOrganisasi:", error);
+ posisiOrganisasi.findManyAll.data = [];
+ } finally {
+ posisiOrganisasi.findManyAll.loading = false;
+ }
+ },
+ },
delete: {
loading: false,
async byId(id: string) {
@@ -231,12 +451,12 @@ const posisiOrganisasi = proxy({
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
- gelarAkademik: z.string().optional(),
- imageId: z.string().nullable().optional(),
- tanggalMasuk: z.string().optional(), // ISO format
+ 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().optional(),
- alamat: z.string().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),
});
@@ -267,9 +487,9 @@ const pegawai = proxy({
try {
pegawai.create.loading = true;
- const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
- "pegawai"
- ]["create"].post(pegawai.create.form);
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['create'].post(
+ pegawai.create.form
+ );
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
@@ -286,45 +506,56 @@ const pegawai = proxy({
},
// In struktur-organisasi.ts
-findMany: {
- data: null as any[] | null,
- page: 1,
- totalPages: 1,
- total: 0,
- loading: false,
- load: async (page = 1, limit = 10) => { // Change to arrow function
- pegawai.findMany.loading = true; // Use the full path to access the property
- pegawai.findMany.page = page;
- try {
- const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
- "pegawai"
- ]["find-many"].get({
- query: { page, limit },
- });
+ findMany: {
+ data: null as
+ | Prisma.PegawaiBumDesGetPayload<{
+ include: {
+ image: true;
+ posisi: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ total: 0,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ // Change to arrow function
+ pegawai.findMany.loading = true; // Use the full path to access the property
+ pegawai.findMany.page = page;
+ pegawai.findMany.search = search;
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
- if (res.status === 200 && res.data?.success) {
- pegawai.findMany.data = res.data.data || [];
- pegawai.findMany.total = res.data.total || 0;
- pegawai.findMany.totalPages = res.data.totalPages || 1;
- } else {
- console.error("Failed to load pegawai:", res.data?.message);
+ const res = await ApiFetch.api.ekonomi['struktur-organisasi'].pegawai['find-many'].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pegawai.findMany.data = res.data.data || [];
+ pegawai.findMany.total = res.data.total || 0;
+ pegawai.findMany.totalPages = res.data.totalPages || 1;
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ pegawai.findMany.data = [];
+ pegawai.findMany.total = 0;
+ pegawai.findMany.totalPages = 1;
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
+ } finally {
+ pegawai.findMany.loading = false;
}
- } catch (error) {
- console.error("Error loading pegawai:", error);
- pegawai.findMany.data = [];
- pegawai.findMany.total = 0;
- pegawai.findMany.totalPages = 1;
- } finally {
- pegawai.findMany.loading = false;
- }
+ },
},
-},
findUnique: {
data: null as
- | (Prisma.PegawaiGetPayload<{
+ | (Prisma.PegawaiBumDesGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
@@ -350,12 +581,9 @@ findMany: {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
- const res = await fetch(
- `/api/ekonomi/struktur-organisasi/pegawai/del/${id}`,
- {
- method: "DELETE",
- }
- );
+ const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
+ method: "DELETE",
+ });
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
@@ -372,6 +600,31 @@ findMany: {
},
},
+ nonActive: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pegawai.nonActive.loading = true;
+ const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/non-active/${id}`, {
+ method: "DELETE", // biasanya nonActive pakai PATCH
+ });
+ const json = await res.json();
+ if (res.ok) {
+ toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
+ await pegawai.findMany.load(); // refresh data
+ } else {
+ toast.error(json.message ?? "Gagal menonaktifkan pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal nonActive:", error);
+ toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
+ } finally {
+ pegawai.nonActive.loading = false;
+ }
+ },
+ },
+
edit: {
id: "",
form: { ...pegawaiDefaultForm },
@@ -384,15 +637,12 @@ findMany: {
}
try {
- const response = await fetch(
- `/api/ekonomi/struktur-organisasi/pegawai/${id}`,
- {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
+ const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -503,299 +753,10 @@ findMany: {
},
});
-// Schema Zod untuk form validasi
-const templateHubunganOrganisasiForm = z.object({
- atasanId: z.string().min(1, "Atasan wajib dipilih"),
- bawahanId: z.string().min(1, "Bawahan wajib dipilih"),
- tipe: z.string().optional(),
-});
-
-// Default form state
-const defaultHubunganOrganisasiForm = {
- atasanId: "",
- bawahanId: "",
- tipe: "",
-};
-
-// ====================== STATE ===========================
-const hubunganOrganisasi = proxy({
- create: {
- form: { ...defaultHubunganOrganisasiForm },
- loading: false,
- async create() {
- const cek = templateHubunganOrganisasiForm.safeParse(
- hubunganOrganisasi.create.form
- );
- if (!cek.success) {
- const err = `[${cek.error.issues
- .map((v) => `${v.path.join(".")}: ${v.message}`)
- .join("\n")}]`;
- return toast.error(err);
- }
-
- try {
- hubunganOrganisasi.create.loading = true;
- const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
- "hubungan-organisasi"
- ]["create"].post(hubunganOrganisasi.create.form);
-
- if (res.status === 200 && res.data?.success) {
- hubunganOrganisasi.findMany.load();
- return toast.success("Berhasil menambahkan hubungan organisasi");
- } else {
- return toast.error(res.data?.message || "Gagal menambahkan data");
- }
- } catch (error) {
- console.error("Gagal create:", error);
- toast.error("Terjadi kesalahan saat menambahkan");
- } finally {
- hubunganOrganisasi.create.loading = false;
- }
- },
- },
- findMany: {
- data: null as Array<{
- id: string;
- atasanId: string;
- bawahanId: string;
- tipe?: string | null;
- atasan: {
- id: string;
- namaLengkap: string;
- gelarAkademik: string | null;
- imageId: string | null;
- tanggalMasuk: Date | null;
- email: string | null;
- telepon: string | null;
- alamat: string | null;
- posisiId: string;
- isActive: boolean;
- createdAt: Date;
- updatedAt: Date;
- };
- bawahan: {
- id: string;
- namaLengkap: string;
- gelarAkademik: string | null;
- imageId: string | null;
- tanggalMasuk: Date | null;
- email: string | null;
- telepon: string | null;
- alamat: string | null;
- posisiId: string;
- isActive: boolean;
- createdAt: Date;
- updatedAt: Date;
- };
- }> | null,
-
- async load() {
- try {
- const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
- "hubungan-organisasi"
- ]["find-many"].get();
-
- if (res.status === 200) {
- hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map(
- (item: any) => ({
- ...item,
- atasan: item.atasan
- ? {
- ...item.atasan,
- isActive: item.atasan.isActive ?? item.atasan.aktif ?? true,
- }
- : null,
- bawahan: item.bawahan
- ? {
- ...item.bawahan,
- isActive:
- item.bawahan.isActive ?? item.bawahan.aktif ?? true,
- }
- : null,
- })
- );
- } else {
- hubunganOrganisasi.findMany.data = [];
- }
- } catch (error) {
- console.error("Fetch list error:", error);
- toast.error("Gagal memuat data hubungan organisasi");
- hubunganOrganisasi.findMany.data = [];
- }
- },
- },
-
- findUnique: {
- data: null as {
- id: string;
- atasanId: string;
- bawahanId: string;
- tipe?: string | null;
- atasan?: {
- id: string;
- namaLengkap: string;
- gelarAkademik: string | null;
- imageId: string;
- tanggalMasuk: Date | null;
- email: string | null;
- telepon: string | null;
- alamat: string | null;
- posisiId: string;
- aktif: boolean;
- createdAt: Date;
- updatedAt: Date;
- };
- bawahan?: {
- id: string;
- namaLengkap: string;
- gelarAkademik: string | null;
- imageId: string;
- tanggalMasuk: Date | null;
- email: string | null;
- telepon: string | null;
- alamat: string | null;
- posisiId: string;
- aktif: boolean;
- createdAt: Date;
- updatedAt: Date;
- };
- } | null,
-
- async load(id: string) {
- try {
- const res = await fetch(
- `/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
- );
- const result = await res.json();
-
- if (res.ok && result?.success) {
- hubunganOrganisasi.findUnique.data = result.data;
- } else {
- hubunganOrganisasi.findUnique.data = null;
- toast.error(result?.message || "Gagal mengambil data");
- }
- } catch (error) {
- console.error("Find unique error:", error);
- hubunganOrganisasi.findUnique.data = null;
- }
- },
- },
-
- edit: {
- id: "",
- form: { ...defaultHubunganOrganisasiForm },
- loading: false,
-
- async load(id: string) {
- if (!id) return toast.warn("ID tidak valid");
-
- try {
- const res = await fetch(
- `/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
- );
- const result = await res.json();
-
- if (res.ok && result?.success) {
- const data = result.data;
- this.id = data.id;
- this.form = {
- atasanId: data.atasanId,
- bawahanId: data.bawahanId,
- tipe: data.tipe || "",
- };
- return data;
- } else {
- throw new Error(result?.message || "Gagal memuat data");
- }
- } catch (error) {
- console.error("Error loading:", error);
- toast.error(
- error instanceof Error ? error.message : "Gagal memuat data"
- );
- return null;
- }
- },
-
- async update() {
- const cek = templateHubunganOrganisasiForm.safeParse(this.form);
- if (!cek.success) {
- const err = `[${cek.error.issues
- .map((v) => `${v.path.join(".")}: ${v.message}`)
- .join("\n")}]`;
- return toast.error(err);
- }
-
- try {
- this.loading = true;
- const res = await fetch(
- `/api/ekonomi/struktur-organisasi/hubungan-organisasi/${this.id}`,
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(this.form),
- }
- );
-
- const result = await res.json();
- if (res.ok && result.success) {
- await hubunganOrganisasi.findMany.load();
- toast.success("Berhasil mengupdate hubungan organisasi");
- return true;
- } else {
- throw new Error(result?.message || "Gagal mengupdate");
- }
- } catch (error) {
- console.error("Update error:", error);
- toast.error(error instanceof Error ? error.message : "Gagal update");
- return false;
- } finally {
- this.loading = false;
- }
- },
-
- reset() {
- hubunganOrganisasi.edit.id = "";
- hubunganOrganisasi.edit.form = { ...defaultHubunganOrganisasiForm };
- },
- },
-
- delete: {
- loading: false,
- async byId(id: string) {
- if (!id) return toast.warn("ID tidak valid");
-
- try {
- hubunganOrganisasi.delete.loading = true;
- const res = await fetch(
- `/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`,
- {
- method: "DELETE",
- }
- );
-
- const result = await res.json();
- if (res.ok && result?.success) {
- toast.success("Hubungan organisasi berhasil dihapus");
- hubunganOrganisasi.findMany.load();
- } else {
- toast.error(result?.message || "Gagal menghapus hubungan organisasi");
- }
- } catch (error) {
- console.error("Delete error:", error);
- toast.error("Terjadi kesalahan saat menghapus");
- } finally {
- hubunganOrganisasi.delete.loading = false;
- }
- },
- },
-});
-
-const strukturorganisasiState = proxy({
+const stateStrukturBumDes = proxy({
+ stateStruktur,
posisiOrganisasi,
pegawai,
- hubunganOrganisasi,
});
-export default strukturorganisasiState;
+export default stateStrukturBumDes;
diff --git a/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts b/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts
index 52cd2f92..7625d59f 100644
--- a/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts
+++ b/src/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur.ts
@@ -51,7 +51,7 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
grafikBerdasarkanUsiaKerjaNganggur.create.form = {
usia18_25: "",
usia26_35: "",
@@ -255,7 +255,7 @@ const grafikBerdasarkanPendidikan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
grafikBerdasarkanPendidikan.create.form = {
SD: "",
SMP: "",
diff --git a/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts b/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts
index 83d4d5cd..7afacdc2 100644
--- a/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts
+++ b/src/app/admin/(dashboard)/_state/inovasi/desa-digital.ts
@@ -37,7 +37,7 @@ const desaDigitalState = proxy({
);
if (res.status === 200) {
desaDigitalState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts
index 710bbcd8..7facc93b 100644
--- a/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts
+++ b/src/app/admin/(dashboard)/_state/inovasi/info-tekno.ts
@@ -37,7 +37,7 @@ const infoTeknoState = proxy({
);
if (res.status === 200) {
infoTeknoState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts b/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts
index 345910cc..125d6b71 100644
--- a/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts
+++ b/src/app/admin/(dashboard)/_state/inovasi/program-kreatif.ts
@@ -6,9 +6,9 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
- name: z.string().min(1, "Nama minimal 1 karakter"),
- deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
- slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
+ name: z.string().min(5, "Nama minimal 5 karakter"),
+ deskripsi: z.string().min(5, "Deskripsi minimal 5 karakter"),
+ slug: z.string().min(5, "Deskripsi singkat minimal 5 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
@@ -29,26 +29,33 @@ const programKreatifState = proxy({
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
- return toast.error(err);
+ toast.error(err);
+ return false; // ⬅️ ini penting
}
-
+
try {
programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form
);
+
if (res.status === 200) {
programKreatifState.findMany.load();
- return toast.success("success create");
+ toast.success("Sukses menambahkan");
+ return true;
}
- console.log(res);
- return toast.error("failed create");
+
+ toast.error("failed create");
+ return false;
} catch (error) {
- console.log((error as Error).message);
+ console.error((error as Error).message);
+ toast.error("Terjadi kesalahan saat create");
+ return false;
} finally {
programKreatifState.create.loading = false;
}
- },
+ }
+
},
findMany: {
data: null as any[] | null,
diff --git a/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts b/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts
index 3fd458f4..1a19352e 100644
--- a/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts
+++ b/src/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan.ts
@@ -37,7 +37,7 @@ const keamananLingkunganState = proxy({
].post(keamananLingkunganState.create.form);
if (res.status === 200) {
keamananLingkunganState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts b/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts
index af0d2401..c3b70625 100644
--- a/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts
+++ b/src/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan.ts
@@ -38,7 +38,7 @@ const kontakDaruratKeamananState = proxy({
].post(kontakDaruratKeamananState.create.form);
if (res.status === 200) {
kontakDaruratKeamananState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
@@ -294,7 +294,7 @@ const kontakDaruratItem = proxy({
);
if (res.status === 200) {
kontakDaruratItem.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts b/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts
index 6db65c2b..5e61039b 100644
--- a/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts
+++ b/src/app/admin/(dashboard)/_state/keamanan/laporan-publik.ts
@@ -88,7 +88,7 @@ const laporanPublikState = proxy({
if (res.status === 200) {
laporanPublikState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
diff --git a/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts b/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts
index 8530d809..644bf0dd 100644
--- a/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts
+++ b/src/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas.ts
@@ -40,7 +40,7 @@ const pencegahanKriminalitasState = proxy({
].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts b/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts
index ad1ee158..4bd4609c 100644
--- a/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts
+++ b/src/app/admin/(dashboard)/_state/keamanan/tips-keamanan.ts
@@ -37,7 +37,7 @@ const tipsKeamananState = proxy({
);
if (res.status === 200) {
tipsKeamananState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts
index 71c04389..016f6b28 100644
--- a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan.ts
@@ -351,7 +351,7 @@ const dokter = proxy({
if (res.status === 200) {
const id = res.data?.data;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
dokter.create.create.form = { ...defaultDokterForm };
dokter.findMany.load();
return id;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts
index 19f7e80a..15b7b4b8 100644
--- a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan.ts
@@ -43,7 +43,7 @@ const grafikkepuasan = proxy({
if (res.status === 200) {
const id = res.data?.data;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
grafikkepuasan.create.form = { ...defaultForm };
grafikkepuasan.findMany.load();
return id;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts
index 4da79df8..e00ab588 100644
--- a/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts
+++ b/src/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran.ts
@@ -50,7 +50,7 @@ const persentasekelahiran = proxy({
if (res.status === 200) {
const id = res.data?.data;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
persentasekelahiran.create.form = { ...defaultForm };
persentasekelahiran.findMany.load();
return id;
diff --git a/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts b/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts
index 8f96d2e2..85480912 100644
--- a/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts
+++ b/src/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat.ts
@@ -9,12 +9,14 @@ const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
+ whatsapp: z.string().min(10, "Whatsapp minimal 10 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
+ whatsapp: "",
};
const kontakDarurat = proxy({
@@ -171,6 +173,7 @@ const kontakDarurat = proxy({
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
+ whatsapp: data.whatsapp,
};
return data; // Return the loaded data
} else {
@@ -207,6 +210,7 @@ const kontakDarurat = proxy({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
+ whatsapp: this.form.whatsapp,
}),
}
);
diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
index c780b1a9..df9cde1d 100644
--- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
+++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
@@ -5,58 +5,117 @@ import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
-const templateapbDesaForm = z.object({
- name: z.string().min(1, "Judul minimal 1 karakter"),
- jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
- imageId: z.string().min(1, "File minimal 1"),
- fileId: z.string().min(1, "File minimal 1"),
+// --- Zod Schema ---
+const ApbdesItemSchema = z.object({
+ kode: z.string().min(1, "Kode wajib diisi"),
+ uraian: z.string().min(1, "Uraian wajib diisi"),
+ anggaran: z.number().min(0),
+ realisasi: z.number().min(0),
+ selisih: z.number(),
+ persentase: z.number(),
+ level: z.number().int().min(1).max(3),
+ tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
});
-const defaultapbdesForm = {
- name: "",
- jumlah: "",
+const ApbdesFormSchema = z.object({
+ tahun: z.number().int().min(2000, "Tahun tidak valid"),
+ imageId: z.string().min(1, "Gambar wajib diunggah"),
+ fileId: z.string().min(1, "File wajib diunggah"),
+ items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"),
+});
+
+// --- Default Form ---
+const defaultApbdesForm = {
+ tahun: new Date().getFullYear(),
imageId: "",
fileId: "",
+ items: [] as z.infer[],
};
+// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
+// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
+function normalizeItem(item: Partial>): z.infer {
+ const anggaran = item.anggaran ?? 0;
+ const realisasi = item.realisasi ?? 0;
+
+
+
+
+ // ✅ Formula yang benar
+ const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget
+ const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
+
+ return {
+ kode: item.kode || "",
+ uraian: item.uraian || "",
+ anggaran,
+ realisasi,
+ selisih,
+ persentase,
+ level: item.level || 1,
+ tipe: item.tipe, // biarkan null jika memang null
+ };
+}
+
+// --- State Utama ---
const apbdes = proxy({
create: {
- form: { ...defaultapbdesForm },
+ form: { ...defaultApbdesForm },
loading: false,
- async create() {
- const cek = templateapbDesaForm.safeParse(apbdes.create.form);
- if (!cek.success) {
- const err = `[${cek.error.issues
- .map((v) => `${v.path.join(".")}`)
- .join("\n")}] required`;
- return toast.error(err);
- }
- try {
- apbdes.create.loading = true;
- const res = await ApiFetch.api.landingpage.apbdes["create"].post({
- ...apbdes.create.form,
- });
- if (res.status === 200) {
+ addItem(item: Partial>) {
+ const normalized = normalizeItem(item);
+ this.form.items.push(normalized);
+ },
+
+ removeItem(index: number) {
+ this.form.items.splice(index, 1);
+ },
+
+ updateItem(index: number, updates: Partial>) {
+ const current = this.form.items[index];
+ if (current) {
+ const updated = normalizeItem({ ...current, ...updates });
+ this.form.items[index] = updated;
+ }
+ },
+
+ reset() {
+ this.form = { ...defaultApbdesForm };
+ },
+
+ async create() {
+ const parsed = ApbdesFormSchema.safeParse(this.form);
+ if (!parsed.success) {
+ const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
+ toast.error(`Validasi gagal:\n${errors.join("\n")}`);
+ return;
+ }
+
+ try {
+ this.loading = true;
+ const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
+
+ if (res.data?.success) {
+ toast.success("APBDes berhasil dibuat");
apbdes.findMany.load();
- return toast.success("Data berhasil ditambahkan");
+ this.reset();
+ } else {
+ toast.error(res.data?.message || "Gagal membuat APBDes");
}
- return toast.error("Gagal menambahkan data");
- } catch (error) {
- console.log(error);
- toast.error("Gagal menambahkan data");
+ } catch (error: any) {
+ console.error("Create APBDes error:", error);
+ toast.error(error?.message || "Terjadi kesalahan saat membuat APBDes");
} finally {
- apbdes.create.loading = false;
+ this.loading = false;
}
},
},
+
findMany: {
data: null as
| Prisma.APBDesGetPayload<{
- include: {
- image: true;
- file: true;
- };
+ include: { image: true; file: true; items: true };
}>[]
| null,
page: 1,
@@ -64,194 +123,202 @@ const apbdes = proxy({
total: 0,
loading: false,
search: "",
- load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
- apbdes.findMany.loading = true; // Use the full path to access the property
+
+ load: async (page = 1, limit = 10, search = "") => {
+ apbdes.findMany.loading = true;
apbdes.findMany.page = page;
apbdes.findMany.search = search;
+
try {
- const query: any = { page, limit };
+ const query: Record = { page: String(page), limit: String(limit) };
if (search) query.search = search;
-
- const res = await ApiFetch.api.landingpage.apbdes[
- "findMany"
- ].get({
- query
- });
-
- if (res.status === 200 && res.data?.success) {
+
+ const res = await ApiFetch.api.landingpage.apbdes["findMany"].get({ query });
+
+ if (res.data?.success) {
apbdes.findMany.data = res.data.data || [];
- apbdes.findMany.total = res.data.total || 0;
- apbdes.findMany.totalPages = res.data.totalPages || 1;
+ apbdes.findMany.total = res.data.meta?.total || 0;
+ apbdes.findMany.totalPages = res.data.meta?.totalPages || 1;
} else {
- console.error("Failed to load pegawai:", res.data?.message);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
+ toast.error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
- console.error("Error loading pegawai:", error);
+ console.error("FindMany error:", error);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
+ toast.error("Gagal memuat daftar APBDes");
} finally {
apbdes.findMany.loading = false;
}
},
},
+
findUnique: {
- data: null as Prisma.APBDesGetPayload<{
- include: {
- image: true;
- file: true;
- };
- }> | null,
+ data: null as
+ | Prisma.APBDesGetPayload<{
+ include: { image: true; file: true; items: true };
+ }>
+ | null,
+ loading: false,
+ error: null as string | null,
+
async load(id: string) {
+ if (!id || id.trim() === '') {
+ this.data = null;
+ this.error = "ID tidak valid";
+ return;
+ }
+
+ this.loading = true;
+ this.error = null;
+
try {
- const res = await fetch(`/api/landingpage/apbdes/${id}`);
- if (res.ok) {
- const data = await res.json();
- apbdes.findUnique.data = data.data ?? null;
+ // Pastikan URL-nya benar
+ const url = `/api/landingpage/apbdes/${id}`;
+ console.log("🌐 Fetching:", url);
+
+ // Gunakan fetch biasa atau ApiFetch dengan cara yang benar
+ const response = await fetch(url);
+ const res = await response.json();
+
+ console.log("📦 Response:", res);
+
+ if (res.success && res.data) {
+ this.data = res.data;
} else {
- console.error("Failed to fetch data", res.status, res.statusText);
- apbdes.findUnique.data = null;
+ this.data = null;
+ this.error = res.message || "Gagal memuat detail APBDes";
+ toast.error(this.error);
}
} catch (error) {
- console.error("Error fetching data:", error);
- apbdes.findUnique.data = null;
+ console.error("❌ FindUnique error:", error);
+ this.data = null;
+ this.error = "Gagal memuat detail APBDes";
+ toast.error(this.error);
+ } finally {
+ this.loading = false;
}
- },
+ }
},
+
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
- apbdes.delete.loading = true;
+ this.loading = true;
+ const res = await (ApiFetch.api.landingpage.apbdes as any)["del"][id].delete();
- const response = await fetch(`/api/landingpage/apbdes/del/${id}`, {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- const result = await response.json();
-
- if (response.ok && result?.success) {
- toast.success(result.message || "apbdes berhasil dihapus");
- await apbdes.findMany.load(); // refresh list
+ if (res.data?.success) {
+ toast.success("APBDes berhasil dihapus");
+ apbdes.findMany.load();
} else {
- toast.error(result?.message || "Gagal menghapus apbdes");
+ toast.error(res.data?.message || "Gagal menghapus APBDes");
}
- } catch (error) {
- console.error("Gagal delete:", error);
- toast.error("Terjadi kesalahan saat menghapus apbdes");
+ } catch (error: any) {
+ console.error("Delete error:", error);
+ toast.error(error?.message || "Terjadi kesalahan saat menghapus");
} finally {
- apbdes.delete.loading = false;
+ this.loading = false;
}
},
},
+
edit: {
id: "",
- form: { ...defaultapbdesForm },
+ form: { ...defaultApbdesForm },
loading: false,
async load(id: string) {
- if (!id) {
- toast.warn("ID tidak valid");
- return null;
- }
+ if (!id) return toast.warn("ID tidak valid");
try {
- apbdes.edit.loading = true;
+ this.loading = true;
+ const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
- const response = await fetch(`/api/landingpage/apbdes/${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;
+ if (res.data?.success) {
+ const data = res.data.data;
this.id = data.id;
this.form = {
- name: data.name,
- jumlah: data.jumlah,
- imageId: data.imageId,
- fileId: data.fileId,
+ tahun: data.tahun || new Date().getFullYear(),
+ imageId: data.imageId || "",
+ fileId: data.fileId || "",
+ items: (data.items || []).map((item: any) => ({
+ kode: item.kode,
+ uraian: item.uraian,
+ anggaran: item.anggaran,
+ realisasi: item.realisasi,
+ selisih: item.selisih,
+ persentase: item.persentase,
+ level: item.level,
+ tipe: item.tipe || 'pendapatan',
+ })),
};
return data;
} else {
- throw new Error(result?.message || "Gagal memuat data");
+ throw new Error(res.data?.message || "Gagal memuat data");
}
- } catch (error) {
- console.error("Error loading apbdes:", error);
- toast.error(
- error instanceof Error ? error.message : "Gagal memuat data"
- );
- return null;
+ } catch (error: any) {
+ console.error("Edit load error:", error);
+ toast.error(error.message || "Gagal memuat data untuk diedit");
} finally {
- apbdes.edit.loading = false;
+ this.loading = false;
}
},
async update() {
- const cek = templateapbDesaForm.safeParse(apbdes.edit.form);
- if (!cek.success) {
- const err = `[${cek.error.issues
- .map((v) => `${v.path.join(".")}`)
- .join("\n")}] required`;
- return toast.error(err);
+ const parsed = ApbdesFormSchema.safeParse(this.form);
+ if (!parsed.success) {
+ const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
+ toast.error(`Validasi gagal:\n${errors.join("\n")}`);
+ return false;
}
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,
- }),
- });
- 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 apbdes");
- await apbdes.findMany.load(); // refresh list
+ this.loading = true;
+ // Include the ID in the request body
+ const requestData = {
+ ...parsed.data,
+ id: this.id, // Add the ID to the request body
+ };
+
+ const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
+
+ if (res.data?.success) {
+ toast.success("APBDes berhasil diperbarui");
+ apbdes.findMany.load();
return true;
} else {
- throw new Error(result.message || "Gagal mengupdate apbdes");
+ throw new Error(res.data?.message || "Gagal memperbarui APBDes");
}
- } catch (error) {
- console.error("Error updating apbdes:", error);
- toast.error(
- error instanceof Error ? error.message : "Gagal mengupdate apbdes"
- );
+ } catch (error: any) {
+ console.error("Update error:", error);
+ toast.error(error.message || "Gagal memperbarui APBDes");
return false;
} finally {
- apbdes.edit.loading = false;
+ this.loading = false;
}
},
+
+ addItem(item: Partial>) {
+ const normalized = normalizeItem(item);
+ this.form.items.push(normalized);
+ },
+
+ removeItem(index: number) {
+ this.form.items.splice(index, 1);
+ },
+
reset() {
- apbdes.edit.id = "";
- apbdes.edit.form = { ...defaultapbdesForm };
+ this.id = "";
+ this.form = { ...defaultApbdesForm };
},
},
});
-export default apbdes;
+export default apbdes;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts
index b2d28e0e..e3a5fe18 100644
--- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts
+++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts
@@ -53,7 +53,7 @@ const programInovasi = proxy({
].post(formData);
if (res.status === 200) {
programInovasi.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
@@ -388,16 +388,15 @@ const pejabatDesa = proxy({
this.error = null;
try {
- const response = await fetch(
- `/api/landingpage/pejabatdesa/${this.id}`,
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(this.form),
- }
- );
+ // Ensure ID is properly encoded in the URL
+ const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
+ const response = await fetch(url.toString(), {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(this.form),
+ });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
@@ -475,7 +474,7 @@ const mediaSosial = proxy({
);
if (res.status === 200) {
mediaSosial.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts b/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
index 51686ee5..832d6457 100644
--- a/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
+++ b/src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts
@@ -93,6 +93,34 @@ const sdgsDesa = proxy({
}
},
},
+ findManyAll: {
+ data: null as any[] | null,
+ loading: false,
+ load: async () => { // Change to arrow function
+ sdgsDesa.findManyAll.loading = true; // Use the full path to access the property
+ try {
+ const query: any = {};
+
+ const res = await ApiFetch.api.landingpage.sdgsdesa[
+ "findManyAll"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ sdgsDesa.findManyAll.data = res.data.data || [];
+ } else {
+ console.error("Failed to load media sosial:", res.data?.message);
+ sdgsDesa.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading media sosial:", error);
+ sdgsDesa.findManyAll.data = [];
+ } finally {
+ sdgsDesa.findManyAll.loading = false;
+ }
+ },
+ },
findUnique: {
data: null as Prisma.SdgsDesaGetPayload<{
include: {
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts b/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts
index f7092245..b1bf9143 100644
--- a/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts
+++ b/src/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa.ts
@@ -39,7 +39,7 @@ const dataLingkunganDesaState = proxy({
);
if (res.status === 200) {
dataLingkunganDesaState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts b/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts
index 584bad91..80750349 100644
--- a/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts
+++ b/src/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah.ts
@@ -35,7 +35,7 @@ const pengelolaanSampah = proxy({
].post(pengelolaanSampah.create.form);
if (res.status === 200) {
pengelolaanSampah.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts b/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts
index 84d3083e..982bdadb 100644
--- a/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts
+++ b/src/app/admin/(dashboard)/_state/lingkungan/program-penghijauan.ts
@@ -39,7 +39,7 @@ const programPenghijauanState = proxy({
);
if (res.status === 200) {
programPenghijauanState.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts b/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
index d8866b37..73d1dc94 100644
--- a/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
+++ b/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts
@@ -9,34 +9,32 @@ import { z } from "zod";
const templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"),
- nik: z.string().min(1, "NIK harus diisi"),
+ nis: z.string().min(1, "NIS harus diisi"),
+ kelas: z.string().min(1, "Kelas harus diisi"),
+ jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
+ alamatDomisili: z.string().min(1, "Alamat domisili 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"),
+ namaOrtu: z.string().min(1, "Nama ortu harus diisi"),
+ nik: z.string().min(1, "NIK harus diisi"),
+ pekerjaanOrtu: z.string().min(1, "Pekerjaan ortu harus diisi"),
+ penghasilan: z.string().min(1, "Penghasilan ortu 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: "",
+ nis: "",
+ kelas: "",
+ jenisKelamin: "",
+ alamatDomisili: "",
tempatLahir: "",
tanggalLahir: "",
- jenisKelamin: "",
- kewarganegaraan: "",
- agama: "",
- alamatKTP: "",
- alamatDomisili: "",
+ namaOrtu: "",
+ nik: "",
+ pekerjaanOrtu: "",
+ penghasilan: "",
noHp: "",
- email: "",
- statusPernikahan: "",
- ukuranBaju: "",
};
const beasiswaPendaftar = proxy({
@@ -200,18 +198,17 @@ const beasiswaPendaftar = proxy({
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
- nik: data.nik,
+ nis: data.nis,
+ kelas: data.kelas,
+ jenisKelamin: data.jenisKelamin,
+ alamatDomisili: data.alamatDomisili,
tempatLahir: data.tempatLahir,
tanggalLahir: data.tanggalLahir,
- jenisKelamin: data.jenisKelamin,
- kewarganegaraan: data.kewarganegaraan,
- agama: data.agama,
- alamatKTP: data.alamatKTP,
- alamatDomisili: data.alamatDomisili,
+ namaOrtu: data.namaOrtu,
+ nik: data.nik,
+ pekerjaanOrtu: data.pekerjaanOrtu,
+ penghasilan: data.penghasilan,
noHp: data.noHp,
- email: data.email,
- statusPernikahan: data.statusPernikahan,
- ukuranBaju: data.ukuranBaju,
};
return data; // Return the loaded data
} else {
@@ -249,17 +246,17 @@ const beasiswaPendaftar = proxy({
},
body: JSON.stringify({
namaLengkap: this.form.namaLengkap,
- nik: this.form.nik,
- tanggalLahir: this.form.tanggalLahir,
+ nis: this.form.nis,
+ kelas: this.form.kelas,
jenisKelamin: this.form.jenisKelamin,
- kewarganegaraan: this.form.kewarganegaraan,
- agama: this.form.agama,
- alamatKTP: this.form.alamatKTP,
alamatDomisili: this.form.alamatDomisili,
+ tempatLahir: this.form.tempatLahir,
+ tanggalLahir: this.form.tanggalLahir,
+ namaOrtu: this.form.namaOrtu,
+ nik: this.form.nik,
+ pekerjaanOrtu: this.form.pekerjaanOrtu,
+ penghasilan: this.form.penghasilan,
noHp: this.form.noHp,
- email: this.form.email,
- statusPernikahan: this.form.statusPernikahan,
- ukuranBaju: this.form.ukuranBaju,
}),
}
);
@@ -332,7 +329,7 @@ const keunggulanProgram = proxy({
].post(keunggulanProgram.create.form);
if (res.status === 200) {
keunggulanProgram.findMany.load();
- return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
+ return toast.success("Data Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts b/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
index 08189a83..f8ffa416 100644
--- a/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
+++ b/src/app/admin/(dashboard)/_state/pendidikan/data-pendidikan.ts
@@ -42,7 +42,7 @@ const dataPendidikan = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
dataPendidikan.create.form = {
name: "",
jumlah: "",
diff --git a/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts b/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts
index da6411e9..69234af2 100644
--- a/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts
+++ b/src/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital.ts
@@ -55,46 +55,95 @@ const dataPerpustakaan = proxy({
},
},
findMany: {
- data: null as
- | Prisma.DataPerpustakaanGetPayload<{
- include: {
- image: true;
- kategori: true;
- };
- }>[]
- | null,
- page: 1,
- totalPages: 1,
- loading: false,
- search: "",
- load: async (page = 1, limit = 10, search = "", kategori = "") => {
- dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
- dataPerpustakaan.findMany.page = page;
- dataPerpustakaan.findMany.search = search;
-
- try {
- const query: any = { page, limit };
- if (search) query.search = search;
- if (kategori) query.kategori = kategori;
-
- const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
-
- if (res.status === 200 && res.data?.success) {
- dataPerpustakaan.findMany.data = res.data.data ?? [];
- dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
- } else {
- dataPerpustakaan.findMany.data = [];
- dataPerpustakaan.findMany.totalPages = 1;
- }
- } catch (err) {
- console.error("Gagal fetch data perpustakaan paginated:", err);
+ data: null as
+ | Prisma.DataPerpustakaanGetPayload<{
+ include: {
+ image: true;
+ kategori: true;
+ };
+ }>[]
+ | null,
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "", kategori = "") => {
+ const startTime = Date.now();
+ dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
+ dataPerpustakaan.findMany.page = page;
+ dataPerpustakaan.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ dataPerpustakaan.findMany.data = res.data.data ?? [];
+ dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
dataPerpustakaan.findMany.data = [];
dataPerpustakaan.findMany.totalPages = 1;
- } finally {
- dataPerpustakaan.findMany.loading = false;
}
- },
+ } catch (err) {
+ console.error("Gagal fetch data perpustakaan paginated:", err);
+ dataPerpustakaan.findMany.data = [];
+ dataPerpustakaan.findMany.totalPages = 1;
+ } finally {
+ // pastikan minimal 300ms sebelum loading = false (biar UX smooth)
+ const elapsed = Date.now() - startTime;
+ const minDelay = 300;
+ const delay = elapsed < minDelay ? minDelay - elapsed : 0;
+
+ setTimeout(() => {
+ dataPerpustakaan.findMany.loading = false;
+ }, delay);
+ }
},
+ },
+ findManyAll: {
+ data: null as
+ | Prisma.DataPerpustakaanGetPayload<{
+ include: {
+ image: true;
+ kategori: true;
+ };
+ }>[]
+ | null,
+ loading: false,
+ search: "",
+ load: async (search = "", kategori = "") => {
+ dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
+ dataPerpustakaan.findMany.search = search;
+
+ try {
+ const query: any = {};
+ if (search) query.search = search;
+ if (kategori) query.kategori = kategori;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
+ "findManyAll"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ dataPerpustakaan.findManyAll.data = res.data.data ?? [];
+ } else {
+ dataPerpustakaan.findManyAll.data = [];
+ }
+ } catch (err) {
+ console.error("Gagal fetch data perpustakaan paginated:", err);
+ dataPerpustakaan.findManyAll.data = [];
+ } finally {
+ dataPerpustakaan.findManyAll.loading = false;
+ }
+ },
+ },
findUnique: {
data: null as Prisma.DataPerpustakaanGetPayload<{
include: {
@@ -321,17 +370,20 @@ const kategoriBuku = proxy({
totalPages: 1,
loading: false,
search: "",
- load: async (page = 1, limit = 10, search = "") => {
+ load: async (page = 1, limit = 10, search = "") => {
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriBuku.findMany.page = page;
kategoriBuku.findMany.search = search;
-
+
try {
const query: any = { page, limit };
if (search) query.search = search;
-
- const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
-
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
+ "findMany"
+ ].get({ query });
+
if (res.status === 200 && res.data?.success) {
kategoriBuku.findMany.data = res.data.data ?? [];
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
@@ -514,9 +566,319 @@ const kategoriBuku = proxy({
},
});
+const templatePeminjamanBuku = z.object({
+ nama: z.string().min(1, "Nama harus diisi"),
+ noTelp: z.string().min(1, "No Telp harus diisi"),
+ alamat: z.string().min(1, "Alamat harus diisi"),
+ bukuId: z.string().min(1, "Buku ID harus diisi"),
+ tanggalPinjam: z.string().min(1, "Tanggal Pinjam harus diisi"),
+ batasKembali: z.string().min(1, "Batas Kembali harus diisi"),
+ tanggalKembali: z.string().min(1, "Tanggal Kembali harus diisi"),
+ catatan: z.string().min(1, "Catatan harus diisi"),
+});
+
+const defaultPeminjamanBuku = {
+ nama: "",
+ noTelp: "",
+ alamat: "",
+ bukuId: "",
+ tanggalPinjam: "",
+ batasKembali: "",
+ tanggalKembali: "",
+ catatan: "",
+};
+
+interface FormEditData {
+ nama: string;
+ noTelp: string;
+ alamat: string;
+ bukuId: string;
+ buku?: {
+ id: string;
+ judul: string;
+ };
+ tanggalPinjam: string;
+ batasKembali: string;
+ tanggalKembali: string;
+ catatan: string;
+ status: "Dipinjam" | "Dikembalikan" | "Terlambat" | "Dibatalkan";
+}
+
+const editForm: FormEditData = {
+ nama: "",
+ noTelp: "",
+ alamat: "",
+ bukuId: "",
+ tanggalPinjam: "",
+ batasKembali: "",
+ tanggalKembali: "",
+ catatan: "",
+ status: "Dipinjam",
+};
+
+const peminjamanBuku = proxy({
+ create: {
+ form: { ...defaultPeminjamanBuku },
+ loading: false,
+ async create() {
+ const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.create.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ return toast.error(err);
+ }
+
+ try {
+ peminjamanBuku.create.loading = true;
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
+ "create"
+ ].post(peminjamanBuku.create.form);
+ if (res.status === 200) {
+ peminjamanBuku.findMany.load();
+ return toast.success("Data Peminjaman Buku Berhasil Dibuat");
+ }
+ console.log(res);
+ return toast.error("failed create");
+ } catch (error) {
+ console.log(error);
+ return toast.error("failed create");
+ } finally {
+ peminjamanBuku.create.loading = false;
+ }
+ },
+ },
+ findMany: {
+ data: [] as Prisma.PeminjamanBukuGetPayload<{
+ include: {
+ buku: true;
+ };
+ }>[],
+ page: 1,
+ totalPages: 1,
+ loading: false,
+ search: "",
+ load: async (page = 1, limit = 10, search = "") => {
+ peminjamanBuku.findMany.loading = true; // ✅ Akses langsung via nama path
+ peminjamanBuku.findMany.page = page;
+ peminjamanBuku.findMany.search = search;
+
+ try {
+ const query: any = { page, limit };
+ if (search) query.search = search;
+
+ const res =
+ await ApiFetch.api.pendidikan.perpustakaandigital.peminjamanbuku[
+ "findMany"
+ ].get({ query });
+
+ if (res.status === 200 && res.data?.success) {
+ peminjamanBuku.findMany.data = res.data.data ?? [];
+ peminjamanBuku.findMany.totalPages = res.data.totalPages ?? 1;
+ } else {
+ peminjamanBuku.findMany.data = [];
+ peminjamanBuku.findMany.totalPages = 1;
+ }
+ } catch (err) {
+ console.error("Gagal fetch data peminjaman buku paginated:", err);
+ peminjamanBuku.findMany.data = [];
+ peminjamanBuku.findMany.totalPages = 1;
+ } finally {
+ peminjamanBuku.findMany.loading = false;
+ }
+ },
+ },
+ findUnique: {
+ data: null as Prisma.PeminjamanBukuGetPayload<{
+ include: {
+ buku: true;
+ };
+ }> | null,
+ loading: false,
+ async load(id: string) {
+ try {
+ const res = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/${id}`
+ );
+ if (res.ok) {
+ const data = await res.json();
+ peminjamanBuku.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch data", res.status, res.statusText);
+ peminjamanBuku.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ peminjamanBuku.findUnique.data = null;
+ }
+ },
+ },
+ delete: {
+ loading: false,
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
+ try {
+ peminjamanBuku.delete.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/del/${id}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(
+ result.message || "Data Peminjaman Buku berhasil dihapus"
+ );
+ await peminjamanBuku.findMany.load(); // refresh list
+ } else {
+ toast.error(
+ result?.message || "Gagal menghapus Data Peminjaman Buku"
+ );
+ }
+ } catch (error) {
+ console.error("Gagal delete:", error);
+ toast.error("Terjadi kesalahan saat menghapus Data Peminjaman Buku");
+ } finally {
+ peminjamanBuku.delete.loading = false;
+ }
+ },
+ },
+ update: {
+ id: "",
+ form: { ...editForm },
+ loading: false,
+ async load(id: string) {
+ if (!id) {
+ toast.warn("ID tidak valid");
+ return null;
+ }
+
+ try {
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/${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,
+ noTelp: data.noTelp,
+ alamat: data.alamat,
+ bukuId: data.bukuId,
+ tanggalPinjam: data.tanggalPinjam,
+ batasKembali: data.batasKembali,
+ tanggalKembali: data.tanggalKembali,
+ catatan: data.catatan,
+ status: data.status,
+ };
+ return data; // Return the loaded data
+ } else {
+ throw new Error(result?.message || "Gagal memuat data");
+ }
+ } catch (error) {
+ console.error("Error loading peminjaman buku:", error);
+ toast.error(
+ error instanceof Error ? error.message : "Gagal memuat data"
+ );
+ return null;
+ }
+ },
+ async update() {
+ const cek = templatePeminjamanBuku.safeParse(peminjamanBuku.update.form);
+ if (!cek.success) {
+ const err = `[${cek.error.issues
+ .map((v) => `${v.path.join(".")}`)
+ .join("\n")}] required`;
+ toast.error(err);
+ return false;
+ }
+
+ try {
+ peminjamanBuku.update.loading = true;
+
+ const response = await fetch(
+ `/api/pendidikan/perpustakaandigital/peminjamanbuku/${this.id}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ nama: this.form.nama,
+ noTelp: this.form.noTelp,
+ alamat: this.form.alamat,
+ bukuId: this.form.bukuId,
+ tanggalPinjam: this.form.tanggalPinjam,
+ batasKembali: this.form.batasKembali,
+ tanggalKembali: this.form.tanggalKembali,
+ catatan: this.form.catatan,
+ status: this.form.status,
+ }),
+ }
+ );
+
+ 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 peminjaman buku");
+ await peminjamanBuku.findMany.load(); // refresh list
+ return true;
+ } else {
+ throw new Error(
+ result.message || "Gagal update data peminjaman buku"
+ );
+ }
+ } catch (error) {
+ console.error("Error updating data peminjaman buku:", error);
+ toast.error(
+ error instanceof Error
+ ? error.message
+ : "Terjadi kesalahan saat update data peminjaman buku"
+ );
+ return false;
+ } finally {
+ peminjamanBuku.update.loading = false;
+ }
+ },
+ reset() {
+ peminjamanBuku.update.id = "";
+ peminjamanBuku.update.form = { ...editForm };
+ },
+ },
+});
+
const perpustakaanDigitalState = proxy({
dataPerpustakaan,
kategoriBuku,
+ peminjamanBuku,
});
export default perpustakaanDigitalState;
diff --git a/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts
index 61f0f23b..07e3b5be 100644
--- a/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts
+++ b/src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts
@@ -38,7 +38,7 @@ const daftarInformasiPublik = proxy({
].post(daftarInformasiPublik.create.form);
if (res.status === 200) {
daftarInformasiPublik.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {
diff --git a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts
index 4eb184ad..ad74dc6b 100644
--- a/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts
+++ b/src/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur.ts
@@ -41,7 +41,7 @@ const grafikBerdasarkanUmur = proxy({
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
- toast.success("Success create");
+ toast.success("Sukses menambahkan");
grafikBerdasarkanUmur.create.form = {
remaja: "",
dewasa: "",
diff --git a/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts
index 363998cf..0a452cdd 100644
--- a/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts
+++ b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts
@@ -88,7 +88,7 @@ const statepermohonanInformasiPublik = proxy({
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
if (res.status === 200) {
statepermohonanInformasiPublik.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {
@@ -112,7 +112,32 @@ const statepermohonanInformasiPublik = proxy({
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
}
}
- }
+ },
+ findUnique: {
+ data: null as Prisma.PermohonanInformasiPublikGetPayload<{
+ include: {
+ jenisInformasiDiminta: true,
+ caraMemperolehInformasi: true,
+ caraMemperolehSalinanInformasi: true,
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch program inovasi:", res.statusText);
+ statepermohonanInformasiPublik.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching program inovasi:", error);
+ statepermohonanInformasiPublik.findUnique.data = null;
+ }
+ },
+ },
+
})
const statepermohonanInformasiPublikForm = proxy({
diff --git a/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts
index 0decf48a..fc316fa9 100644
--- a/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts
+++ b/src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts
@@ -37,7 +37,7 @@ const permohonanKeberatanInformasi = proxy({
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
if (res.status === 200) {
permohonanKeberatanInformasi.findMany.load();
- return toast.success("success create");
+ return toast.success("Sukses menambahkan");
}
return toast.error("failed create");
} catch (error) {
@@ -57,7 +57,29 @@ const permohonanKeberatanInformasi = proxy({
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
}
}
- }
+ },
+ findUnique: {
+ data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
+ omit: {
+ isActive: true;
+ };
+ }> | null,
+ async load(id: string) {
+ try {
+ const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
+ if (res.ok) {
+ const data = await res.json();
+ permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
+ } else {
+ console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
+ permohonanKeberatanInformasi.findUnique.data = null;
+ }
+ } catch (error) {
+ console.error("Error fetching permohonan keberatan informasi:", error);
+ permohonanKeberatanInformasi.findUnique.data = null;
+ }
+ },
+ }
});
export default permohonanKeberatanInformasi;
diff --git a/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts b/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts
index 0a6304b8..4d0a3df4 100644
--- a/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts
+++ b/src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts
@@ -3,9 +3,6 @@ import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
-/**
- * Schema validasi form ProfilePPID menggunakan Zod.
- */
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
biodata: z.string().min(3, "Biodata minimal 3 karakter"),
@@ -33,25 +30,16 @@ type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
pengalaman: true;
unggulan: true;
imageId: true;
- image?: {
- select: {
- link: true;
- };
- };
+ image?: { select: { link: true } };
};
}>;
-/**
- * Improved State Management - Consolidated and more robust
- */
const stateProfilePPID = proxy({
- // Consolidated data management
profile: {
data: null as ProfilePPIDForm | null,
loading: false,
error: null as string | null,
- // Single method to load profile data
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
@@ -62,52 +50,42 @@ const stateProfilePPID = proxy({
this.error = null;
try {
- const response = await fetch(`/api/ppid/profileppid/${id}`);
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
+ const res = await fetch(`/api/ppid/profileppid/${id}`);
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
- const result = await response.json();
-
+ const result = await res.json();
if (result.success) {
this.data = result.data;
return result.data;
- } else {
- throw new Error(result.message || "Gagal mengambil data profile");
- }
- } catch (error) {
- const errorMessage = (error as Error).message;
- this.error = errorMessage;
- console.error("Load profile error:", errorMessage);
- toast.error("Terjadi kesalahan saat mengambil data profile");
+ } else throw new Error(result.message || "Gagal memuat data profile");
+ } catch (err) {
+ const msg = (err as Error).message;
+ this.error = msg;
+ console.error("Load profile error:", msg);
+ toast.error("Gagal memuat data profile");
return null;
} finally {
this.loading = false;
}
},
- // Reset profile data
reset() {
this.data = null;
this.error = null;
this.loading = false;
- }
+ },
},
- // Edit form management
editForm: {
id: "",
form: { ...defaultForm },
+ originalForm: { ...defaultForm }, // ✅ Tambah field originalForm
loading: false,
error: null as string | null,
- isReadOnly: false, // Flag untuk data yang tidak bisa diedit
- // Initialize form with profile data
initialize(profileData: ProfilePPIDForm) {
this.id = profileData.id;
- this.isReadOnly = false; // Semua data bisa diedit
- this.form = {
+ const data = {
name: profileData.name || "",
biodata: profileData.biodata || "",
riwayat: profileData.riwayat || "",
@@ -115,23 +93,20 @@ const stateProfilePPID = proxy({
unggulan: profileData.unggulan || "",
imageId: profileData.imageId || "",
};
+ this.form = { ...data };
+ this.originalForm = { ...data }; // ✅ Simpan versi original
},
- // Update form field
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
- // Submit form
async submit() {
- // Validate form
- 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}`);
+ const check = templateForm.safeParse(this.form);
+ if (!check.success) {
+ toast.error(
+ check.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
+ );
return false;
}
@@ -139,63 +114,54 @@ const stateProfilePPID = proxy({
this.error = null;
try {
- const response = await fetch(`/api/ppid/profileppid/${this.id}`, {
+ const res = await fetch(`/api/ppid/profileppid/${this.id}`, {
method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
+ 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 (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const result = await res.json();
if (result.success) {
toast.success("Berhasil update profile");
- // Refresh profile data
- await stateProfilePPID.profile.load(this.id);
+ this.originalForm = { ...this.form }; // ✅ Update original setelah sukses
return true;
- } else {
- throw new Error(result.message || "Gagal update profile");
- }
- } catch (error) {
- const errorMessage = (error as Error).message;
- this.error = errorMessage;
- console.error("Update profile error:", errorMessage);
- toast.error("Terjadi kesalahan saat update profile");
+ } else throw new Error(result.message || "Gagal update profile");
+ } catch (err) {
+ const msg = (err as Error).message;
+ this.error = msg;
+ toast.error(msg);
return false;
} finally {
this.loading = false;
}
},
- // Reset form
+ // ✅ Tambahan reset ke original data
+ resetToOriginal() {
+ this.form = { ...this.originalForm };
+ toast.info("Data dikembalikan ke kondisi awal");
+ },
+
reset() {
this.id = "";
this.form = { ...defaultForm };
+ this.originalForm = { ...defaultForm };
this.error = null;
this.loading = false;
- this.isReadOnly = false;
- }
+ },
},
- // Helper methods
async loadForEdit(id: string) {
- const profileData = await this.profile.load(id);
- if (profileData) {
- this.editForm.initialize(profileData);
- }
- return profileData;
+ const data = await this.profile.load(id);
+ if (data) this.editForm.initialize(data);
+ return data;
},
reset() {
this.profile.reset();
this.editForm.reset();
- }
+ },
});
-export default stateProfilePPID;
\ No newline at end of file
+export default stateProfilePPID;
diff --git a/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts b/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts
index 9dba990e..cc326aed 100644
--- a/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts
+++ b/src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts
@@ -381,7 +381,44 @@ const posisiOrganisasi = proxy({
}
},
},
+ findManyAll: {
+ data: [] as Array<{
+ id: string;
+ nama: string;
+ deskripsi: string | null;
+ hierarki: number;
+ }>,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ // Change to arrow function
+ posisiOrganisasi.findManyAll.loading = true; // Use the full path to access the property
+ posisiOrganisasi.findManyAll.search = search;
+ try {
+ const query: any = { search };
+ if (search) query.search = search;
+ const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
+ "find-many-all"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ posisiOrganisasi.findManyAll.data = res.data.data || [];
+
+ } else {
+ console.error("Failed to load posisiOrganisasi:", res.data?.message);
+ posisiOrganisasi.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading posisiOrganisasi:", error);
+ posisiOrganisasi.findManyAll.data = [];
+ } finally {
+ posisiOrganisasi.findManyAll.loading = false;
+ }
+ },
+ },
delete: {
loading: false,
async byId(id: string) {
@@ -524,9 +561,48 @@ const pegawai = proxy({
}
},
},
+ findManyAll: {
+ data: null as
+ | Prisma.PegawaiPPIDGetPayload<{
+ include: {
+ image: true;
+ posisi: true;
+ };
+ }>[]
+ | null,
+ loading: false,
+ search: "",
+ load: async (search = "") => {
+ // Change to arrow function
+ pegawai.findManyAll.loading = true; // Use the full path to access the property
+ pegawai.findManyAll.search = search;
+ try {
+ const query: any = { search };
+ if (search) query.search = search;
+
+ const res = await ApiFetch.api.ppid.strukturppid.pegawai[
+ "find-many-all"
+ ].get({
+ query,
+ });
+
+ if (res.status === 200 && res.data?.success) {
+ pegawai.findManyAll.data = res.data.data || [];
+ } else {
+ console.error("Failed to load pegawai:", res.data?.message);
+ pegawai.findManyAll.data = [];
+ }
+ } catch (error) {
+ console.error("Error loading pegawai:", error);
+ pegawai.findManyAll.data = [];
+ } finally {
+ pegawai.findManyAll.loading = false;
+ }
+ },
+ },
findUnique: {
data: null as
- | (Prisma.PegawaiGetPayload<{
+ | (Prisma.PegawaiPPIDGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
@@ -571,6 +647,31 @@ const pegawai = proxy({
},
},
+ nonActive: {
+ loading: false,
+ async byId(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+ try {
+ pegawai.nonActive.loading = true;
+ const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
+ method: "DELETE", // biasanya nonActive pakai PATCH
+ });
+ const json = await res.json();
+ if (res.ok) {
+ toast.success(json.message ?? "Pegawai berhasil dinonaktifkan");
+ await pegawai.findMany.load(); // refresh data
+ } else {
+ toast.error(json.message ?? "Gagal menonaktifkan pegawai");
+ }
+ } catch (error) {
+ console.error("Gagal nonActive:", error);
+ toast.error("Terjadi kesalahan saat menonaktifkan pegawai");
+ } finally {
+ pegawai.nonActive.loading = false;
+ }
+ },
+ },
+
edit: {
id: "",
form: { ...pegawaiDefaultForm },
diff --git a/src/app/admin/(dashboard)/_state/user/user-state.ts b/src/app/admin/(dashboard)/_state/user/user-state.ts
index 93594956..e3864904 100644
--- a/src/app/admin/(dashboard)/_state/user/user-state.ts
+++ b/src/app/admin/(dashboard)/_state/user/user-state.ts
@@ -90,42 +90,96 @@ const userState = proxy({
}
},
},
- updateActive: {
+ deleteUser: {
loading: false,
- async submit(id: string, isActive: boolean) {
- this.loading = true;
+
+ async delete(id: string) {
+ if (!id) return toast.warn("ID tidak valid");
+
try {
- const res = await fetch(`/api/user/updt`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ id, isActive }),
+ userState.deleteUser.loading = true;
+
+ const response = await fetch(`/api/user/delUser/${id}`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
});
-
- const data = await res.json();
- if (res.status === 200 && data.success) {
- toast.success(data.message);
- userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
+
+ const result = await response.json();
+
+ if (response.ok && result?.success) {
+ toast.success(result.message || "User berhasil dihapus permanen");
+ await userState.findMany.load(); // refresh list user setelah delete
} else {
- toast.error(data.message || "Gagal update status user");
+ toast.error(result?.message || "Gagal menghapus user");
}
- } catch (e) {
- console.error(e);
- toast.error("Gagal update status user");
+ } catch (error) {
+ console.error("Gagal delete user:", error);
+ toast.error("Terjadi kesalahan saat menghapus user");
} finally {
- this.loading = false;
+ userState.deleteUser.loading = false;
}
},
+ },
+ // Di file userState.ts atau dimana state user berada
+
+update: {
+ loading: false,
+
+ async submit(payload: { id: string; isActive?: boolean; roleId?: string }) {
+ this.loading = true;
+ try {
+ const res = await fetch(`/api/user/updt`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ });
+
+ const data = await res.json();
+
+ if (res.status === 200 && data.success) {
+ // ✅ Tampilkan pesan yang berbeda jika role berubah
+ if (data.roleChanged) {
+ toast.success(
+ `${data.message}\n\nUser akan logout otomatis dalam beberapa detik.`,
+ {
+ autoClose: 5000,
+ }
+ );
+ } else {
+ toast.success(data.message);
+ }
+
+ // Refresh list
+ await userState.findMany.load(
+ userState.findMany.page,
+ 10,
+ userState.findMany.search
+ );
+
+ return true; // ✅ Return success untuk handling di component
+ } else {
+ toast.error(data.message || "Gagal update user");
+ return false;
+ }
+ } catch (e) {
+ console.error("❌ Error update user:", e);
+ toast.error("Gagal update user");
+ return false;
+ } finally {
+ this.loading = false;
+ }
},
+},
});
const templateRole = z.object({
name: z.string().min(1, "Nama harus diisi"),
- permissions: z.array(z.string()).min(1, "Permission harus diisi"),
});
const defaultRole = {
name: "",
- permissions: [] as string[],
};
const roleState = proxy({
@@ -237,7 +291,7 @@ const roleState = proxy({
toast.warn("ID tidak valid");
return null;
}
-
+
try {
const response = await fetch(`/api/role/${id}`, {
method: "GET",
@@ -245,31 +299,25 @@ const roleState = proxy({
"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 = {
+
+ // langsung set melalui root state, bukan this
+ roleState.update.id = data.id;
+ roleState.update.form = {
name: data.name,
- permissions: data.permissions,
};
- return data; // Return the loaded data
- } else {
- throw new Error(result?.message || "Gagal memuat data");
+
+ return data;
}
} catch (error) {
console.error("Error loading role:", error);
- toast.error(
- error instanceof Error ? error.message : "Gagal memuat data"
- );
- return null;
+ toast.error("Gagal memuat data");
}
- },
+ },
async update() {
const cek = templateRole.safeParse(roleState.update.form);
if (!cek.success) {
@@ -290,7 +338,6 @@ const roleState = proxy({
},
body: JSON.stringify({
name: this.form.name,
- permissions: this.form.permissions,
}),
});
diff --git a/src/app/admin/(dashboard)/auth/login-admin/page.tsx b/src/app/admin/(dashboard)/auth/login-admin/page.tsx
index ab591207..3457cf45 100644
--- a/src/app/admin/(dashboard)/auth/login-admin/page.tsx
+++ b/src/app/admin/(dashboard)/auth/login-admin/page.tsx
@@ -1,104 +1,103 @@
-'use client'
-import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
+'use client';
+import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
import colors from '@/con/colors';
-import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
-import Link from 'next/link';
+import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
-import { PhoneInput } from "react-international-phone";
-import "react-international-phone/style.css";
+import { PhoneInput } from 'react-international-phone';
+import 'react-international-phone/style.css';
import { toast } from 'react-toastify';
-
-
function Login() {
- const router = useRouter()
- const [phone, setPhone] = useState("")
- const [isError, setError] = useState(false)
- const [loading, setLoading] = useState(false)
+ const router = useRouter();
+ const [phone, setPhone] = useState('');
+ const [loading, setLoading] = useState(false);
+ // Login.tsx
async function onLogin() {
- const nomor = phone.substring(1);
- if (nomor.length <= 4) return setError(true)
+ const cleanPhone = phone.replace(/\D/g, '');
+ console.log(cleanPhone);
+ if (cleanPhone.length < 10) {
+ toast.error('Nomor telepon tidak valid');
+ return;
+ }
try {
setLoading(true);
- const response = await apiFetchLogin({ nomor: nomor })
- if (response && response.success) {
- localStorage.setItem("hipmi_auth_code_id", response.kodeId);
- toast.success(response.message);
- router.push("/validasi", { scroll: false });
+ const response = await apiFetchLogin({ nomor: cleanPhone });
+
+ console.log(response);
+
+ if (!response.success) {
+ toast.error(response.message || 'Gagal memproses login');
+ return;
+ }
+
+ // Simpan nomor untuk register
+ localStorage.setItem('auth_nomor', cleanPhone);
+ if (response.isRegistered) {
+ // ✅ User lama: simpan kodeId
+ localStorage.setItem('auth_kodeId', response.kodeId);
+
+ // ✅ Cookie sudah di-set oleh API, langsung redirect
+ router.push('/validasi'); // Clean URL
} else {
- setLoading(false);
- toast.error(response?.message);
+ // ❌ User baru: langsung ke registrasi (tanpa kodeId)
+ router.push('/registrasi');
}
} catch (error) {
- setLoading(false)
- console.log("Error Login", error)
- toast.error("Terjadi kesalahan saat login")
+ console.error('Error Login:', error);
+ toast.error('Terjadi kesalahan saat login');
+ } finally {
+ setLoading(false);
}
}
return (
-
+
-
-
-
+
+
+
-
+
Login
-
+
-
- {/*
- Masuk Untuk Akses Admin
- setUsername(e.target.value)}
- required
- />
- */}
+
{
- setPhone(val);
- }}
+ value={phone}
+ onChange={(val) => setPhone(val)}
/>
- {isError ? (
- toast.error("Masukan nomor telepon anda")
- ) : (
- ""
- )}
-
+
Masuk
+ loading={loading}
+ >
+ Masuk
-
- Belum punya akun?
-
- Registrasi
-
-
@@ -108,4 +107,4 @@ function Login() {
);
}
-export default Login;
+export default Login;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
index 62d2554b..537ae814 100644
--- a/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
+++ b/src/app/admin/(dashboard)/auth/registrasi-admin/page.tsx
@@ -1,113 +1,127 @@
-/* eslint-disable @typescript-eslint/no-unused-expressions */
-'use client'
-import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
+// app/registrasi/page.tsx
+'use client';
+
+import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
-import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
+import {
+ Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title,
+} from '@mantine/core';
import { useRouter } from 'next/navigation';
-import { useState } from 'react';
-import { PhoneInput } from "react-international-phone";
-import "react-international-phone/style.css";
+import { useEffect, useState } from 'react';
+import { PhoneInput } from 'react-international-phone';
+import 'react-international-phone/style.css';
import { toast } from 'react-toastify';
-function Registrasi() {
- const [phone, setPhone] = useState("")
- const router = useRouter()
- const [value, setValue] = useState("")
- const [isValue, setIsValue] = useState(false);
+export default function Registrasi() {
+ const router = useRouter();
+ const [username, setUsername] = useState('');
const [loading, setLoading] = useState(false);
+ const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
- async function onRegistarsi() {
- if (value.length < 5) {
- toast.error("Username minimal 5 karakter!");
+ // Ambil data dari localStorage (dari login)
+ useEffect(() => {
+ const storedNomor = localStorage.getItem('auth_nomor');
+ if (!storedNomor) {
+ toast.error('Akses tidak valid');
+ router.push('/login');
return;
}
-
- if (value.includes(" ")) {
- toast.error("Username tidak boleh ada spasi!");
+ setPhone(storedNomor);
+ }, [router]);
+
+ const handleRegister = async () => {
+ if (!username || username.trim().length < 5) {
+ toast.error('Username minimal 5 karakter!');
return;
}
-
- if (!phone) {
- toast.error("Nomor telepon wajib diisi!");
+ if (username.includes(' ')) {
+ toast.error('Username tidak boleh ada spasi!');
return;
}
-
+
+ const cleanPhone = phone.replace(/\D/g, '');
+ if (cleanPhone.length < 10) {
+ toast.error('Nomor tidak valid!');
+ return;
+ }
+
try {
setLoading(true);
- const respone = await apiFetchRegister({ nomor: phone, username: value });
+ // ✅ Hanya kirim username & nomor → dapat kodeId
+ const response = await apiFetchRegister({ username, nomor: cleanPhone });
- if (respone.success) {
- router.push("/login", { scroll: false });
- toast.success(respone.message);
+ if (response.success) {
+ // Simpan sementara
+ localStorage.setItem('auth_kodeId', response.kodeId);
+ localStorage.setItem('auth_username', username); // simpan username
- } else {
- setLoading(false);
- toast.error(respone.message);
+ toast.success('Kode verifikasi dikirim!');
+ router.push('/validasi'); // ✅ ke halaman validasi
}
} catch (error) {
+ console.error('Error Registrasi:', error);
+ toast.error('Gagal mengirim OTP');
+ } finally {
setLoading(false);
- console.log("Error Registrasi", error);
}
- }
+ };
+
return (
-
+
-
-
-
-
+
+
+
+
Registrasi
-
+
-
-
+ setUsername(e.currentTarget.value)}
error={
- value.length > 0 && value.length < 5
- ? "Minimal 5 karakter !"
- : value.includes(" ")
- ? "Tidak boleh ada spasi"
- : isValue
- ? "Masukan username anda"
- : ""
+ username.length > 0 && username.length < 5
+ ? 'Minimal 5 karakter!'
+ : username.includes(' ')
+ ? 'Tidak boleh ada spasi'
+ : ''
}
- onChange={(val) => {
- val.currentTarget.value.length > 0 ? setIsValue(false) : "";
- setValue(val.currentTarget.value);
- }}
required
-
/>
-
- Nomor Telepon
+
+
+ Nomor Telepon
{
- setPhone(val);
- }}
+ value={phone}
+ disabled
/>
-
-
+
+
+
-
- Daftar
+
+
+
+ Kirim Kode Verifikasi
+
@@ -116,6 +130,4 @@ function Registrasi() {
);
-}
-
-export default Registrasi;
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
index 862edb33..e6fe0a4b 100644
--- a/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
+++ b/src/app/admin/(dashboard)/auth/validasi-admin/page.tsx
@@ -1,31 +1,306 @@
-'use client'
-import colors from '@/con/colors';
-import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
-import { useRouter } from 'next/navigation';
+'use client';
+
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Center,
+ Loader,
+ Paper,
+ PinInput,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
+import { authStore } from '@/store/authStore';
+
+export default function Validasi() {
+ const router = useRouter();
+
+ const [nomor, setNomor] = useState(null);
+ const [otp, setOtp] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [kodeId, setKodeId] = useState(null);
+ const [isRegistrationFlow, setIsRegistrationFlow] = useState(false);
+
+ // ✅ Deteksi flow dari cookie via API
+ useEffect(() => {
+ const checkFlow = async () => {
+ try {
+ const res = await fetch('/api/auth/get-flow', {
+ credentials: 'include'
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ setIsRegistrationFlow(data.flow === 'register');
+ console.log('🔍 Flow detected from cookie:', data.flow);
+ }
+ } catch (error) {
+ console.error('❌ Error getting flow:', error);
+ setIsRegistrationFlow(false);
+ }
+ };
+
+ checkFlow();
+ }, []);
+
+ useEffect(() => {
+ const storedKodeId = localStorage.getItem('auth_kodeId');
+ if (!storedKodeId) {
+ toast.error('Akses tidak valid');
+ router.replace('/login');
+ return;
+ }
+
+ setKodeId(storedKodeId);
+ const loadOtpData = async () => {
+ try {
+ const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
+ const result = await res.json();
+
+ if (res.ok && result.data?.nomor) {
+ setNomor(result.data.nomor);
+ } else {
+ throw new Error('Data OTP tidak valid');
+ }
+ } catch (error) {
+ console.error('Gagal memuat data OTP:', error);
+ toast.error('Kode verifikasi tidak valid');
+ router.replace('/login');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadOtpData();
+ }, [router]);
+
+ const handleVerify = async () => {
+ if (!kodeId || !nomor || otp.length < 4) return;
+
+ setLoading(true);
+ try {
+ if (isRegistrationFlow) {
+ await handleRegistrationVerification();
+ } else {
+ await handleLoginVerification();
+ }
+ } catch (error) {
+ console.error('Error saat verifikasi:', error);
+ toast.error('Terjadi kesalahan sistem');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRegistrationVerification = async () => {
+ const username = localStorage.getItem('auth_username');
+ if (!username) {
+ toast.error('Data registrasi tidak ditemukan.');
+ return;
+ }
+
+ const cleanNomor = nomor?.replace(/\D/g, '') ?? '';
+ if (cleanNomor.length < 10 || username.trim().length < 5) {
+ toast.error('Data tidak valid');
+ return;
+ }
+
+ // ✅ Verify OTP
+ const verifyRes = await fetch('/api/auth/verify-otp-register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
+ credentials: 'include'
+ });
+
+ const verifyData = await verifyRes.json();
+ if (!verifyRes.ok) {
+ toast.error(verifyData.message || 'Verifikasi OTP gagal');
+ return;
+ }
+
+ // ✅ Finalize registration
+ const finalizeRes = await fetch('/api/auth/finalize-registration', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor: cleanNomor, username, kodeId }),
+ credentials: 'include'
+ });
+
+ const data = await finalizeRes.json();
+
+ // ✅ Check JSON response (bukan redirect)
+ if (data.success) {
+ toast.success('Registrasi berhasil! Menunggu persetujuan admin.');
+ await cleanupStorage();
+
+ // ✅ Client-side redirect
+ setTimeout(() => {
+ window.location.href = '/waiting-room';
+ }, 1000);
+ } else {
+ toast.error(data.message || 'Registrasi gagal');
+ }
+ };
+
+ const handleLoginVerification = async () => {
+ const loginRes = await fetch('/api/auth/verify-otp-login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor, otp, kodeId }),
+ credentials: 'include'
+ });
+
+ const loginData = await loginRes.json();
+
+ if (!loginRes.ok) {
+ toast.error(loginData.message || 'Verifikasi gagal');
+ return;
+ }
+
+ const { id, name, roleId, isActive } = loginData.user;
+
+ authStore.setUser({
+ id,
+ name: name || 'User',
+ roleId: Number(roleId),
+ });
+
+ // ✅ Cleanup setelah login sukses
+ await cleanupStorage();
+
+ if (!isActive) {
+ window.location.href = '/waiting-room';
+ return;
+ }
+
+ const redirectPath = getRedirectPath(Number(roleId));
+ router.replace(redirectPath);
+ };
+
+ const getRedirectPath = (roleId: number): string => {
+ switch (roleId) {
+ case 0:
+ case 1:
+ case 2:
+ return '/admin/landing-page/profil/program-inovasi';
+ case 3:
+ return '/admin/kesehatan/posyandu';
+ case 4:
+ return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
+ default:
+ return '/admin';
+ }
+ };
+
+ // ✅ CLEANUP FUNCTION - Hapus localStorage + Cookie
+ const cleanupStorage = async () => {
+ // Clear localStorage
+ localStorage.removeItem('auth_kodeId');
+ localStorage.removeItem('auth_nomor');
+ localStorage.removeItem('auth_username');
+
+ // Clear cookie
+ try {
+ await fetch('/api/auth/clear-flow', {
+ method: 'POST',
+ credentials: 'include'
+ });
+ } catch (error) {
+ console.error('Error clearing flow cookie:', error);
+ }
+ };
+
+ const handleResend = async () => {
+ if (!nomor) return;
+ try {
+ const res = await fetch('/api/auth/resend', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ nomor }),
+ });
+ const data = await res.json();
+ if (data.success) {
+ localStorage.setItem('auth_kodeId', data.kodeId);
+ toast.success('OTP baru dikirim');
+ } else {
+ toast.error(data.message || 'Gagal mengirim ulang OTP');
+ }
+ } catch {
+ toast.error('Gagal menghubungi server');
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!nomor) return null;
-function Validasi() {
- const router = useRouter()
return (
-
+
-
-
-
+
+
+
-
- Kode Verifikasi
+
+ {isRegistrationFlow ? 'Verifikasi Registrasi' : 'Verifikasi Login'}
+
+ Kami telah mengirim kode ke nomor {nomor}
+
-
-
- Masukkan Kode Verifikasi
-
+
+
+
+ Masukkan Kode Verifikasi
+
+
+
+
-
- router.push("/admin/landing-page/profile/program-inovasi")}>
- Page
+
+
+ Verifikasi
+
+
+
+ Tidak menerima kode?{' '}
+
+ Kirim Ulang
-
+
@@ -33,6 +308,4 @@ function Validasi() {
);
-}
-
-export default Validasi;
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx b/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx
index 4ec40e47..9d44874b 100644
--- a/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx
+++ b/src/app/admin/(dashboard)/desa/_com/layoutTabLayanan.tsx
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
+import { IconBuildingStore, IconFileText, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,36 +14,31 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
label: "Pelayanan Surat Keterangan",
value: "pelayanansuratketerangan",
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
- icon: ,
- tooltip: "Layanan terkait surat keterangan resmi desa"
+ icon:
},
{
label: "Pelayanan Perizinan Berusaha",
value: "pelayananperizinanusaha",
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
- icon: ,
- tooltip: "Layanan untuk izin usaha masyarakat"
+ icon:
},
{
label: "Pelayanan Telunjuk Sakti Desa",
value: "pelayanantelunjuksaktidesa",
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
- icon: ,
- tooltip: "Layanan inovasi khusus desa"
+ icon:
},
{
label: "Pelayanan Penduduk Non-Permanent",
value: "pelayanannonpermanent",
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
- icon: ,
- tooltip: "Pendataan penduduk non-permanent"
+ icon:
},
{
label: "Ajukan Permohonan",
value: "ajukanpermohonan",
href: "/admin/desa/layanan/ajukan_permohonan",
- icon: ,
- tooltip: "Ajukan permohonan"
+ icon:
}
];
@@ -91,14 +86,8 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
{tab.label}
-
))}
diff --git a/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx
index d19825c5..dfd56b13 100644
--- a/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/_com/layoutTabs.tsx
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
+import { IconCategory, IconNews } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconNews, IconCategory } from '@tabler/icons-react';
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -15,15 +15,13 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
label: "List Berita",
value: "list_berita",
href: "/admin/desa/berita/list-berita",
- icon: ,
- tooltip: "Lihat dan kelola semua berita desa"
+ icon:
},
{
label: "Kategori Berita",
value: "kategori_berita",
href: "/admin/desa/berita/kategori-berita",
- icon: ,
- tooltip: "Kelola kategori berita desa"
+ icon:
},
];
@@ -71,46 +69,39 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
-
-
+
+
- {tabs.map((tab, i) => (
-
- {/* Konten dummy, bisa diganti sesuai routing */}
- <>{children}>
-
- ))}
-
-
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
);
}
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
index e42a0071..2e86057b 100644
--- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx
@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
+
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors';
import {
@@ -10,7 +11,7 @@ import {
Stack,
TextInput,
Title,
- Tooltip
+ Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -22,9 +23,14 @@ function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ });
const [formData, setFormData] = useState({
- name: editState.update.form.name || '',
+ name: '',
});
useEffect(() => {
@@ -38,6 +44,9 @@ function EditKategoriBerita() {
setFormData({
name: data.name || '',
});
+ setOriginalData({
+ name: data.name || '',
+ });
}
} catch (error) {
console.error('Error loading kategori Berita:', error);
@@ -48,8 +57,24 @@ function EditKategoriBerita() {
loadKategori();
}, [params?.id]);
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // update global state hanya saat submit
editState.update.form = {
...editState.update.form,
name: formData.name,
@@ -61,6 +86,8 @@ function EditKategoriBerita() {
} catch (error) {
console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -68,7 +95,6 @@ function EditKategoriBerita() {
{/* Back Button + Title */}
-
router.back()}
@@ -77,7 +103,6 @@ function EditKategoriBerita() {
>
-
Edit Kategori Berita
@@ -94,14 +119,26 @@ function EditKategoriBerita() {
>
setFormData({ ...formData, name: e.target.value })}
+ onChange={handleChange}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
index 06fa984b..be739576 100644
--- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx
@@ -7,18 +7,20 @@ import {
Group,
Paper,
Stack,
- Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
@@ -27,16 +29,23 @@ function CreateKategoriBerita() {
};
const handleSubmit = async () => {
- await createState.create.create();
- resetForm();
- router.push('/admin/desa/berita/kategori-berita');
+ setIsSubmitting(true);
+ try {
+ await createState.create.create();
+ resetForm();
+ router.push('/admin/desa/berita/kategori-berita');
+ } catch (error) {
+ console.error('Error creating kategori berita:', error);
+ toast.error('Gagal menambahkan kategori berita');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan back button */}
-
router.back()}
@@ -45,7 +54,6 @@ function CreateKategoriBerita() {
>
-
Tambah Kategori Berita
@@ -62,7 +70,7 @@ function CreateKategoriBerita() {
>
Nama Kategori Berita}
+ label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita"
value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)}
@@ -70,6 +78,17 @@ function CreateKategoriBerita() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
index 94dc8326..391ccb48 100644
--- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx
@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -86,7 +85,6 @@ function ListKategoriBerita({ search }: { search: string }) {
Daftar Kategori Berita
-
}
color="blue"
@@ -97,7 +95,6 @@ function ListKategoriBerita({ search }: { search: string }) {
>
Tambah Baru
-
@@ -123,7 +120,6 @@ function ListKategoriBerita({ search }: { search: string }) {
-
-
-
-
))
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
index 7c52837f..48cd08ea 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx
@@ -6,6 +6,7 @@ import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import {
+ ActionIcon,
Box,
Button,
Group,
@@ -16,10 +17,15 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
-import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
+import {
+ IconArrowBack,
+ IconPhoto,
+ IconUpload,
+ IconX,
+} from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
@@ -33,16 +39,28 @@ function EditBerita() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const [formData, setFormData] = useState({
- judul: beritaState.berita.edit.form.judul || "",
- deskripsi: beritaState.berita.edit.form.deskripsi || "",
- kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "",
- content: beritaState.berita.edit.form.content || "",
- imageId: beritaState.berita.edit.form.imageId || "",
+ judul: "",
+ deskripsi: "",
+ kategoriBeritaId: "",
+ content: "",
+ imageId: "",
});
- // Load berita by id saat pertama kali
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ judul: "",
+ deskripsi: "",
+ kategoriBeritaId: "",
+ content: "",
+ imageId: "",
+ imageUrl: ""
+ });
+
+ // Load kategori + berita
useEffect(() => {
beritaState.kategoriBerita.findMany.load();
+
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
@@ -58,6 +76,15 @@ function EditBerita() {
imageId: data.imageId || "",
});
+ setOriginalData({
+ judul: data.judul || "",
+ deskripsi: data.deskripsi || "",
+ kategoriBeritaId: data.kategoriBeritaId || "",
+ content: data.content || "",
+ imageId: data.imageId || "",
+ imageUrl: data.image?.link || ""
+ });
+
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
@@ -71,8 +98,14 @@ function EditBerita() {
loadBerita();
}, [params?.id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // Update global state hanya sekali di sini
beritaState.berita.edit.form = {
...beritaState.berita.edit.form,
...formData,
@@ -98,27 +131,42 @@ function EditBerita() {
} catch (error) {
console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita");
+ } finally {
+ setIsSubmitting(false);
}
};
+ const handleResetForm = () => {
+ setFormData({
+ judul: originalData.judul,
+ deskripsi: originalData.deskripsi,
+ kategoriBeritaId: originalData.kategoriBeritaId,
+ content: originalData.content,
+ imageId: originalData.imageId,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
return (
+ {/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Berita
+ {/* Form */}
- setFormData({ ...formData, judul: e.target.value })
- }
+ onChange={(e) => handleChange("judul", e.target.value)}
required
/>
-
- setFormData({ ...formData, deskripsi: e.target.value })
- }
- required
- />
-
-
-
- Gambar Berita
-
- {
- const selectedFile = files[0];
- if (selectedFile) {
- setFile(selectedFile);
- setPreviewImage(URL.createObjectURL(selectedFile));
- }
- }}
- onReject={() => toast.error("File tidak valid, gunakan format gambar")}
- maxSize={5 * 1024 ** 2}
- accept={{ "image/*": [] }}
- radius="md"
- p="xl"
- >
-
-
-
-
-
-
-
-
-
-
-
-
- Seret gambar atau klik untuk memilih file
-
-
- Maksimal 5MB, format gambar wajib
-
-
-
-
-
- {previewImage && (
-
-
-
- )}
-
-
-
-
- Konten
-
- {
- setFormData((prev) => ({ ...prev, content: htmlContent }));
- beritaState.berita.edit.form.content = htmlContent;
- }}
- />
-
-
- setFormData({ ...formData, kategoriBeritaId: val || "" })
- }
+ onChange={(val) => handleChange("kategoriBeritaId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -236,18 +201,138 @@ function EditBerita() {
error={!formData.kategoriBeritaId ? "Pilih kategori" : undefined}
/>
+
+
+ Deskripsi Singkat
+
+
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
+ />
+
+
+
+ {/* Upload Gambar */}
+
+
+ Gambar Berita
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() =>
+ toast.error("File tidak valid, gunakan format gambar")
+ }
+ maxSize={5 * 1024 ** 2}
+ accept={{ "image/*": [] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
+
+
+
+
+
+ {previewImage && (
+
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
+
+ )}
+
+
+ {/* Konten */}
+
+
+ Konten
+
+
+ setFormData((prev) => ({ ...prev, content: htmlContent }))
+ }
+ />
+
+
+ {/* Action */}
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx
index 2ffcb31c..54f53634 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/page.tsx
@@ -1,14 +1,14 @@
'use client'
-import { useProxy } from 'valtio/utils';
-import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
-import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
+import colors from '@/con/colors';
function DetailBerita() {
const beritaState = useProxy(stateDashboardBerita);
@@ -80,7 +80,7 @@ function DetailBerita() {
Deskripsi
- {data.deskripsi || '-'}
+
@@ -111,7 +111,6 @@ function DetailBerita() {
{/* Action Button */}
-
{
@@ -124,9 +123,7 @@ function DetailBerita() {
>
-
-
router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
@@ -136,7 +133,6 @@ function DetailBerita() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
index 3100163a..9a1d5acb 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx
@@ -14,7 +14,8 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader,
+ ActionIcon
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks';
@@ -29,6 +30,7 @@ export default function CreateBerita() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load();
@@ -47,42 +49,48 @@ export default function CreateBerita() {
};
const handleSubmit = async () => {
- if (!file) {
- return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true);
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ beritaState.berita.create.form.imageId = uploaded.id;
+
+ await beritaState.berita.create.create();
+
+ resetForm();
+ router.push('/admin/desa/berita/list-berita');
+ } catch (error) {
+ console.error('Error creating berita:', error);
+ toast.error('Terjadi kesalahan saat membuat berita');
+ } finally {
+ setIsSubmitting(false);
}
-
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
-
- const uploaded = res.data?.data;
- if (!uploaded?.id) {
- return toast.error('Gagal mengunggah gambar, silakan coba lagi');
- }
-
- beritaState.berita.create.form.imageId = uploaded.id;
-
- await beritaState.berita.create.create();
-
- resetForm();
- router.push('/admin/desa/berita/list-berita');
};
return (
{/* Header dengan tombol kembali */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Berita
@@ -131,12 +139,17 @@ export default function CreateBerita() {
required
/>
- (beritaState.berita.create.form.deskripsi = e.target.value)}
- />
+
+
+ Deskripsi Singkat
+
+ {
+ beritaState.berita.create.form.deskripsi = htmlContent;
+ }}
+ />
+
@@ -152,7 +165,7 @@ export default function CreateBerita() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -173,7 +186,7 @@ export default function CreateBerita() {
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -202,6 +235,17 @@ export default function CreateBerita() {
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
index 47b628d8..38bd0ffa 100644
--- a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
+++ b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
@@ -68,7 +67,6 @@ function ListBerita({ search }: { search: string }) {
Daftar Berita
-
}
color="blue"
@@ -77,7 +75,6 @@ function ListBerita({ search }: { search: string }) {
>
Tambah Baru
-
diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx
index 3b08fbea..ac66a2df 100644
--- a/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/foto/page.tsx
@@ -13,8 +13,7 @@ import {
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
@@ -103,7 +102,6 @@ export default function ListImage() {
-
-
diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx
index 803a6884..43a029bb 100644
--- a/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/lib/layoutTabs.tsx
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
+import { IconPhoto, IconVideo } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconPhoto, IconVideo } from '@tabler/icons-react';
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,15 +14,13 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
label: "Foto",
value: "foto",
href: "/admin/desa/gallery/foto",
- icon: ,
- tooltip: "Kelola foto-foto galeri desa"
+ icon:
},
{
label: "Video",
value: "video",
href: "/admin/desa/gallery/video",
- icon: ,
- tooltip: "Kelola video galeri desa"
+ icon:
},
];
@@ -70,25 +68,18 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts b/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts
index 26fa1175..afbca78b 100644
--- a/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts
+++ b/src/app/admin/(dashboard)/desa/gallery/lib/youtube-utils.ts
@@ -1,31 +1,11 @@
+// export function convertYoutubeUrlToEmbed(url: string) {
+// const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
+// return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
+// }
+
export function convertYoutubeUrlToEmbed(url: string) {
- const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
+ const videoIdMatch = url.match(
+ /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
+ );
return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
-}
-
-
-
-
-
-
-
-
-
-// (url: string): string | null {
-// const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/;
-// const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/;
-
-// const matchWatch = url.match(watchRegex);
-// const matchShort = url.match(shortRegex);
-
-// if (matchWatch) {
-// return `https://www.youtube.com/embed/${matchWatch[1]}`;
-// }
-
-// if (matchShort) {
-// return `https://www.youtube.com/embed/${matchShort[1]}`;
-// }
-
-// return null;
-// }
-
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
index df68af39..21387eb2 100644
--- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx
@@ -4,6 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import {
+ ActionIcon,
Box,
Button,
Group,
@@ -11,11 +12,11 @@ import {
Stack,
TextInput,
Title,
- Tooltip
+ Loader
} from '@mantine/core';
-import { IconArrowBack } from '@tabler/icons-react';
+import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
@@ -25,23 +26,38 @@ function EditVideo() {
const videoState = useProxy(stateGallery.video);
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ name: "",
+ deskripsi: "",
+ linkVideo: "",
+ });
+
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
linkVideo: '',
});
+ // load data video sekali saat id ada
useEffect(() => {
const loadVideo = async () => {
const id = params?.id as string;
if (!id) return;
+
try {
const data = await videoState.update.load(id);
if (data) {
setFormData({
- name: data.name || '',
- deskripsi: data.deskripsi || '',
- linkVideo: data.linkVideo || '',
+ name: data.name ?? '',
+ deskripsi: data.deskripsi ?? '',
+ linkVideo: data.linkVideo ?? '',
+ });
+ setOriginalData({
+ name: data.name ?? '',
+ deskripsi: data.deskripsi ?? '',
+ linkVideo: data.linkVideo ?? '',
});
}
} catch (error) {
@@ -49,42 +65,69 @@ function EditVideo() {
toast.error('Gagal memuat data video');
}
};
+
loadVideo();
}, [params?.id]);
- const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
+ const handleChange = useCallback(
+ (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ linkVideo: originalData.linkVideo,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
const handleSubmit = async () => {
- const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
- if (!converted) {
- toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
- return;
- }
-
try {
- videoState.update.form = {
- ...videoState.update.form,
- name: formData.name,
- deskripsi: formData.deskripsi,
- linkVideo: formData.linkVideo,
- };
- await videoState.update.update();
- toast.success('Video berhasil diperbarui!');
- router.push('/admin/desa/gallery/video');
+ setIsSubmitting(true);
+ const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
+ if (!converted) {
+ toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
+ return;
+ }
+
+ try {
+ videoState.update.form = {
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ linkVideo: formData.linkVideo,
+ };
+ await videoState.update.update();
+ toast.success('Video berhasil diperbarui!');
+ router.push('/admin/desa/gallery/video');
+ } catch (error) {
+ console.error('Error updating video:', error);
+ toast.error('Terjadi kesalahan saat memperbarui video');
+ }
} catch (error) {
console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video');
+ } finally {
+ setIsSubmitting(false);
}
};
+ const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
+
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Video
@@ -103,7 +146,7 @@ function EditVideo() {
label="Judul Video"
placeholder="Masukkan judul video"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange('name', e.currentTarget.value)}
required
/>
@@ -112,11 +155,11 @@ function EditVideo() {
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo}
- onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })}
+ onChange={(e) => handleChange('linkVideo', e.currentTarget.value)}
required
/>
{embedLink && (
-
+
+ />
+ {
+ setFormData({
+ ...formData,
+ linkVideo: '',
+ });
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -135,11 +198,22 @@ function EditVideo() {
setFormData({ ...formData, deskripsi: val })}
+ onChange={(val) => handleChange('deskripsi', val)}
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx
index 52bef7cd..598c745b 100644
--- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx
@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
-import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -102,6 +102,7 @@ function DetailVideo() {
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
) : (
Tidak ada deskripsi
@@ -110,7 +111,6 @@ function DetailVideo() {
{/* Tombol Aksi */}
-
{
@@ -123,9 +123,7 @@ function DetailVideo() {
>
-
-
@@ -137,7 +135,6 @@ function DetailVideo() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
index 9194a15e..3e53d4cd 100644
--- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx
@@ -3,6 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import {
+ ActionIcon,
Box,
Button,
Group,
@@ -11,9 +12,9 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from '@mantine/core';
-import { IconArrowBack } from '@tabler/icons-react';
+import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
@@ -25,6 +26,7 @@ function CreateVideo() {
const router = useRouter();
const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
videoState.create.form = {
@@ -36,31 +38,37 @@ function CreateVideo() {
};
const handleSubmit = async () => {
- if (!embedLink) {
- toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
- return;
- }
+ try {
+ setIsSubmitting(true);
+ if (!embedLink) {
+ toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
+ return;
+ }
- videoState.create.form.linkVideo = embedLink;
- await videoState.create.create();
- resetForm();
- router.push('/admin/desa/gallery/video');
+ videoState.create.form.linkVideo = embedLink;
+ await videoState.create.create();
+ resetForm();
+ router.push('/admin/desa/gallery/video');
+ } catch (error) {
+ console.error("Error creating video:", error);
+ toast.error("Terjadi kesalahan saat menambahkan video");
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header Back Button + Title */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Video
@@ -98,7 +106,7 @@ function CreateVideo() {
{/* Preview Video */}
{embedLink && (
-
+
+ />
+ {
+ setLink('');
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -128,6 +153,17 @@ function CreateVideo() {
{/* Button Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx
index 232204ff..384fb7d9 100644
--- a/src/app/admin/(dashboard)/desa/gallery/video/page.tsx
+++ b/src/app/admin/(dashboard)/desa/gallery/video/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -74,7 +73,6 @@ function ListVideo({ search }: { search: string }) {
Daftar Video
-
}
color="blue"
@@ -83,7 +81,6 @@ function ListVideo({ search }: { search: string }) {
>
Tambah Baru
-
diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx
index 79fe9452..0ee0d2e2 100644
--- a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/edit/page.tsx
@@ -1,17 +1,17 @@
-'use client'
/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
- Box,
- Button,
- Group,
- Paper,
- Select,
- Stack,
- TextInput,
- Title,
- Tooltip
+ Box,
+ Button,
+ Group,
+ Loader,
+ Paper,
+ Select,
+ Stack,
+ TextInput,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -20,159 +20,199 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditAjukanPermohonan() {
- const router = useRouter();
- const params = useParams();
- const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
+ const router = useRouter();
+ const params = useParams();
+ const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
- const [formData, setFormData] = useState({
- nama: stateAjukan.edit.form.nama,
- nik: stateAjukan.edit.form.nik,
- alamat: stateAjukan.edit.form.alamat,
- nomorKk: stateAjukan.edit.form.nomorKk,
- kategoriId: stateAjukan.edit.form.kategoriId,
- });
+ const [isSubmitting, setIsSubmitting] = useState(false);
- useEffect(() => {
- stateLayananDesa.suratKeterangan.findManyAll.load();
- const loadAjukan = async () => {
- const id = params?.id as string;
- if (!id) return;
+ const [originalData, setOriginalData] = useState({
+ nama: "",
+ nik: "",
+ alamat: "",
+ nomorKk: "",
+ kategoriId: "",
+ });
- try {
- const data = await stateAjukan.edit.load(id);
- if (data) {
- setFormData({
- nama: data.nama || '',
- nik: data.nik || '',
- alamat: data.alamat || '',
- nomorKk: data.nomorKk || '',
- kategoriId: data.kategoriId || '',
- });
- }
- } catch (error) {
- console.error('Error loading ajukan:', error);
- toast.error('Gagal memuat data ajukan');
- }
- };
+ // State lokal form
+ const [formData, setFormData] = useState({
+ nama: '',
+ nik: '',
+ alamat: '',
+ nomorKk: '',
+ kategoriId: '',
+ });
- loadAjukan();
- }, [params?.id]);
+ // Load data awal
+ useEffect(() => {
+ stateLayananDesa.suratKeterangan.findManyAll.load();
- const handleSubmit = async () => {
- try {
- stateAjukan.edit.form = {
- ...stateAjukan.edit.form,
- ...formData,
- };
- toast.success('Ajukan berhasil diperbarui!');
- router.push('/admin/desa/layanan/ajukan_permohonan');
- } catch (error) {
- console.error('Error updating ajukan:', error);
- toast.error('Terjadi kesalahan saat memperbarui ajukan');
+ const loadAjukan = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await stateAjukan.edit.load(id);
+ if (data) {
+ setFormData({
+ nama: data.nama || '',
+ nik: data.nik || '',
+ alamat: data.alamat || '',
+ nomorKk: data.nomorKk || '',
+ kategoriId: data.kategoriId || '',
+ });
+ setOriginalData({
+ nama: data.nama || '',
+ nik: data.nik || '',
+ alamat: data.alamat || '',
+ nomorKk: data.nomorKk || '',
+ kategoriId: data.kategoriId || '',
+ });
}
+ } catch (error) {
+ console.error('Error loading ajukan:', error);
+ toast.error('Gagal memuat data ajukan');
+ }
};
- return (
-
- {/* Back Button */}
-
-
- router.back()} p="xs" radius="md">
-
-
-
-
- Edit Ajukan Permohonan
-
-
+ loadAjukan();
+ }, [params?.id]);
- {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ nik: originalData.nik,
+ alamat: originalData.alamat,
+ nomorKk: originalData.nomorKk,
+ kategoriId: originalData.kategoriId,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ const handleSubmit = async () => {
+ try {
+ setIsSubmitting(true);
+ stateAjukan.edit.form = {
+ ...stateAjukan.edit.form,
+ ...formData,
+ };
+ toast.success('Ajukan berhasil diperbarui!');
+ router.push('/admin/desa/layanan/ajukan_permohonan');
+ } catch (error) {
+ console.error('Error updating ajukan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui ajukan');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ {/* Back Button */}
+
+ router.back()} p="xs" radius="md">
+
+
+
+ Edit Ajukan Permohonan
+
+
+
+
+
+ handleChange('nama', e.target.value)}
+ required
+ />
+
+ handleChange('nik', e.target.value)}
+ required
+ />
+
+ handleChange('alamat', e.target.value)}
+ required
+ />
+
+ handleChange('nomorKk', e.target.value)}
+ required
+ />
+
+ ({
+ label: item.name,
+ value: item.id,
+ }))}
+ value={formData.kategoriId || null}
+ onChange={(val) => handleChange('kategoriId', val || '')}
+ searchable
+ clearable
+ nothingFoundMessage="Tidak ditemukan"
+ required
+ />
+
+
+
-
- setFormData({ ...formData, nama: e.target.value })}
- required
- />
+ Batal
+
- setFormData({ ...formData, nik: e.target.value })}
- required
- />
-
- setFormData({ ...formData, alamat: e.target.value })}
- required
- />
-
- setFormData({ ...formData, nomorKk: e.target.value })}
- required
- />
-
- ({
- label: item.name,
- value: item.id,
- }))}
- value={formData.kategoriId || null}
- onChange={(val: string | null) => {
- if (val) {
- const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
- (item) => item.id === val
- );
- if (selected) {
- stateAjukan.edit.form.kategoriId = selected.id;
- }
- } else {
- stateAjukan.edit.form.kategoriId = '';
- }
- }}
- searchable
- clearable
- nothingFoundMessage="Tidak ditemukan"
- required
- />
-
-
-
- Simpan
-
-
-
-
-
- );
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
+
+
+
+ );
}
export default EditAjukanPermohonan;
diff --git a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx
index 808c0229..c53534c0 100644
--- a/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/ajukan_permohonan/[id]/page.tsx
@@ -3,14 +3,13 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
- Box,
- Button,
- Group,
- Paper,
- Skeleton,
- Stack,
- Text,
- Tooltip
+ Box,
+ Button,
+ Group,
+ Paper,
+ Skeleton,
+ Stack,
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -79,7 +78,7 @@ function DetailAjukanPermohonan() {
Nama
-
+
{data?.nama || '-'}
@@ -97,7 +96,7 @@ function DetailAjukanPermohonan() {
Alamat
-
+
{data?.alamat || '-'}
@@ -121,7 +120,6 @@ function DetailAjukanPermohonan() {
-
{
@@ -135,9 +133,7 @@ function DetailAjukanPermohonan() {
>
-
-
@@ -151,7 +147,6 @@ function DetailAjukanPermohonan() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
new file mode 100644
index 00000000..a425a358
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx
@@ -0,0 +1,192 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Loader,
+ 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 EditPelayananPendudukNonPermanent() {
+ const router = useRouter();
+ const params = useParams();
+ const statePendudukNonPermanent = useProxy(
+ stateLayananDesa.pelayananPendudukNonPermanen
+ );
+
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ });
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ deskripsi: '',
+ });
+
+
+ // Load data sekali dari backend
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) return;
+
+ try {
+ const data = await statePendudukNonPermanent.update.load(id);
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ });
+ setOriginalData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ });
+ }
+ } catch (error) {
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data pelayanan penduduk non permanent');
+ }
+ };
+
+ loadData();
+ }, [params?.id]);
+
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ const handleSubmit = async () => {
+ try {
+ setIsSubmitting(true);
+ if (!statePendudukNonPermanent.findById.data) return;
+
+ // Update global state hanya di submit
+ const updated = {
+ ...statePendudukNonPermanent.findById.data,
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ };
+
+ await statePendudukNonPermanent.update.update(updated);
+ router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
+ } catch (error) {
+ console.error('Error updating data:', error);
+ toast.error('Gagal memuat data pelayanan penduduk non permanent');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+ Edit Pelayanan Penduduk Non Permanent
+
+
+
+
+
+ Edit Pelayanan Penduduk Non Permanent
+
+ {/* Nama Field */}
+ handleChange('name')(e.target.value)}
+ required
+ />
+
+ {/* Deskripsi Field */}
+
+
+ Deskripsi
+
+
+
+
+ {/* Submit Button */}
+
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
+
+
+
+
+ );
+}
+
+export default EditPelayananPendudukNonPermanent;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx
deleted file mode 100644
index eed1a9a3..00000000
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-'use client'
-/* eslint-disable react-hooks/exhaustive-deps */
-import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
-import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
-import colors from '@/con/colors';
-import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
-import { IconArrowBack } from '@tabler/icons-react';
-import { useParams, useRouter } from 'next/navigation';
-import { useEffect, useState } from 'react';
-import { toast } from 'react-toastify';
-import { useProxy } from 'valtio/utils';
-
-function EditPelayananPendudukNonPermanent() {
- const router = useRouter();
- const params = useParams()
- const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen)
- const [formData, setFormData] = useState({
- name: statePendudukNonPermanent.findById.data?.name || '',
- deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '',
- })
-
- useEffect(() => {
- const loadPelayananPerizinan = async () => {
- const id = params?.id as string;
- if (!id) return;
- try {
- const data = await statePendudukNonPermanent.update.load(id);
- if (data) {
- setFormData({
- name: data.name || '',
- deskripsi: data.deskripsi || '',
- });
- }
- } catch (error) {
- console.error("Error loading pelayanan perizinan berusaha:", error);
- toast.error("Gagal memuat data pelayanan perizinan berusaha");
- }
- };
- loadPelayananPerizinan();
- }, [params?.id]);
-
- const handleSubmit = async () => {
- if (statePendudukNonPermanent.findById.data) {
- statePendudukNonPermanent.findById.data.name = formData.name;
- statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi;
- statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data)
- }
- router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent')
- }
- return (
-
-
-
-
- router.back()} p="xs" radius="md">
-
-
-
-
- Edit Pelayanan Penduduk Non Permanent
-
-
-
-
-
- Edit Pelayanan Penduduk Non Permanent
-
- {/* Nama Field */}
-
- setFormData({ ...formData, name: e.target.value })
- }
- required
- />
-
- {/* Posisi Field */}
-
-
- Deskripsi
-
- {
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- }}
- />
-
-
- {/* Submit Button */}
-
-
- {statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
-
-
- router.back()}
- disabled={statePendudukNonPermanent.update.loading}
- >
- Batal
-
-
-
-
-
-
- );
-}
-
-export default EditPelayananPendudukNonPermanent;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx
index 0d89b55b..cfa734ca 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx
@@ -11,8 +11,7 @@ import {
Skeleton,
Stack,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
@@ -27,7 +26,7 @@ function PelayananPendudukNonPermanent() {
);
useShallowEffect(() => {
- pelayananPendudukNonPermanen.findById.load('1');
+ pelayananPendudukNonPermanen.findById.load('edit');
}, []);
if (!pelayananPendudukNonPermanen.findById.data) {
@@ -51,7 +50,6 @@ function PelayananPendudukNonPermanent() {
-
router.push(
- '/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit'
+ `/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
)
}
>
Edit
-
@@ -91,6 +88,7 @@ function PelayananPendudukNonPermanent() {
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
+ style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
new file mode 100644
index 00000000..9fdc6c56
--- /dev/null
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx
@@ -0,0 +1,211 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
+import colors from '@/con/colors';
+import {
+ Box,
+ Button,
+ Group,
+ Loader,
+ Paper,
+ Skeleton,
+ Stack,
+ 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 EditPelayananPerizinanBerusaha() {
+ const router = useRouter();
+ const params = useParams<{ id: string }>();
+ const id = params?.id; // ini langsung string
+ const state = useProxy(stateLayananDesa.pelayananPerizinanBerusaha);
+
+ const [loading, setLoading] = useState(true);
+ const [formData, setFormData] = useState({
+ id: '',
+ name: '',
+ deskripsi: '',
+ link: '',
+ });
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [originalData, setOriginalData] = useState({
+ id: '',
+ name: '',
+ deskripsi: '',
+ link: '',
+ });
+
+ // Load data detail
+ useEffect(() => {
+ if (!id) {
+ toast.error("ID tidak valid");
+ return;
+ }
+
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ const data = await state.findById.load(id);
+ if (data) {
+ setFormData({
+ id: data.id,
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ link: data.link || "",
+ });
+ setOriginalData({
+ id: data.id,
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ link: data.link || "",
+ });
+ } else {
+ toast.error("Data tidak ditemukan");
+ }
+ } catch (error) {
+ console.error("Error loading data:", error);
+ toast.error("Gagal memuat data");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, [id]);
+
+
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ id: originalData.id,
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ link: originalData.link,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ const handleSubmit = async () => {
+ try {
+ setIsSubmitting(true);
+ await state.update.update(formData);
+ router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
+ } catch (error) {
+ console.error('Error updating pelayanan perizinan berusaha:', error);
+ toast.error('Terjadi kesalahan saat update data');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
+
+ Edit Pelayanan Perizinan Berusaha
+
+
+
+ {/* Form */}
+
+
+ Edit Pelayanan Perizinan Berusaha
+
+ handleChange('name')(e.target.value)}
+ required
+ />
+
+ handleChange('link')(e.target.value)}
+ />
+
+
+ Deskripsi
+
+
+
+
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
+
+
+
+
+ );
+}
+
+export default EditPelayananPerizinanBerusaha;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/edit/page.tsx
deleted file mode 100644
index 05488b2d..00000000
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/edit/page.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-'use client'
-/* eslint-disable react-hooks/exhaustive-deps */
-import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
-import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
-import colors from '@/con/colors';
-import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
-import { IconArrowBack } from '@tabler/icons-react';
-import { useParams, useRouter } from 'next/navigation';
-import { useEffect, useState } from 'react';
-import { toast } from 'react-toastify';
-import { useProxy } from 'valtio/utils';
-
-function EditPelayananPerizinanBerusaha() {
- const router = useRouter();
- const params = useParams()
- const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha)
-
- const [formData, setFormData] = useState({
- name: statePerizinanBerusaha.findById.data?.name || '',
- deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '',
- link: statePerizinanBerusaha.findById.data?.link || '',
- })
-
- useEffect(() => {
- const loadPelayananPerizinan = async () => {
- const id = params?.id as string;
- if (!id) return;
- try {
- const data = await statePerizinanBerusaha.update.load(id);
- if (data) {
- setFormData({
- name: data.name || '',
- deskripsi: data.deskripsi || '',
- link: data.link || '',
- });
- }
- } catch (error) {
- console.error("Error loading pelayanan perizinan berusaha:", error);
- toast.error("Gagal memuat data pelayanan perizinan berusaha");
- }
- };
- loadPelayananPerizinan();
- }, [params?.id]);
-
- const handleSubmit = async () => {
- if (statePerizinanBerusaha.findById.data) {
- statePerizinanBerusaha.findById.data.name = formData.name;
- statePerizinanBerusaha.findById.data.deskripsi = formData.deskripsi;
- statePerizinanBerusaha.findById.data.link = formData.link;
- statePerizinanBerusaha.update.update(statePerizinanBerusaha.findById.data)
- }
- router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha')
- }
-
- return (
-
-
- {/* Header Section */}
-
-
- router.back()} p="xs" radius="md">
-
-
-
-
- Edit Pelayanan Perizinan Berusaha
-
-
-
- {/* Form Section */}
-
-
- Edit Pelayanan Perizinan Berusaha
-
- {/* Nama Field */}
- setFormData({ ...formData, name: e.target.value })}
- required
- />
-
- {/* Link Field */}
- setFormData({ ...formData, link: e.target.value })}
- />
-
- {/* Deskripsi Field */}
-
- Deskripsi
- setFormData({ ...formData, deskripsi: val })}
- />
-
-
- {/* Action Buttons */}
-
-
- {statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
-
-
- router.back()}
- disabled={statePerizinanBerusaha.update.loading}
- >
- Batal
-
-
-
-
-
-
- );
-}
-
-export default EditPelayananPerizinanBerusaha;
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx
index 2c6a41fe..a1cac551 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
@@ -15,39 +16,61 @@ import {
StepperCompleted,
StepperStep,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useState } from 'react';
-import stateLayananDesa from '../../../_state/desa/layananDesa';
+import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
-import { useShallowEffect } from '@mantine/hooks';
+import stateLayananDesa from '../../../_state/desa/layananDesa';
function PerizinanBerusaha() {
const router = useRouter();
const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
const [active, setActive] = useState(1);
const nextStep = () =>
setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
- useShallowEffect(() => {
- pelayananPerizinanBerusaha.findById.load('1');
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ // You should get the ID from your router query or params
+ const id = 'edit'; // Replace with actual ID or get from URL params
+ await pelayananPerizinanBerusaha.findById.load(id);
+ } catch (err) {
+ setError('Gagal memuat data');
+ console.error('Error:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
}, []);
- if (!pelayananPerizinanBerusaha.findById.data) {
+ if (loading) {
return (
-
+
);
}
+ if (error || !pelayananPerizinanBerusaha.findById.data) {
+ return (
+
+ {error || 'Data tidak ditemukan'}
+
+ );
+ }
+
const data = pelayananPerizinanBerusaha.findById.data;
return (
@@ -61,7 +84,6 @@ function PerizinanBerusaha() {
-
router.push(
- '/admin/desa/layanan/pelayanan_perizinan_berusaha/edit'
+ `/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
)
}
>
Edit
-
@@ -101,6 +122,7 @@ function PerizinanBerusaha() {
ta="justify"
fz={{ base: '1rem', md: '1.2rem' }}
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
+ style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
>;
+ preview: string | null;
+ setPreview: React.Dispatch>;
+}
+
+// 🔹 File Uploader Component
+const FileUploader: React.FC = ({
+ title,
+ file,
+ setFile,
+ preview,
+ setPreview
+}) => {
+ const handleDrop = (files: File[]) => {
+ const selected = files[0];
+ if (selected) {
+ setFile(selected);
+ setPreview(URL.createObjectURL(selected));
+ }
+ };
+
+ const handleRemove = () => {
+ setPreview(null);
+ setFile(null);
+ };
+
+ return (
+
+
+ {title}
+
+ toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format .png, .jpg, .jpeg, .webp
+
+
+
+
+
+ {preview && (
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+// 🔹 Main Component
function EditSuratKeterangan() {
const router = useRouter();
const params = useParams();
- const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
- const [previewImage, setPreviewImage] = useState(null);
- const [previewImage2, setPreviewImage2] = useState(null);
+ // 🧩 State
+ const [formData, setFormData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ image2Id: '',
+ imageUrl: '',
+ image2Url: '',
+ });
+ const [originalData, setOriginalData] = useState(formData);
const [file, setFile] = useState(null);
const [file2, setFile2] = useState(null);
- const [formData, setFormData] = useState({
- name: stateSurat.edit.form.name,
- deskripsi: stateSurat.edit.form.deskripsi,
- imageId: stateSurat.edit.form.imageId,
- image2Id: stateSurat.edit.form.image2Id,
- });
+ const [previewImage, setPreviewImage] = useState(null);
+ const [previewImage2, setPreviewImage2] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ // 🧭 Load Initial Data
useEffect(() => {
const loadSurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
- const data = await stateSurat.edit.load(id);
- if (data) {
- setFormData({
- name: data.name || '',
- deskripsi: data.deskripsi || '',
- imageId: data.imageId || '',
- image2Id: data.image2Id || '',
- });
+ const data = await stateLayananDesa.suratKeterangan.edit.load(id);
+ if (!data) return;
- setPreviewImage(data.image?.link || null);
- setPreviewImage2(data.image2?.link || null);
- }
+ const mapped: FormData = {
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ image2Id: data.image2Id || '',
+ imageUrl: data.image?.link || '',
+ image2Url: data.image2?.link || ''
+ };
+
+ setFormData(mapped);
+ setOriginalData(mapped);
+
+ if (data.image?.link) setPreviewImage(data.image.link);
+ if (data.image2?.link) setPreviewImage2(data.image2.link);
} catch (error) {
console.error('Error loading surat:', error);
toast.error('Gagal memuat data surat');
@@ -66,50 +182,99 @@ function EditSuratKeterangan() {
loadSurat();
}, [params?.id]);
- const handleSubmit = async () => {
+ // 📤 Upload File Helper
+ const uploadFile = async (file: File): Promise => {
try {
- stateSurat.edit.form = {
- ...stateSurat.edit.form,
- ...formData,
- };
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+ const uploaded = res.data?.data;
+ return uploaded?.id || null;
+ } catch (error) {
+ console.error('Error uploading file:', error);
+ return null;
+ }
+ };
+ // 🔁 Reset Form
+ const handleResetForm = () => {
+ setFormData(originalData);
+ setPreviewImage(originalData.imageUrl || null);
+ setPreviewImage2(originalData.image2Url || null);
+ setFile(null);
+ setFile2(null);
+ toast.info('Form dikembalikan ke data awal');
+ };
+
+ // 💾 Submit Handler
+ const handleSubmit = useCallback(async () => {
+ try {
+ setIsSubmitting(true);
+
+ // ✅ Access original state directly (not proxy)
+ const originalState = stateLayananDesa.suratKeterangan;
+
+ // Update form data properties individually
+ originalState.edit.form.name = formData.name;
+ originalState.edit.form.deskripsi = formData.deskripsi;
+ originalState.edit.form.imageId = formData.imageId;
+ originalState.edit.form.image2Id = formData.image2Id;
+
+ // Upload file 1 if exists
if (file) {
- const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
- const uploaded = res.data?.data;
- if (!uploaded?.id) return toast.error('Gagal upload gambar');
- stateSurat.edit.form.imageId = uploaded.id;
+ const uploadedId = await uploadFile(file);
+ if (!uploadedId) {
+ toast.error('Gagal upload gambar pertama');
+ return;
+ }
+ originalState.edit.form.imageId = uploadedId;
}
+ // Upload file 2 if exists
if (file2) {
- const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
- const uploaded = res.data?.data;
- if (!uploaded?.id) return toast.error('Gagal upload gambar');
- stateSurat.edit.form.image2Id = uploaded.id;
+ const uploadedId = await uploadFile(file2);
+ if (!uploadedId) {
+ toast.error('Gagal upload gambar kedua');
+ return;
+ }
+ originalState.edit.form.image2Id = uploadedId;
}
- await stateSurat.edit.update();
+ // Submit update
+ await originalState.edit.update();
toast.success('Surat berhasil diperbarui!');
router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) {
console.error('Error updating surat:', error);
toast.error('Terjadi kesalahan saat memperbarui surat');
+ } finally {
+ setIsSubmitting(false);
}
+ }, [formData, file, file2, router]);
+
+ // 📝 Form Field Handlers
+ const handleNameChange = (e: React.ChangeEvent) => {
+ setFormData(prev => ({ ...prev, name: e.target.value }));
+ };
+
+ const handleDeskripsiChange = (html: string) => {
+ setFormData(prev => ({ ...prev, deskripsi: html }));
};
return (
- {/* Back Button */}
+ {/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Surat Keterangan
+ {/* Form */}
+ {/* Nama Surat */}
setFormData({ ...formData, name: e.target.value })}
+ onChange={handleNameChange}
required
/>
+ {/* Deskripsi */}
Konten
setFormData({ ...formData, deskripsi: htmlContent })}
+ onChange={handleDeskripsiChange}
/>
- {/* Upload Gambar 1 */}
-
-
- Gambar Konten Pelayanan
-
- {
- const selectedFile = files[0];
- if (selectedFile) {
- setFile(selectedFile);
- setPreviewImage(URL.createObjectURL(selectedFile));
- }
- }}
- onReject={() => toast.error('File tidak valid, gunakan format gambar')}
- maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
- radius="md"
- p="xl"
- >
-
-
-
-
-
-
-
-
-
-
-
-
- Seret gambar atau klik untuk memilih file
-
-
- Maksimal 5MB, format gambar wajib
-
-
-
-
+ {/* Gambar 1 */}
+
- {previewImage && (
-
-
-
- )}
-
-
- {/* Upload Gambar 2 */}
-
-
- Gambar Alur Pelayanan Surat
-
- {
- const selectedFile = files[0];
- if (selectedFile) {
- setFile2(selectedFile);
- setPreviewImage2(URL.createObjectURL(selectedFile));
- }
- }}
- onReject={() => toast.error('File tidak valid, gunakan format gambar')}
- maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
- radius="md"
- p="xl"
- >
-
-
-
-
-
-
-
-
-
-
-
-
- Seret gambar atau klik untuk memilih file
-
-
- Maksimal 5MB, format gambar wajib
-
-
-
-
-
- {previewImage2 && (
-
-
-
- )}
-
+ {/* Gambar 2 */}
+
+ {/* Action Buttons */}
+
+ Batal
+
- Simpan
+ {isSubmitting ? : 'Simpan'}
@@ -271,4 +352,4 @@ function EditSuratKeterangan() {
);
}
-export default EditSuratKeterangan;
+export default EditSuratKeterangan;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx
index 2da12ed5..4c911ca3 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/page.tsx
@@ -10,8 +10,7 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -95,6 +94,7 @@ function DetailSuratKeterangan() {
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
@@ -141,7 +141,6 @@ function DetailSuratKeterangan() {
-
{
@@ -155,9 +154,7 @@ function DetailSuratKeterangan() {
>
-
-
@@ -171,7 +168,6 @@ function DetailSuratKeterangan() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx
index 2af7efc7..9c065b52 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/create/page.tsx
@@ -5,16 +5,17 @@ import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -28,6 +29,7 @@ function CreateSuratKeterangan() {
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateSurat.create.form = {
@@ -46,6 +48,7 @@ function CreateSuratKeterangan() {
}
try {
+ setIsSubmitting(true);
// Upload gambar utama
const res1 = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file,
@@ -78,6 +81,8 @@ function CreateSuratKeterangan() {
} catch (error) {
console.error('Error creating surat keterangan:', error);
toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -85,11 +90,9 @@ function CreateSuratKeterangan() {
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Surat Keterangan
@@ -108,7 +111,7 @@ function CreateSuratKeterangan() {
(stateSurat.create.form.name = val.target.value)}
- label={Nama Surat Keterangan }
+ label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan"
required
/>
@@ -143,7 +146,7 @@ function CreateSuratKeterangan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -164,7 +167,7 @@ function CreateSuratKeterangan() {
{previewImage && (
-
+
+ {
+ setPreviewImage(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -193,7 +213,7 @@ function CreateSuratKeterangan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -214,7 +234,7 @@ function CreateSuratKeterangan() {
{previewImage2 ? (
-
+
+ {
+ setPreviewImage2(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
) : (
@@ -232,6 +269,17 @@ function CreateSuratKeterangan() {
{/* Tombol Simpan */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx
index db80a721..2b90ada8 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx
@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -82,7 +81,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
List Surat Keterangan
-
}
color="blue"
@@ -93,7 +91,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
>
Tambah Baru
-
@@ -119,6 +116,7 @@ function ListSuratKeterangan({ search }: { search: string }) {
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
index a80f7bb1..08ae9730 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/edit/page.tsx
@@ -6,42 +6,54 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
- Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
-import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: stateTelunjukDesa.edit.form.name,
- deskripsi: stateTelunjukDesa.edit.form.deskripsi,
- link: stateTelunjukDesa.edit.form.link,
+ name: '',
+ deskripsi: '',
+ link: '',
});
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ deskripsi: '',
+ link: '',
+ });
+
+ // Load data awal hanya sekali (pas ada id)
useEffect(() => {
- const loadPelayananTelunjukSakti = async () => {
+ const loadData = async () => {
const id = params?.id as string;
if (!id) return;
+
try {
const data = await stateTelunjukDesa.edit.load(id);
if (data) {
setFormData({
- name: data.name || '',
- deskripsi: data.deskripsi || '',
- link: data.link || '',
+ name: data.name ?? '',
+ deskripsi: data.deskripsi ?? '',
+ link: data.link ?? '',
+ });
+ setOriginalData({
+ name: data.name ?? '',
+ deskripsi: data.deskripsi ?? '',
+ link: data.link ?? '',
});
}
} catch (error) {
@@ -49,11 +61,31 @@ function EditPelayananTelunjukSakti() {
toast.error('Gagal memuat data pelayanan telunjuk sakti');
}
};
- loadPelayananTelunjukSakti();
+
+ loadData();
}, [params?.id]);
+ // Handler input controlled
+ const handleChange = useCallback(
+ (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ link: originalData.link,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ // Submit: update global state hanya saat simpan
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form,
...formData,
@@ -64,6 +96,8 @@ function EditPelayananTelunjukSakti() {
} catch (error) {
console.error('Error updating pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -71,11 +105,9 @@ function EditPelayananTelunjukSakti() {
{/* Back Button + Title */}
-
router.back()} p="xs" radius="md">
-
Edit Pelayanan Telunjuk Sakti Desa
@@ -95,31 +127,40 @@ function EditPelayananTelunjukSakti() {
label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange('name', e.target.value)}
required
/>
- {/* Deskripsi pakai editor */}
-
-
- Deskripsi
-
- setFormData({ ...formData, deskripsi: htmlContent })}
- />
-
+ {/* Deskripsi */}
+ handleChange('deskripsi', e.target.value)}
+ label="Judul Link"
+ placeholder="Masukkan judul link"
+ required
+ />
{/* Link */}
setFormData({ ...formData, link: e.target.value })}
+ onChange={(e) => handleChange('link', e.target.value)}
/>
{/* Tombol Simpan */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx
index 37ca8a44..180534c2 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/[id]/page.tsx
@@ -9,8 +9,7 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -103,6 +102,7 @@ function DetailPelayananTelunjukSakti() {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
+ wordBreak: "break-word",
}}
>
{data.link}
@@ -124,11 +124,11 @@ function DetailPelayananTelunjukSakti() {
dangerouslySetInnerHTML={{
__html: data?.deskripsi || '-',
}}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
-
{
@@ -142,9 +142,7 @@ function DetailPelayananTelunjukSakti() {
>
-
-
@@ -158,7 +156,6 @@ function DetailPelayananTelunjukSakti() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
index 5cf7028f..28faa0ff 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/create/page.tsx
@@ -6,21 +6,22 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
- Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useProxy } from 'valtio/utils';
+import { useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateTelunjukDesa.create.form = {
@@ -32,6 +33,7 @@ function CreatePelayananTelunjukDesa() {
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
await stateTelunjukDesa.create.create();
resetForm();
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
@@ -39,6 +41,8 @@ function CreatePelayananTelunjukDesa() {
} catch (error) {
console.error('Error create pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat menambahkan data');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -46,11 +50,9 @@ function CreatePelayananTelunjukDesa() {
{/* Header */}
-
router.back()} p="xs" radius="md">
-
Tambah Pelayanan Telunjuk Sakti Desa
@@ -72,7 +74,7 @@ function CreatePelayananTelunjukDesa() {
onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value;
}}
- label={Nama Pelayanan }
+ label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan telunjuk sakti desa"
required
/>
@@ -83,8 +85,9 @@ function CreatePelayananTelunjukDesa() {
onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value;
}}
- label={Deskripsi }
- placeholder="Masukkan deskripsi pelayanan"
+ label="Judul Link"
+ placeholder="Masukkan judul link"
+ required
/>
{/* Link */}
@@ -93,12 +96,24 @@ function CreatePelayananTelunjukDesa() {
onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value;
}}
- label={Link }
+ label="Link"
placeholder="Masukkan link pelayanan"
+ required
/>
{/* Tombol Simpan */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx
index be6ea158..b553773f 100644
--- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_telunjuk_sakti_desa/page.tsx
@@ -1,159 +1,3 @@
-// /* eslint-disable react-hooks/exhaustive-deps */
-// 'use client'
-// import colors from '@/con/colors';
-// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
-// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
-// import { useRouter } from 'next/navigation';
-// import { useEffect, useMemo, useState } from 'react';
-// import { useProxy } from 'valtio/utils';
-// import HeaderSearch from '../../../_com/header';
-// import JudulList from '../../../_com/judulList';
-// import stateLayananDesa from '../../../_state/desa/layananDesa';
-
-// function PelayananTelunjukSakti() {
-// const [search, setSearch] = useState("");
-// return (
-//
-// }
-// value={search}
-// onChange={(e) => setSearch(e.currentTarget.value)}
-// />
-//
-//
-// );
-// }
-
-// function ListPelayananTelunjukSakti({ search }: { search: string }) {
-// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
-// const router = useRouter()
-
-// const {
-// data,
-// page,
-// totalPages,
-// loading,
-// load,
-// } = telunjukSaktiState.findMany;
-
-// useEffect(() => {
-// load(page, 10)
-// }, [])
-
-// const filteredData = useMemo(() => {
-// if (!data) return [];
-// return data.filter(item => {
-// const keyword = search.toLowerCase();
-// return (
-// item.name?.toLowerCase().includes(keyword) ||
-// item.link?.toLowerCase().includes(keyword) ||
-// item.deskripsi?.toLowerCase().includes(keyword)
-// );
-// })
-// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
-// }, [data, search]);
-
-// if (loading || !data) {
-// return (
-//
-//
-//
-// );
-// }
-
-// if (data.length === 0) {
-// return (
-//
-//
-//
-//
-//
-//
-// Nama
-// Link
-// Detail
-//
-//
-//
-//
-//
-//
-// Tidak ada data
-//
-//
-//
-//
-//
-//
-//
-// );
-// }
-
-// return (
-//
-//
-//
-//
-//
-//
-// Nama
-// Link
-// Detail
-//
-//
-//
-// {filteredData.map((item) => (
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
-//
-//
-//
-//
-//
-// ))}
-//
-//
-//
-//
-// {
-// load(newPage, 10);
-// window.scrollTo(0, 0);
-// }}
-// total={totalPages}
-// mt="md"
-// mb="md"
-// />
-//
-//
-// );
-// }
-
-// export default PelayananTelunjukSakti;
-
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
@@ -174,8 +18,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -225,7 +68,6 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
Daftar Pelayanan Telunjuk Sakti
-
}
color="blue"
@@ -236,7 +78,6 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
>
Tambah Baru
-
@@ -261,7 +102,7 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
-
+
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
index 8e5606dc..53ec0c12 100644
--- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx
@@ -5,16 +5,17 @@ import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -24,18 +25,31 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPenghargaan() {
- const statePenghargaan = useProxy(penghargaanState)
- const router = useRouter()
- const params = useParams()
- const [previewImage, setPreviewImage] = useState(null)
- const [file, setFile] = useState(null)
- const [formData, setFormData] = useState({
- name: statePenghargaan.findUnique.data?.name || '',
- juara: statePenghargaan.findUnique.data?.juara || '',
- deskripsi: statePenghargaan.findUnique.data?.deskripsi || '',
- imageId: statePenghargaan.findUnique.data?.imageId || '',
- })
+ const statePenghargaan = useProxy(penghargaanState);
+ const router = useRouter();
+ const params = useParams();
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ name: "",
+ juara: "",
+ deskripsi: "",
+ imageId: "",
+ imageUrl: "",
+ });
+
+ // Lokal formData
+ const [formData, setFormData] = useState({
+ name: '',
+ juara: '',
+ deskripsi: '',
+ imageId: '',
+ });
+
+ // Load data pertama kali
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
@@ -43,66 +57,88 @@ function EditPenghargaan() {
try {
const data = await statePenghargaan.edit.load(id);
+
if (data) {
- setFormData({
- name: data.name || '',
- juara: data.juara || '',
- deskripsi: data.deskripsi || '',
- imageId: data.imageId || '',
+ const newForm = {
+ name: data.name || "",
+ juara: data.juara || "",
+ deskripsi: data.deskripsi || "",
+ imageId: data.imageId || "",
+ };
+ setFormData(newForm);
+
+ // simpan juga versi original
+ const imageUrl = data.image?.link || "";
+
+ setOriginalData({
+ ...newForm,
+ imageUrl: imageUrl,
});
- if (data?.image?.link) {
- setPreviewImage(data.image.link);
- }
+ setPreviewImage(imageUrl || null);
}
} catch (error) {
- console.error("Error loading penghargaan:", error);
- toast.error("Gagal memuat data penghargaan");
+ console.error('Error loading penghargaan:', error);
+ toast.error('Gagal memuat data penghargaan');
}
};
loadPenghargaan();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ juara: originalData.juara,
+ deskripsi: originalData.deskripsi,
+ imageId: originalData.imageId,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ // Submit
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // Sync ke global state saat submit
+ let imageId = formData.imageId;
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error("Gagal upload gambar");
+ }
+ imageId = uploaded.id;
+ }
+
+ // Update global state form (baru di sini)
statePenghargaan.edit.form = {
...statePenghargaan.edit.form,
name: formData.name,
juara: formData.juara,
deskripsi: formData.deskripsi,
- imageId: formData.imageId,
+ imageId,
}
-
- if (file) {
- const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
- const uploaded = res.data?.data;
-
- if (!uploaded?.id) {
- return toast.error("Gagal upload gambar");
- }
-
- statePenghargaan.edit.form.imageId = uploaded.id;
- }
-
await statePenghargaan.edit.update();
- toast.success("Penghargaan berhasil diperbarui!");
- router.push("/admin/desa/penghargaan");
+ toast.success('Penghargaan berhasil diperbarui!');
+ router.push('/admin/desa/penghargaan');
} catch (error) {
- console.error("Error updating penghargaan:", error);
- toast.error("Terjadi kesalahan saat memperbarui penghargaan");
+ console.error('Error updating penghargaan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui penghargaan');
+ } finally {
+ setIsSubmitting(false);
}
- }
+ };
return (
{/* Tombol Back + Title */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Penghargaan
@@ -123,7 +159,7 @@ function EditPenghargaan() {
label="Judul"
placeholder="Masukkan judul penghargaan"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
@@ -132,7 +168,7 @@ function EditPenghargaan() {
label="Juara"
placeholder="Masukkan juara"
value={formData.juara}
- onChange={(e) => setFormData({ ...formData, juara: e.target.value })}
+ onChange={(e) => setFormData((prev) => ({ ...prev, juara: e.target.value }))}
required
/>
@@ -151,7 +187,7 @@ function EditPenghargaan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -170,21 +206,47 @@ function EditPenghargaan() {
Seret gambar atau klik untuk memilih file
- Maksimal 5MB, format gambar wajib
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && (
-
-
+
+
+
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -196,15 +258,25 @@ function EditPenghargaan() {
{
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- statePenghargaan.edit.form.deskripsi = htmlContent;
- }}
+ onChange={(htmlContent) =>
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
/>
{/* Tombol Simpan */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx
index a8d902e4..fb8fb55e 100644
--- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx
@@ -1,9 +1,5 @@
'use client'
-import React, { useState } from 'react';
-import penghargaanState from '../../../_state/desa/penghargaan';
-import { useProxy } from 'valtio/utils';
-import { useParams, useRouter } from 'next/navigation';
-import { useShallowEffect } from '@mantine/hooks';
+import colors from '@/con/colors';
import {
Box,
Button,
@@ -12,12 +8,15 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
-import colors from '@/con/colors';
+import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import penghargaanState from '../../../_state/desa/penghargaan';
function DetailPenghargaan() {
const statePenghargaan = useProxy(penghargaanState);
@@ -122,38 +121,35 @@ function DetailPenghargaan() {
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
-
- router.push(`/admin/desa/penghargaan/${data.id}/edit`)
- }
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+
+ router.push(`/admin/desa/penghargaan/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
index 94a3c4e4..a76063c9 100644
--- a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx
@@ -2,16 +2,17 @@
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -27,6 +28,7 @@ function CreatePenghargaan() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
statePenghargaan.create.form = {
@@ -40,37 +42,43 @@ function CreatePenghargaan() {
};
const handleSubmit = async () => {
- if (!file) {
- return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true);
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ statePenghargaan.create.form.imageId = uploaded.id;
+
+ await statePenghargaan.create.create();
+ resetForm();
+ router.push('/admin/desa/penghargaan');
+ } catch (error) {
+ console.error('Error creating penghargaan:', error);
+ toast.error('Terjadi kesalahan saat menambahkan penghargaan');
+ } finally {
+ setIsSubmitting(false);
}
-
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
-
- const uploaded = res.data?.data;
-
- if (!uploaded?.id) {
- return toast.error('Gagal mengunggah gambar, silakan coba lagi');
- }
-
- statePenghargaan.create.form.imageId = uploaded.id;
-
- await statePenghargaan.create.create();
- resetForm();
- router.push('/admin/desa/penghargaan');
};
return (
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Penghargaan
@@ -89,7 +97,7 @@ function CreatePenghargaan() {
(statePenghargaan.create.form.name = val.target.value)}
- label={Nama Penghargaan }
+ label="Nama Penghargaan"
placeholder="Masukkan nama penghargaan"
required
/>
@@ -97,7 +105,7 @@ function CreatePenghargaan() {
(statePenghargaan.create.form.juara = val.target.value)}
- label={Juara }
+ label="Juara"
placeholder="Masukkan juara"
required
/>
@@ -125,7 +133,7 @@ function CreatePenghargaan() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -146,7 +154,7 @@ function CreatePenghargaan() {
{previewImage && (
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
{/* Button Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx
index 4e54e837..40e2420b 100644
--- a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx
+++ b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx
@@ -18,8 +18,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -69,7 +68,6 @@ function ListPenghargaan({ search }: { search: string }) {
List Penghargaan
-
}
color="blue"
@@ -78,7 +76,6 @@ function ListPenghargaan({ search }: { search: string }) {
>
Tambah Baru
-
@@ -108,6 +105,7 @@ function ListPenghargaan({ search }: { search: string }) {
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
+ style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx
index e8702d32..3ae568ed 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
+import { IconCategory, IconListDetails } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconListDetails, IconCategory } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,15 +14,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
label: "List Pengumuman",
value: "listpengumuman",
href: "/admin/desa/pengumuman/list-pengumuman",
- icon: ,
- tooltip: "Lihat semua daftar pengumuman"
+ icon:
},
{
label: "Kategori Pengumuman",
value: "kategoripengumuman",
href: "/admin/desa/pengumuman/kategori-pengumuman",
- icon: ,
- tooltip: "Kelola kategori pengumuman"
+ icon:
},
];
@@ -70,19 +68,18 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
index fe07bfd4..1438347d 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx
@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
+
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import {
@@ -10,8 +11,7 @@ import {
Stack,
TextInput,
Title,
- Tooltip,
- Text,
+ Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,11 +23,11 @@ function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [formData, setFormData] = useState({ name: '' });
+ const [originalData, setOriginalData] = useState({ name: '' });
- const [formData, setFormData] = useState({
- name: editState.update.form.name || '',
- });
-
+ // Load data awal sekali aja
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -36,9 +36,8 @@ function EditKategoriPengumuman() {
try {
const data = await editState.update.load(id);
if (data) {
- setFormData({
- name: data.name || '',
- });
+ setFormData({ name: data.name || '' });
+ setOriginalData({ name: data.name || '' });
}
} catch (error) {
console.error('Error loading kategori Pengumuman:', error);
@@ -49,8 +48,17 @@ function EditKategoriPengumuman() {
loadKategori();
}, [params?.id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // Update global state hanya di sini
editState.update.form = {
...editState.update.form,
name: formData.name,
@@ -62,14 +70,23 @@ function EditKategoriPengumuman() {
} catch (error) {
console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
+ } finally {
+ setIsSubmitting(false);
}
};
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+
return (
{/* Header */}
-
router.back()}
@@ -78,7 +95,6 @@ function EditKategoriPengumuman() {
>
-
Edit Kategori Pengumuman
@@ -95,20 +111,25 @@ function EditKategoriPengumuman() {
>
- Nama Kategori Pengumuman
-
- }
+ label="Nama Kategori Pengumuman"
placeholder="Masukkan nama kategori Pengumuman"
value={formData.name}
- onChange={(e) =>
- setFormData({ ...formData, name: e.target.value })
- }
+ onChange={(e) => handleChange('name', e.target.value)}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
index 1284d18f..da1fb9c4 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/create/page.tsx
@@ -7,18 +7,20 @@ import {
Group,
Paper,
Stack,
- Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
function CreateKategoriPengumuman() {
const createState = useProxy(stateDesaPengumuman.category);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
@@ -27,16 +29,22 @@ function CreateKategoriPengumuman() {
};
const handleSubmit = async () => {
- await createState.create.create();
- resetForm();
- router.push('/admin/desa/pengumuman/kategori-pengumuman');
+ try {
+ await createState.create.create();
+ resetForm();
+ router.push('/admin/desa/pengumuman/kategori-pengumuman');
+ } catch (error) {
+ console.error(error);
+ toast.error('Gagal menambahkan kategori pengumuman');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan back button */}
-
router.back()}
@@ -45,7 +53,6 @@ function CreateKategoriPengumuman() {
>
-
Tambah Kategori Pengumuman
@@ -62,7 +69,7 @@ function CreateKategoriPengumuman() {
>
Nama Kategori Pengumuman}
+ label="Nama Kategori Pengumuman"
placeholder="Masukkan nama kategori pengumuman"
value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)}
@@ -70,6 +77,17 @@ function CreateKategoriPengumuman() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx
index 541c0cae..bbd20857 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx
@@ -2,11 +2,13 @@
'use client'
import colors from '@/con/colors';
import {
- Box, Button, Center, Paper, Skeleton, Stack,
+ Box, Button, Center,
+ Pagination,
+ Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
- Text, Title, Tooltip, Pagination
+ Text, Title
} from '@mantine/core';
-import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -66,16 +68,14 @@ function ListKategoriPengumuman({ search }: { search: string }) {
List Kategori Pengumuman
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
+ >
+ Tambah Baru
+
@@ -99,29 +99,25 @@ function ListKategoriPengumuman({ search }: { search: string }) {
{item.name}
-
- router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
- >
-
-
-
+ router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
+ >
+
+
-
- {
- setSelectedId(item.id)
- setModalHapus(true)
- }}>
-
-
-
+ {
+ setSelectedId(item.id)
+ setModalHapus(true)
+ }}>
+
+
))
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
index 745672ee..cbeeceef 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx
@@ -14,7 +14,7 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
@@ -28,16 +28,25 @@ function EditPengumuman() {
const params = useParams();
const [formData, setFormData] = useState({
- judul: editState.pengumuman.edit.form.judul || "",
- deskripsi: editState.pengumuman.edit.form.deskripsi || "",
- categoryPengumumanId:
- editState.pengumuman.edit.form.categoryPengumumanId || "",
- content: editState.pengumuman.edit.form.content || "",
+ judul: "",
+ deskripsi: "",
+ categoryPengumumanId: "",
+ content: "",
});
- // Load pengumuman by id saat pertama kali
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ judul: "",
+ deskripsi: "",
+ categoryPengumumanId: "",
+ content: "",
+ });
+
+ // Load kategori & pengumuman by id saat pertama kali
useEffect(() => {
editState.category.findMany.load();
+
const loadpengumuman = async () => {
const id = params?.id as string;
if (!id) return;
@@ -51,6 +60,12 @@ function EditPengumuman() {
categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || "",
});
+ setOriginalData({
+ judul: data.judul || "",
+ deskripsi: data.deskripsi || "",
+ categoryPengumumanId: data.categoryPengumumanId || "",
+ content: data.content || "",
+ });
}
} catch (error) {
console.error("Error loading pengumuman:", error);
@@ -61,9 +76,14 @@ function EditPengumuman() {
loadpengumuman();
}, [params?.id]);
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
const handleSubmit = async () => {
try {
- // update global state
+ setIsSubmitting(true);
+ // update global state hanya sekali pas submit
editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form,
...formData,
@@ -75,22 +95,32 @@ function EditPengumuman() {
} catch (error) {
console.error("Error updating pengumuman:", error);
toast.error("Terjadi kesalahan saat memperbarui pengumuman");
+ } finally {
+ setIsSubmitting(false);
}
};
+ const handleResetForm = () => {
+ setFormData({
+ judul: originalData.judul,
+ deskripsi: originalData.deskripsi,
+ categoryPengumumanId: originalData.categoryPengumumanId,
+ content: originalData.content,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
return (
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Pengumuman
@@ -109,7 +139,7 @@ function EditPengumuman() {
label="Judul Pengumuman"
placeholder="Masukkan judul"
value={formData.judul}
- onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
+ onChange={(e) => handleChange("judul", e.target.value)}
required
/>
@@ -117,17 +147,13 @@ function EditPengumuman() {
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
value={formData.deskripsi}
- onChange={(e) =>
- setFormData({ ...formData, deskripsi: e.target.value })
- }
+ onChange={(e) => handleChange("deskripsi", e.target.value)}
required
/>
- setFormData({ ...formData, categoryPengumumanId: val || "" })
- }
+ onChange={(val) => handleChange("categoryPengumumanId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -150,24 +176,34 @@ function EditPengumuman() {
- setFormData({ ...formData, content: htmlContent })
- }
+ onChange={(htmlContent) => handleChange("content", htmlContent)}
/>
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx
index eaa244ba..5484c688 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx
@@ -1,5 +1,7 @@
'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
+import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import {
Box,
@@ -8,16 +10,13 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
-import { useRouter, useParams } from 'next/navigation';
+import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
-import { useShallowEffect } from '@mantine/hooks';
-import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
-import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
export default function DetailPengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman);
@@ -97,7 +96,7 @@ export default function DetailPengumuman() {
Deskripsi
-
+
{data?.deskripsi || '-'}
@@ -112,11 +111,11 @@ export default function DetailPengumuman() {
dangerouslySetInnerHTML={{
__html: data?.content || '-',
}}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
-
{
@@ -129,9 +128,7 @@ export default function DetailPengumuman() {
>
-
-
@@ -145,7 +142,6 @@ export default function DetailPengumuman() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
index fb142b99..44a40fb0 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx
@@ -13,25 +13,36 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
useShallowEffect(() => {
pengumumanState.category.findMany.load();
}, []);
const handleSubmit = async () => {
- await pengumumanState.pengumuman.create.create();
- resetForm();
- router.push('/admin/desa/pengumuman/list-pengumuman');
+ try {
+ setIsSubmitting(true);
+ await pengumumanState.pengumuman.create.create();
+ resetForm();
+ router.push('/admin/desa/pengumuman/list-pengumuman');
+ } catch (error) {
+ console.error('Error creating pengumuman:', error);
+ toast.error('Terjadi kesalahan saat membuat pengumuman');
+ } finally {
+ setIsSubmitting(false);
+ }
};
const resetForm = () => {
@@ -47,11 +58,9 @@ function CreatePengumuman() {
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Pengumuman
@@ -70,32 +79,43 @@ function CreatePengumuman() {
(pengumumanState.pengumuman.create.form.judul = val.target.value)}
- label={Judul }
+ label="Judul"
placeholder="Masukkan judul pengumuman"
required
/>
{/* Kategori */}
Kategori}
+ label="Kategori"
placeholder="Pilih kategori"
- value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""}
- onChange={(val) => {
- pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
- }}
data={pengumumanState.category.findMany.data?.map((item) => ({
label: item.name,
value: item.id,
- }))}
+ })) || []}
+ value={pengumumanState.pengumuman.create.form.categoryPengumumanId || null}
+ onChange={(val: string | null) => {
+ if (val) {
+ const selected = pengumumanState.category.findMany.data?.find(
+ (item) => item.id === val
+ );
+ if (selected) {
+ pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
+ }
+ } else {
+ pengumumanState.pengumuman.create.form.categoryPengumumanId = '';
+ }
+ }}
searchable
+ clearable
nothingFoundMessage="Tidak ditemukan"
+ required
/>
{/* Deskripsi Singkat */}
(pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
- label={Deskripsi Singkat }
+ label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
required
/>
@@ -115,6 +135,17 @@ function CreatePengumuman() {
{/* Tombol Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx
index e4c819e4..358fc1c5 100644
--- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx
+++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx
@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
@@ -69,16 +68,14 @@ function ListPengumuman({ search }: { search: string }) {
Daftar Pengumuman
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
index dc981aff..cccd64a8 100644
--- a/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/_lib/layoutTabs.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconCategory, IconListCheck } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -14,15 +14,13 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
label: "List Potensi",
value: "list_potensi",
href: "/admin/desa/potensi/list-potensi",
- icon: ,
- tooltip: "Lihat semua potensi desa"
+ icon:
},
{
label: "Kategori Potensi",
value: "kategori_potensi",
href: "/admin/desa/potensi/kategori-potensi",
- icon: ,
- tooltip: "Kelola kategori potensi"
+ icon:
},
];
@@ -70,19 +68,18 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
index 73092b8f..4c9f4c26 100644
--- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx
@@ -10,7 +10,7 @@ import {
Stack,
TextInput,
Title,
- Tooltip
+ Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -24,9 +24,16 @@ function EditKategoriPotensi() {
const params = useParams();
const [formData, setFormData] = useState({
- nama: editState.update.form.nama || '',
+ nama: '',
});
+ const [originalData, setOriginalData] = useState({
+ nama: '',
+ });
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Load data dari backend -> isi ke formData lokal
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -38,6 +45,9 @@ function EditKategoriPotensi() {
setFormData({
nama: data.nama || '',
});
+ setOriginalData({
+ nama: data.nama || '',
+ });
}
} catch (error) {
console.error('Error loading kategori potensi:', error);
@@ -48,8 +58,24 @@ function EditKategoriPotensi() {
loadKategori();
}, [params?.id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // Update global state hanya pas submit
editState.update.form = {
...editState.update.form,
nama: formData.nama,
@@ -61,17 +87,22 @@ function EditKategoriPotensi() {
} catch (error) {
console.error('Error updating kategori potensi:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori potensi');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
+ router.back()}
+ p="xs"
+ radius="md"
+ >
-
Edit Kategori Potensi
@@ -90,11 +121,22 @@ function EditKategoriPotensi() {
label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi"
value={formData.nama}
- onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
+ onChange={(e) => handleChange('nama', e.currentTarget.value)}
required
/>
-
+
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
index 61a46a0a..5647f386 100644
--- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
@@ -7,18 +7,19 @@ import {
Group,
Paper,
Stack,
- Text,
TextInput,
Title,
- Tooltip,
+ Loader
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
+import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function CreateKategoriPotensi() {
const createState = useProxy(potensiDesaState.kategoriPotensi);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
@@ -27,25 +28,30 @@ function CreateKategoriPotensi() {
};
const handleSubmit = async () => {
- await createState.create.create();
- resetForm();
- router.push('/admin/desa/potensi/kategori-potensi');
+ try {
+ setIsSubmitting(true);
+ await createState.create.create();
+ resetForm();
+ router.push('/admin/desa/potensi/kategori-potensi');
+ } catch (error) {
+ console.error('Error creating kategori potensi:', error);
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan back button */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Kategori Potensi
@@ -62,7 +68,7 @@ function CreateKategoriPotensi() {
>
Nama Kategori Potensi}
+ label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi"
value={createState.create.form.nama || ''}
onChange={(e) => (createState.create.form.nama = e.target.value)}
@@ -70,6 +76,17 @@ function CreateKategoriPotensi() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
index 3d408f8f..55e51247 100644
--- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx
@@ -1,14 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core';
-import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
-import potensiDesaState from '../../../_state/desa/potensi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import potensiDesaState from '../../../_state/desa/potensi';
function KategoriPotensi() {
const [search, setSearch] = useState('');
@@ -62,7 +62,6 @@ function ListKategoriPotensi({ search }: { search: string }) {
List Kategori Potensi
-
}
color="blue"
@@ -71,7 +70,6 @@ function ListKategoriPotensi({ search }: { search: string }) {
>
Tambah Baru
-
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
index 1bcdc41a..bf2197e6 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx
@@ -16,7 +16,8 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader,
+ ActionIcon
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
@@ -39,9 +40,25 @@ function EditPotensi() {
content: "",
imageId: "",
});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ name: "",
+ deskripsi: "",
+ kategoriId: "",
+ content: "",
+ imageId: "",
+ imageUrl: "",
+ });
+
+ // handle input changes
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load();
+
const loadPotensi = async () => {
const id = params?.id as string;
if (!id) return;
@@ -57,9 +74,16 @@ function EditPotensi() {
imageId: data.imageId || "",
});
- if (data?.image?.link) {
- setPreviewImage(data.image.link);
- }
+ setOriginalData({
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ kategoriId: data.kategoriId || "",
+ content: data.content || "",
+ imageId: data.imageId || "",
+ imageUrl: data.image?.link || "",
+ });
+ setPreviewImage(data.image.link);
+
}
} catch (error) {
console.error("Error loading potensi:", error);
@@ -70,41 +94,65 @@ function EditPotensi() {
loadPotensi();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name || "",
+ deskripsi: originalData.deskripsi || "",
+ kategoriId: originalData.kategoriId || "",
+ content: originalData.content || "",
+ imageId: originalData.imageId || ""
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+
const handleSubmit = async () => {
try {
- potensiState.edit.form = {
- ...potensiState.edit.form,
- ...formData,
- };
-
+ setIsSubmitting(true);
+ let imageId = formData.imageId;
if (file) {
- const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
- potensiState.edit.form.imageId = uploaded.id;
+ imageId = uploaded.id;
}
+ potensiState.edit.form = {
+ ...formData,
+ imageId,
+ };
+
await potensiState.edit.update();
toast.success("Potensi berhasil diperbarui!");
router.push("/admin/desa/potensi/list-potensi");
} catch (error) {
console.error("Error updating potensi:", error);
toast.error("Terjadi kesalahan saat memperbarui potensi");
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Potensi Desa
@@ -123,21 +171,51 @@ function EditPotensi() {
label="Judul Potensi"
placeholder="Masukkan judul"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange("name", e.target.value)}
required
/>
- setFormData({ ...formData, deskripsi: e.target.value })}
- required
- />
+
+
+ Deskripsi Singkat
+
+
+ handleChange("deskripsi", htmlContent)
+ }
+ />
+
({
+ label: item.nama,
+ value: item.id,
+ })) || []}
+ value={formData.kategoriId || null}
+ onChange={(val: string | null) => {
+ if (val) {
+ const selected = potensiDesaState.kategoriPotensi.findMany.data?.find(
+ (item) => item.id === val
+ );
+ if (selected) {
+ handleChange("kategoriId", selected.id);
+ }
+ } else {
+ handleChange("kategoriId", "");
+ }
+ }}
+ searchable
+ clearable
+ nothingFoundMessage="Tidak ditemukan"
+ required
+ />
+
+ {/* setFormData({ ...formData, kategoriId: val || "" })}
+ onChange={(val) => handleChange("kategoriId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -150,7 +228,7 @@ function EditPotensi() {
searchable
required
error={!formData.kategoriId ? "Pilih kategori" : undefined}
- />
+ /> */}
@@ -164,7 +242,9 @@ function EditPotensi() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
- onReject={() => toast.error("File tidak valid, gunakan format gambar")}
+ onReject={() =>
+ toast.error("File tidak valid, gunakan format gambar")
+ }
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
@@ -172,7 +252,11 @@ function EditPotensi() {
>
-
+
@@ -185,25 +269,45 @@ function EditPotensi() {
Seret gambar atau klik untuk memilih file
- Maksimal 5MB, format gambar wajib
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -214,22 +318,36 @@ function EditPotensi() {
setFormData({ ...formData, content: htmlContent })}
+ onChange={(htmlContent) =>
+ handleChange("content", htmlContent)
+ }
/>
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
index dc40fae6..b2e5cf17 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx
@@ -1,13 +1,13 @@
'use client'
-import colors from '@/con/colors';
-import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
-import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
-import { useRouter, useParams } from 'next/navigation';
-import { useState } from 'react';
-import { useProxy } from 'valtio/utils';
-import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
+import colors from '@/con/colors';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
export default function DetailPotensi() {
const router = useRouter();
@@ -25,7 +25,7 @@ export default function DetailPotensi() {
potensiState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
- router.push("/admin/desa/potensi");
+ router.push("/admin/desa/potensi/list-potensi");
}
};
@@ -77,7 +77,7 @@ export default function DetailPotensi() {
Deskripsi
- {data.deskripsi || '-'}
+
@@ -103,11 +103,11 @@ export default function DetailPotensi() {
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
-
{
@@ -120,9 +120,7 @@ export default function DetailPotensi() {
>
-
-
@@ -134,7 +132,6 @@ export default function DetailPotensi() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
index b7a9f1dd..d18bca1b 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx
@@ -15,7 +15,8 @@ import {
Text,
TextInput,
Title,
- Tooltip,
+ Loader,
+ ActionIcon
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -29,30 +30,39 @@ function CreatePotensi() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load();
}, []);
const handleSubmit = async () => {
- if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true);
+ if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
- const uploaded = res.data?.data;
- if (!uploaded?.id) {
- return toast.error('Gagal upload gambar');
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal upload gambar');
+ }
+
+ potensiState.create.form.imageId = uploaded.id;
+
+ await potensiState.create.create();
+
+ resetForm();
+ router.push('/admin/desa/potensi/list-potensi');
+ } catch (error) {
+ console.error('Error creating potensi:', error);
+ toast.error('Terjadi kesalahan saat menambahkan potensi');
+ } finally {
+ setIsSubmitting(false);
}
-
- potensiState.create.form.imageId = uploaded.id;
-
- await potensiState.create.create();
-
- resetForm();
- router.push('/admin/desa/potensi/list-potensi');
};
const resetForm = () => {
@@ -72,11 +82,9 @@ function CreatePotensi() {
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Potensi Desa
@@ -95,23 +103,53 @@ function CreatePotensi() {
(potensiState.create.form.name = val.target.value)}
- label={Judul }
+ label="Judul"
placeholder="Masukkan judul potensi"
required
/>
{/* Deskripsi */}
- (potensiState.create.form.deskripsi = val.target.value)}
- label={Deskripsi }
- placeholder="Masukkan deskripsi singkat"
- required
- />
+
+
+ Deskripsi Singkat
+
+ {
+ potensiState.create.form.deskripsi = htmlContent;
+ }}
+ />
+
{/* Kategori */}
Kategori}
+ label="Kategori"
+ placeholder="Pilih kategori"
+ data={potensiDesaState.kategoriPotensi.findMany.data?.map((item) => ({
+ label: item.nama,
+ value: item.id,
+ })) || []}
+ value={potensiState.create.form.kategoriId || null}
+ onChange={(val: string | null) => {
+ if (val) {
+ const selected = potensiDesaState.kategoriPotensi.findMany.data?.find(
+ (item) => item.id === val
+ );
+ if (selected) {
+ potensiState.create.form.kategoriId = selected.id;
+ }
+ } else {
+ potensiState.create.form.kategoriId = '';
+ }
+ }}
+ searchable
+ clearable
+ nothingFoundMessage="Tidak ditemukan"
+ required
+ />
+
+ {/* {
@@ -121,7 +159,7 @@ function CreatePotensi() {
value: item.id,
label: item.nama,
}))}
- />
+ /> */}
{/* Upload Gambar */}
@@ -138,7 +176,7 @@ function CreatePotensi() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -156,17 +194,44 @@ function CreatePotensi() {
Seret gambar atau klik untuk memilih file (maks 5MB)
+
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
+
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -186,6 +251,17 @@ function CreatePotensi() {
{/* Tombol Simpan */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
index 2a7c6eb8..9ee2259b 100644
--- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
+++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx
@@ -18,8 +18,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -76,7 +75,6 @@ function ListPotensi({ search }: { search: string }) {
Daftar Potensi Desa
-
}
color="blue"
@@ -85,7 +83,6 @@ function ListPotensi({ search }: { search: string }) {
>
Tambah Baru
-
@@ -102,19 +99,25 @@ function ListPotensi({ search }: { search: string }) {
filteredData.map((item) => (
-
- {item.name}
-
+
+
+ {item.name}
+
+
- {item.kategori?.nama || '-'}
+
+ {item.kategori?.nama || '-'}
+
diff --git a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx
index 7554a1c8..25771ec3 100644
--- a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
+import { IconCalendar, IconUser, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -14,22 +14,19 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
label: "Profile Desa",
value: "profiledesa",
href: "/admin/desa/profile/profile-desa",
- icon: ,
- tooltip: "Lihat dan kelola profil desa"
+ icon:
},
{
label: "Profile Perbekel",
value: "profileperbekel",
href: "/admin/desa/profile/profile-perbekel",
- icon: ,
- tooltip: "Kelola data Perbekel"
+ icon:
},
{
label: "Profile Perbekel Dari Masa Ke Masa",
value: "profile-perbekel-dari-masa-ke-masa",
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
- icon: ,
- tooltip: "Riwayat Perbekel dari masa ke masa"
+ icon:
}
];
@@ -76,42 +73,41 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
- {tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
- ))}
-
-
-
{tabs.map((tab, i) => (
-
- {/* Konten dummy, bisa diganti sesuai routing */}
- <>{children}>
-
+ {tab.label}
+
))}
-
-
- );
+
+
+
+ {tabs.map((tab, i) => (
+
+ {/* Konten dummy, bisa diganti sesuai routing */}
+ <>{children}>
+
+ ))}
+
+
+ );
}
- export default LayoutTabsDetail;
+export default LayoutTabsDetail;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx
index c0ee0456..6d182103 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx
@@ -1,154 +1,244 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-'use client'
+'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
-import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import {
+ Alert,
+ Box,
+ Button,
+ Center,
+ Group,
+ Loader,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+} from '@mantine/core';
import { IconAlertCircle, 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';
+// 🧩 Type untuk form
+interface FormData {
+ judul: string;
+ deskripsi: string;
+}
+
+// 🧩 Main Component
function Page() {
- const lambangState = useProxy(stateProfileDesa.lambangDesa)
- const router = useRouter()
- const params = useParams()
- const [isSubmitting, setIsSubmitting] = useState(false);
- // Load data
- useEffect(() => {
- const loadData = async () => {
- const id = params?.id as string;
- if (!id) {
- toast.error("ID tidak valid");
- router.push("/admin/desa/profile/profile-desa");
- return;
- }
+ const router = useRouter();
+ const params = useParams();
- try {
- const data = await lambangState.findUnique.load(id);
- lambangState.update.initialize(data);
- } catch (error) {
- console.error("Error loading lambang:", error);
- toast.error("Gagal memuat data lambang desa");
- }
- };
+ const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
+ const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' });
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [loadError, setLoadError] = useState(null);
- loadData();
+ // 🧭 Load data awal
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error('ID tidak valid');
+ router.push('/admin/desa/profile/profile-desa');
+ return;
+ }
- return () => {
- lambangState.update.reset();
- lambangState.findUnique.reset();
- };
- }, [params?.id, router]);
+ setIsLoading(true);
+ setLoadError(null);
- const handleSubmit = async () => {
- if (isSubmitting || !lambangState.update.form.judul.trim()) {
- toast.error("Judul wajib diisi");
- return;
- }
-
- setIsSubmitting(true);
-
- try {
- const success = await lambangState.update.submit();
-
- if (success) {
- toast.success("Data berhasil disimpan");
- router.push("/admin/desa/profile/profile-desa");
- }
- } catch (error) {
- console.error("Error update lambang desa:", error);
- toast.error("Terjadi kesalahan saat update lambang desa");
- } finally {
- setIsSubmitting(false);
+ try {
+ const data = await stateProfileDesa.lambangDesa.findUnique.load(id);
+
+ if (data) {
+ const initial: FormData = {
+ judul: data.judul || '',
+ deskripsi: data.deskripsi || '',
+ };
+ setFormData(initial);
+ setOriginalData(initial);
+
+ // Penting untuk isi id di state sebelum submit
+ stateProfileDesa.lambangDesa.update.initialize(data);
+ } else {
+ setLoadError('Data tidak ditemukan');
}
+ } catch (error) {
+ console.error('Error loading lambang:', error);
+ setLoadError('Gagal memuat data lambang desa');
+ toast.error('Gagal memuat data lambang desa');
+ } finally {
+ setIsLoading(false);
+ }
};
- const handleBack = () => router.back();
+ loadData();
- // Loading state
- if (lambangState.findUnique.loading || lambangState.update.loading) {
- return (
-
-
- Memuat data...
-
-
- );
+ return () => {
+ stateProfileDesa.lambangDesa.update.reset();
+ stateProfileDesa.lambangDesa.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ // 🔁 Reset form
+ const handleResetForm = () => {
+ setFormData(originalData);
+ toast.info('Form dikembalikan ke data awal');
+ };
+
+ // 💾 Submit handler
+ const handleSubmit = async () => {
+ if (!formData.judul.trim()) {
+ toast.error('Judul wajib diisi');
+ return;
}
- // Error state
- if (lambangState.findUnique.error) {
- return (
-
-
-
-
-
- } color="red">
- Error
- {lambangState.findUnique.error}
-
-
-
- );
- }
+ setIsSubmitting(true);
+ try {
+ const state = stateProfileDesa.lambangDesa;
+ state.update.form.judul = formData.judul;
+ state.update.form.deskripsi = formData.deskripsi;
+ const success = await state.update.submit();
+
+ if (success) {
+ toast.success('Data berhasil disimpan');
+ router.push('/admin/desa/profile/profile-desa');
+ } else {
+ toast.error('Gagal menyimpan data');
+ }
+ } catch (error) {
+ console.error('Error update lambang desa:', error);
+ toast.error('Terjadi kesalahan saat update lambang desa');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // 📝 Handlers
+ const handleJudulChange = (e: React.ChangeEvent) => {
+ setFormData(prev => ({ ...prev, judul: e.target.value }));
+ };
+
+ const handleDeskripsiChange = (html: string) => {
+ setFormData(prev => ({ ...prev, deskripsi: html }));
+ };
+
+ const handleBack = () => router.back();
+
+ // 🔄 Loading
+ if (isLoading) {
return (
-
-
-
-
-
-
-
-
- Edit Lambang Desa
-
-
-
-
- Edit Lambang Desa
-
- {/* Judul */}
- Judul}
- placeholder="Judul lambang"
- value={lambangState.update.form.judul}
- onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
- error={!lambangState.update.form.judul && "Judul wajib diisi"}
- />
-
- {/* Deskripsi */}
-
- Deskripsi
- lambangState.update.form.deskripsi = val}
- />
-
-
- {/* Buttons */}
-
-
- {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
-
-
-
- Batal
-
-
-
-
-
-
+
+
+
+
+
+ Memuat data lambang desa...
+
+
+
+
);
+ }
+
+ // ❌ Error
+ if (loadError) {
+ return (
+
+
+
+
+
+ } color="red" title="Terjadi Kesalahan" radius="md">
+ {loadError}
+
+ router.push('/admin/desa/profile/profile-desa')} variant="outline">
+ Kembali ke Halaman Utama
+
+
+
+ );
+ }
+
+ // 🧱 UI utama
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ Edit Lambang Desa
+
+
+
+ {/* Form */}
+
+
+ {/* Judul */}
+ Judul}
+ placeholder="Masukkan judul lambang desa"
+ value={formData.judul}
+ onChange={handleJudulChange}
+ error={!formData.judul.trim() && 'Judul wajib diisi'}
+ required
+ size="md"
+ radius="md"
+ />
+
+ {/* Deskripsi */}
+
+
+ Deskripsi
+
+
+
+
+ {/* Tombol Aksi */}
+
+
+ Batal
+
+
+ Simpan
+
+
+
+
+
+
+ );
}
export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx
index 8926e3bf..a33601da 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx
@@ -5,9 +5,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
-import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } from '@mantine/core';
+import { Alert, Box, Button, Center, Group, Image, Loader, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
-import { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } from '@tabler/icons-react';
+import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -18,7 +18,9 @@ function Page() {
const router = useRouter();
const params = useParams();
- const [images, setImages] = useState>([]);
+ const [images, setImages] = useState<
+ Array<{ file: File | null; preview: string; label: string; imageId?: string }>
+ >([]);
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
@@ -26,6 +28,12 @@ function Page() {
});
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [originalData, setOriginalData] = useState({
+ judul: "",
+ deskripsi: "",
+ images: [] as Array<{ label: string; imageId: string }>
+ });
+
// Load data
useEffect(() => {
const loadData = async () => {
@@ -50,11 +58,23 @@ function Page() {
})),
});
+ setOriginalData({
+ judul: data.judul || '',
+ deskripsi: data.deskripsi || '',
+ images: (data.images || []).map((img: any) => ({
+ label: img.label,
+ imageId: img.image?.id ?? '',
+ preview: img.image?.link ?? '',
+ })),
+ });
+
+
if (data?.images?.length > 0 && data.images[0].image?.link) {
setImages(data.images.map((img: any) => ({
file: null,
preview: img.image.link,
label: img.label,
+ imageId: img.image.id, // simpan id lama
})));
}
}
@@ -74,40 +94,60 @@ function Page() {
const handleBack = () => router.back();
+ const handleResetForm = () => {
+ setFormData({
+ judul: originalData.judul,
+ deskripsi: originalData.deskripsi,
+ images: originalData.images.map((img) => ({
+ label: img.label,
+ imageId: img.imageId,
+ })),
+ });
+
+ setImages(
+ originalData.images.map((img: any) => ({
+ file: null,
+ preview: img.preview, // pakai preview masing-masing, bukan cuma satu
+ label: img.label,
+ imageId: img.imageId,
+ }))
+ );
+
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+
const handleSubmit = async () => {
if (isSubmitting || !formData.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
-
- setIsSubmitting(true);
-
try {
+ setIsSubmitting(true);
const uploadedImages = [];
// Upload semua gambar baru
for (const img of images) {
if (!img.file) {
- // Kalau gambar lama, skip upload
- if (!img.preview) continue;
- uploadedImages.push({ imageId: '', label: img.label });
+ if (!img.imageId) continue; // kalau benar2 kosong, skip
+ uploadedImages.push({ imageId: img.imageId, label: img.label });
continue;
}
+ // upload baru
const res = await ApiFetch.api.fileStorage.create.post({
file: img.file,
name: img.file.name,
});
-
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload salah satu gambar");
return;
}
-
- uploadedImages.push({ imageId: uploaded.id, label: img.label || 'main' });
+ uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
}
+
// Update ke global state
maskotState.update.updateField("judul", formData.judul);
maskotState.update.updateField("deskripsi", formData.deskripsi);
@@ -159,11 +199,9 @@ function Page() {
-
-
-
-
-
+
+
+
Edit Maskot Desa
@@ -231,7 +269,7 @@ function Page() {
setImages(updated);
}}
>
- Hapus
+
{/* Buttons */}
-
+
+ {/* Tombol Batal */}
- {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
-
-
Batal
+
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx
index 89bd1d64..7bbd5c16 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx
@@ -1,148 +1,272 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-'use client'
+'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
-import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import {
+ Alert,
+ Box,
+ Button,
+ Center,
+ Group,
+ Loader,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title
+} from '@mantine/core';
import { IconAlertCircle, 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';
+// 🔹 Types
+interface FormData {
+ judul: string;
+ deskripsi: string;
+}
+
+// 🔹 Main Component
function Page() {
- const sejarahState = useProxy(stateProfileDesa.sejarahDesa)
- const router = useRouter()
- const params = useParams()
- const [isSubmitting, setIsSubmitting] = useState(false);
+ const router = useRouter();
+ const params = useParams();
- // Load data
+ // 🧩 Local State
+ const [formData, setFormData] = useState({
+ judul: '',
+ deskripsi: '',
+ });
+ const [originalData, setOriginalData] = useState({
+ judul: '',
+ deskripsi: '',
+ });
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [loadError, setLoadError] = useState(null);
+
+ // 🧭 Load Initial Data
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
- toast.error("ID tidak valid");
- router.push("/admin/desa/profile/profile-desa");
+ toast.error('ID tidak valid');
+ router.push('/admin/desa/profile/profile-desa');
return;
}
+ setIsLoading(true);
+ setLoadError(null);
+
try {
- const data = await sejarahState.findUnique.load(id);
+ const data = await stateProfileDesa.sejarahDesa.findUnique.load(id);
+
if (data) {
- sejarahState.update.initialize(data);
+ const initialData: FormData = {
+ judul: data.judul || '',
+ deskripsi: data.deskripsi || '',
+ };
+
+ setFormData(initialData);
+ setOriginalData(initialData);
+
+ stateProfileDesa.sejarahDesa.update.initialize(data);
+ } else {
+ setLoadError('Data tidak ditemukan');
}
} catch (error) {
- console.error("Error loading sejarah:", error);
- toast.error("Gagal memuat data sejarah desa");
+ console.error('Error loading sejarah:', error);
+ setLoadError('Gagal memuat data sejarah desa');
+ toast.error('Gagal memuat data sejarah desa');
+ } finally {
+ setIsLoading(false);
}
};
loadData();
return () => {
- sejarahState.update.reset();
- sejarahState.findUnique.reset();
+ stateProfileDesa.sejarahDesa.update.reset();
+ stateProfileDesa.sejarahDesa.findUnique.reset();
};
}, [params?.id, router]);
+ // 🔄 Check if form has changes
+
+
+ // 🔁 Reset Form to Original Data
+ const handleResetForm = () => {
+ setFormData(originalData);
+ toast.info('Form dikembalikan ke data awal');
+ };
+
+ // 💾 Submit Handler
const handleSubmit = async () => {
- if (isSubmitting || !sejarahState.update.form.judul.trim()) {
- toast.error("Judul wajib diisi");
+ // Validation
+ if (!formData.judul.trim()) {
+ toast.error('Judul wajib diisi');
return;
}
setIsSubmitting(true);
+
try {
- const success = await sejarahState.update.submit();
+ // Access original state directly (not proxy)
+ const originalState = stateProfileDesa.sejarahDesa;
+
+ // Update form data
+ originalState.update.form.judul = formData.judul;
+ originalState.update.form.deskripsi = formData.deskripsi;
+
+ // Submit
+ const success = await originalState.update.submit();
+
if (success) {
- toast.success("Data berhasil disimpan");
- router.push("/admin/desa/profile/profile-desa");
+ toast.success('Data berhasil disimpan');
+ router.push('/admin/desa/profile/profile-desa');
+ } else {
+ toast.error('Gagal menyimpan data');
}
} catch (error) {
- console.error("Error update sejarah desa:", error);
- toast.error("Terjadi kesalahan saat update sejarah desa");
+ console.error('Error update sejarah desa:', error);
+ toast.error('Terjadi kesalahan saat update sejarah desa');
} finally {
setIsSubmitting(false);
}
};
- const handleBack = () => router.back();
+ // 📝 Form Field Handlers
+ const handleJudulChange = (e: React.ChangeEvent) => {
+ setFormData(prev => ({ ...prev, judul: e.target.value }));
+ };
- // Loading state
- if (sejarahState.findUnique.loading || sejarahState.update.loading) {
+ const handleDeskripsiChange = (html: string) => {
+ setFormData(prev => ({ ...prev, deskripsi: html }));
+ };
+
+ const handleBack = () => {
+ router.back();
+ };
+ // 🔄 Loading State
+ if (isLoading) {
return (
- Memuat data...
+
+
+
+ Memuat data...
+
+
);
}
- // Error state
- if (sejarahState.findUnique.error) {
+ // ❌ Error State
+ if (loadError) {
return (
-
+
-
-
+ router.back()} p="xs" radius="md">
+
- } color="red">
- Error
- {sejarahState.findUnique.error}
+ }
+ color="red"
+ title="Terjadi Kesalahan"
+ radius="md"
+ >
+ {loadError}
+ router.push('/admin/desa/profile/profile-desa')}
+ variant="outline"
+ >
+ Kembali ke Halaman Utama
+
);
}
return (
-
-
-
-
-
-
-
-
- Edit Sejarah Desa
+
+
+ {/* Header */}
+
+
+
+
+
+ Edit Sejarah Desa
+
-
-
- Edit Sejarah Desa
-
- {/* Judul */}
+ {/* Form */}
+
+
+ {/* Form Title */}
+
+
+ Informasi Sejarah Desa
+
+
+ {/* Judul Field */}
Judul}
- placeholder="Judul sejarah"
- value={sejarahState.update.form.judul}
- onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
- error={!sejarahState.update.form.judul && "Judul wajib diisi"}
+ label={Judul Sejarah }
+ placeholder="Masukkan judul sejarah desa"
+ value={formData.judul}
+ onChange={handleJudulChange}
+ error={!formData.judul.trim() && 'Judul wajib diisi'}
+ required
+ size="md"
+ radius="md"
/>
- {/* Deskripsi */}
+ {/* Deskripsi Field */}
- Deskripsi
+
+ Deskripsi Sejarah
+
sejarahState.update.form.deskripsi = val}
+ value={formData.deskripsi}
+ onChange={handleDeskripsiChange}
/>
+
- {/* Buttons */}
-
+ {/* Action Buttons */}
+
+ {/* Tombol Batal */}
- {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
+ Batal
-
- Batal
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
@@ -152,4 +276,4 @@ function Page() {
);
}
-export default Page;
+export default Page;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx
index 0a06bc88..938172b1 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx
@@ -1,155 +1,247 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-'use client'
+'use client';
+
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
-import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
+import {
+ Alert,
+ Box,
+ Button,
+ Center,
+ Group,
+ Loader,
+ Paper,
+ Stack,
+ Text,
+ Title,
+} from '@mantine/core';
import { IconAlertCircle, 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';
+// 🔹 Types
+interface FormData {
+ visi: string;
+ misi: string;
+}
+
+// 🔹 Main Component
function Page() {
- const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa)
- const router = useRouter()
- const params = useParams()
- const [isSubmitting, setIsSubmitting] = useState(false);
+ const router = useRouter();
+ const params = useParams();
+ const [formData, setFormData] = useState({ visi: '', misi: '' });
+ const [originalData, setOriginalData] = useState({ visi: '', misi: '' });
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [loadError, setLoadError] = useState(null);
- // Load data
- useEffect(() => {
- const loadData = async () => {
- const id = params?.id as string;
- if (!id) {
- toast.error("ID tidak valid");
- router.push("/admin/desa/profile/profile-desa");
- return;
- }
+ // 🧭 Load Data
+ useEffect(() => {
+ const loadData = async () => {
+ const id = params?.id as string;
+ if (!id) {
+ toast.error('ID tidak valid');
+ router.push('/admin/desa/profile/profile-desa');
+ return;
+ }
- try {
- const data = await visiMisiState.findUnique.load(id);
- visiMisiState.update.initialize(data);
- } catch (error) {
- console.error("Error loading visi misi:", error);
- toast.error("Gagal memuat data visi misi desa");
- }
- };
+ setIsLoading(true);
+ setLoadError(null);
- loadData();
+ try {
+ const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id);
+ if (data) {
+ const initialData: FormData = {
+ visi: data.visi || '',
+ misi: data.misi || '',
+ };
+ setFormData(initialData);
+ setOriginalData(initialData);
- return () => {
- visiMisiState.update.reset();
- visiMisiState.findUnique.reset();
- };
- }, [params?.id, router]);
-
- const handleSubmit = async () => {
- if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
- toast.error("Visi wajib diisi");
- return;
- }
-
- setIsSubmitting(true);
-
- try {
- const success = await visiMisiState.update.submit();
-
- if (success) {
- toast.success("Data berhasil disimpan");
- router.push("/admin/desa/profile/profile-desa");
- }
- } catch (error) {
- console.error("Error update visi misi desa:", error);
- toast.error("Terjadi kesalahan saat update visi misi desa");
- } finally {
- setIsSubmitting(false);
+ // set id ke state agar submit pakai endpoint benar
+ stateProfileDesa.visiMisiDesa.update.initialize(data);
+ } else {
+ setLoadError('Data tidak ditemukan');
}
+ } catch (error) {
+ console.error('Error load visi misi:', error);
+ setLoadError('Gagal memuat data visi misi desa');
+ toast.error('Gagal memuat data visi misi desa');
+ } finally {
+ setIsLoading(false);
+ }
};
- const handleBack = () => router.back();
+ loadData();
- // Loading state
- if (visiMisiState.findUnique.loading || visiMisiState.update.loading) {
- return (
-
-
- Memuat data...
-
-
- );
+ return () => {
+ stateProfileDesa.visiMisiDesa.update.reset();
+ stateProfileDesa.visiMisiDesa.findUnique.reset();
+ };
+ }, [params?.id, router]);
+
+ // 🔄 Reset Form
+ const handleResetForm = () => {
+ setFormData(originalData);
+ toast.info('Form dikembalikan ke data awal');
+ };
+
+ // 💾 Submit
+ const handleSubmit = async () => {
+ if (!formData.visi.trim()) {
+ toast.error('Visi wajib diisi');
+ return;
}
- // Error state
- if (visiMisiState.findUnique.error) {
- return (
-
-
-
-
-
- } color="red">
- Error
- {visiMisiState.findUnique.error}
-
-
-
- );
- }
+ setIsSubmitting(true);
+ try {
+ const originalState = stateProfileDesa.visiMisiDesa;
+ // update data form ke state sebelum submit
+ originalState.update.form.visi = formData.visi;
+ originalState.update.form.misi = formData.misi;
+
+ const success = await originalState.update.submit();
+
+ if (success) {
+ toast.success('Data berhasil disimpan');
+ router.push('/admin/desa/profile/profile-desa');
+ } else {
+ toast.error('Gagal menyimpan data');
+ }
+ } catch (error) {
+ console.error('Error update visi misi desa:', error);
+ toast.error('Terjadi kesalahan saat update visi misi desa');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // 🧭 Field handlers
+ const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html }));
+ const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html }));
+ const handleBack = () => router.back();
+
+ // ⏳ Loading
+ if (isLoading) {
return (
-
-
-
-
-
-
-
-
- Edit Visi Misi Desa
-
-
-
-
- Edit Visi Misi Desa
-
- {/* Visi */}
-
- Visi
- visiMisiState.update.form.visi = val}
- />
-
-
- {/* Misi */}
-
- Misi
- visiMisiState.update.form.misi = val}
- />
-
-
- {/* Buttons */}
-
-
- {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
-
-
-
- Batal
-
-
-
-
-
-
+
+
+
+
+
+ Memuat data...
+
+
+
+
);
+ }
+
+ // ❌ Error
+ if (loadError) {
+ return (
+
+
+
+
+
+ }
+ color="red"
+ title="Terjadi Kesalahan"
+ radius="md"
+ >
+ {loadError}
+
+ router.push('/admin/desa/profile/profile-desa')}
+ variant="outline"
+ >
+ Kembali ke Halaman Utama
+
+
+
+ );
+ }
+
+ // ✅ UI
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ Edit Visi & Misi Desa
+
+
+
+ {/* Form */}
+
+
+
+
+ Informasi Visi & Misi Desa
+
+
+
+ {/* Visi */}
+
+
+ Visi
+
+
+
+
+ {/* Misi */}
+
+
+ Misi
+
+
+
+
+ {/* Actions */}
+
+
+ Batal
+
+
+ Simpan
+
+
+
+
+
+
+ );
}
export default Page;
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx
index 218819e3..14a4fd5b 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx
@@ -1,12 +1,12 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
-import { useSnapshot } from 'valtio';
-import stateProfileDesa from '../../../_state/desa/profile';
-import { useEffect } from 'react';
+import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { useSnapshot } from 'valtio';
+import stateProfileDesa from '../../../_state/desa/profile';
function Page() {
const router = useRouter();
@@ -37,7 +37,6 @@ function Page() {
Preview Sejarah Desa
-
Edit
-
@@ -70,7 +68,7 @@ function Page() {
-
+
@@ -84,7 +82,6 @@ function Page() {
Preview Visi Misi Desa
-
Edit
-
@@ -118,9 +114,9 @@ function Page() {
Visi Desa
-
+
Misi Desa
-
+
@@ -134,7 +130,6 @@ function Page() {
Preview Lambang Desa
-
Edit
-
@@ -162,12 +156,12 @@ function Page() {
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
- Lambang Desa
+ {lambang.judul}
-
+
@@ -181,7 +175,6 @@ function Page() {
Preview Maskot Desa
-
Edit
-
@@ -214,9 +206,9 @@ function Page() {
-
+
-
+
{maskot.images.map((img, idx) => (
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx
index f48a8bba..d7c3b632 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx
@@ -4,6 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
@@ -13,7 +14,7 @@ import {
Text,
TextInput,
Title,
- Tooltip
+ Loader
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -26,15 +27,27 @@ function EditPerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel);
const router = useRouter();
const params = useParams();
+
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const [formData, setFormData] = useState({
- nama: state.update.form.nama || '',
- daerah: state.update.form.daerah || '',
- periode: state.update.form.periode || '',
- imageId: state.update.form.imageId || ''
+ nama: '',
+ daerah: '',
+ periode: '',
+ imageId: ''
});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [originalData, setOriginalData] = useState({
+ nama: '',
+ daerah: '',
+ periode: '',
+ imageId: '',
+ imageUrl: "",
+ });
+
+ // load data pertama kali
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
@@ -48,7 +61,14 @@ function EditPerbekelDariMasaKeMasa() {
periode: data.periode || '',
imageId: data.imageId || ''
});
- if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link);
+ setOriginalData({
+ nama: data.nama || '',
+ daerah: data.daerah || '',
+ periode: data.periode || '',
+ imageId: data.imageId || '',
+ imageUrl: data.image.link || '',
+ })
+ setPreviewImage(data.image.link);
}
} catch (error) {
console.error('Error loading foto:', error);
@@ -58,8 +78,31 @@ function EditPerbekelDariMasaKeMasa() {
loadFoto();
}, [params?.id]);
+ // helper ubah state formData
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ daerah: originalData.daerah,
+ periode: originalData.periode,
+ imageId: originalData.imageId,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // update global state hanya sekali pas submit
state.update.form = { ...state.update.form, ...formData };
if (file) {
@@ -78,17 +121,17 @@ function EditPerbekelDariMasaKeMasa() {
} catch (error) {
console.error('Error updating perbekel dari masa ke masa:', error);
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Perbekel Dari Masa Ke Masa
@@ -107,7 +150,7 @@ function EditPerbekelDariMasaKeMasa() {
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
- onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
+ onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -125,7 +168,7 @@ function EditPerbekelDariMasaKeMasa() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -144,25 +187,45 @@ function EditPerbekelDariMasaKeMasa() {
Seret gambar atau klik untuk memilih file
- Maksimal 5MB, format gambar wajib
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -171,7 +234,7 @@ function EditPerbekelDariMasaKeMasa() {
label="Daerah"
placeholder="Masukkan daerah"
value={formData.daerah}
- onChange={(e) => setFormData({ ...formData, daerah: e.target.value })}
+ onChange={(e) => handleChange('daerah', e.target.value)}
required
/>
@@ -179,11 +242,22 @@ function EditPerbekelDariMasaKeMasa() {
label="Periode"
placeholder="Masukkan periode"
value={formData.periode}
- onChange={(e) => setFormData({ ...formData, periode: e.target.value })}
+ onChange={(e) => handleChange('periode', e.target.value)}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx
index 048d8494..8a63f508 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx
@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
-import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Image, 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';
@@ -98,7 +98,6 @@ function DetailPerbekelDariMasa() {
-
{
@@ -111,9 +110,7 @@ function DetailPerbekelDariMasa() {
>
-
-
router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
@@ -123,7 +120,6 @@ function DetailPerbekelDariMasa() {
>
-
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx
index ff5f7104..4a8f6aaa 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx
@@ -2,7 +2,7 @@
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
-import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { Loader, ActionIcon, Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -14,6 +14,7 @@ function CreatePerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel);
const router = useRouter();
const [previewImage, setPreviewImage] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [file, setFile] = useState(null);
const resetForm = () => {
@@ -28,33 +29,39 @@ function CreatePerbekelDariMasaKeMasa() {
};
const handleSubmit = async () => {
- if (!file) {
- return toast.warn('Pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true);
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+
+ state.create.form.imageId = uploaded.id;
+ await state.create.create();
+ resetForm();
+ router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
+ } catch (error) {
+ console.error(error);
+ toast.error('Gagal menambahkan perbekel dari masa ke masa');
+ } finally {
+ setIsSubmitting(false);
}
-
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
-
- const uploaded = res.data?.data;
- if (!uploaded?.id) return toast.error('Gagal upload gambar');
-
- state.create.form.imageId = uploaded.id;
- await state.create.create();
- resetForm();
- router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
};
return (
{/* Back button + Title */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Create Perbekel Dari Masa Ke Masa
@@ -70,21 +77,21 @@ function CreatePerbekelDariMasaKeMasa() {
>
Nama Perbekel}
+ label="Nama Perbekel"
placeholder="Masukkan nama perbekel"
value={state.create.form.nama}
onChange={(e) => (state.create.form.nama = e.target.value)}
required
/>
Daerah}
+ label="Daerah"
placeholder="Masukkan daerah"
value={state.create.form.daerah}
onChange={(e) => (state.create.form.daerah = e.target.value)}
required
/>
Periode}
+ label="Periode"
placeholder="Masukkan periode"
value={state.create.form.periode}
onChange={(e) => (state.create.form.periode = e.target.value)}
@@ -104,7 +111,7 @@ function CreatePerbekelDariMasaKeMasa() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -120,25 +127,63 @@ function CreatePerbekelDariMasaKeMasa() {
- Seret gambar atau klik untuk memilih file (maks 5MB)
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
{/* Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx
index 01367ad9..aaaac6d3 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx
@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -49,7 +49,6 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
List Perbekel Dari Masa Ke Masa
-
}
color="blue"
@@ -58,7 +57,6 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
>
Tambah Baru
-
@@ -75,22 +73,28 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
filteredData.map((item) => (
- {item.nama}
+
+ {item.nama}
+
- {item.periode}
+
+ {item.periode}
+
- }
- onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
- >
- Detail
-
+
+ }
+ onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
+ >
+ Detail
+
+
))
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx
index 43eefc8a..1a916bfe 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx
@@ -4,9 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
-import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
-import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react';
+import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -101,11 +101,9 @@ function ProfilePerbekel() {
{/* Header */}
-
-
Edit Profil Perbekel
@@ -128,8 +126,8 @@ function ProfilePerbekel() {
handleFileChange(files[0])}
onReject={() => toast.error('File tidak valid.')}
- maxSize={5 * 1024 ** 2} // 5MB
- accept={{ 'image/*': [] }}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
>
diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx
index 1f435fea..2d2ddf4a 100644
--- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx
+++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx
@@ -1,7 +1,7 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
@@ -36,7 +36,6 @@ function Page() {
Preview Profil PPID
-
Edit
-
@@ -97,16 +95,16 @@ function Page() {
{/* Biodata & Info */}
Biodata
-
+
Pengalaman
-
+
Pengalaman Organisasi
-
+
Program Kerja Unggulan
-
+
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx
index dca2dfe2..ff6d98fc 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx
@@ -2,23 +2,22 @@
'use client'
import colors from '@/con/colors';
import {
+ ScrollArea,
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
- Title,
- Tooltip,
- ScrollArea,
+ Title
} from '@mantine/core';
-import { usePathname, useRouter } from 'next/navigation';
-import React, { useEffect, useState } from 'react';
import {
- IconFileAnalytics,
IconCoins,
+ IconFileAnalytics,
IconShoppingCart,
IconWallet,
} from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -29,29 +28,25 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
label: "APB Desa",
value: "apbdesa",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa",
- icon: ,
- tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa",
+ icon:
},
{
label: "Pendapatan",
value: "pendapatan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan",
icon: ,
- tooltip: "Kelola data pendapatan desa",
},
{
label: "Belanja",
value: "belanja",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja",
icon: ,
- tooltip: "Atur data belanja desa",
},
{
label: "Pembiayaan",
value: "pembiayaan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan",
icon: ,
- tooltip: "Kelola data pembiayaan desa",
},
];
@@ -104,26 +99,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
index 7d07fd77..121be66a 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx
@@ -1,11 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-'use client'
/* eslint-disable react-hooks/exhaustive-deps */
+'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import {
+ Alert,
Box,
Button,
+ Group,
+ Loader,
MultiSelect,
Paper,
Skeleton,
@@ -13,8 +16,6 @@ import {
Text,
TextInput,
Title,
- Tooltip,
- Group,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
@@ -23,76 +24,132 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
+// ==================== HELPERS ====================
+const safeStringArray = (arr: any[]): string[] => {
+ if (!Array.isArray(arr)) return [];
+ return arr
+ .filter(item => item != null && item !== '')
+ .map(item => String(item))
+ .filter(item => item.trim() !== '');
+};
+
+const createEmptyForm = () => ({
+ tahun: '',
+ pendapatanIds: [] as string[],
+ belanjaIds: [] as string[],
+ pembiayaanIds: [] as string[],
+});
+
+// ==================== COMPONENT ====================
function EditAPBDesa() {
const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter();
const params = useParams();
- const [formData, setFormData] = useState({
- tahun: apbState.update.form.tahun || '',
- pendapatanIds: apbState.update.form.pendapatanIds || [],
- belanjaIds: apbState.update.form.belanjaIds || [],
- pembiayaanIds: apbState.update.form.pembiayaanIds || [],
- });
+ const [formData, setFormData] = useState(createEmptyForm());
+ const [originalData, setOriginalData] = useState(createEmptyForm());
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
- // Load APB desa by id
+ // ==================== LOAD DATA ====================
useEffect(() => {
const loadAPBdesa = async () => {
const id = params?.id as string;
- if (!id) return;
+ if (!id) {
+ toast.error('ID tidak valid');
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
+ return;
+ }
try {
+ setIsLoading(true);
const data = await apbState.update.load(id);
- if (data) {
- setFormData({
- tahun: data.tahun || 0,
- pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
- belanjaIds: data.belanja?.map((b: any) => b.id) || [],
- pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
- });
+
+ if (!data) {
+ toast.error('Data APB Desa tidak ditemukan');
+ return;
}
- } catch (error) {
- console.error("Error loading APBdesa:", error);
- toast.error("Gagal memuat data APBdesa");
+
+ const normalized = {
+ tahun: String(data.tahun || ''),
+ pendapatanIds: safeStringArray(data.pendapatan?.map((p: any) => p.id) || []),
+ belanjaIds: safeStringArray(data.belanja?.map((b: any) => b.id) || []),
+ pembiayaanIds: safeStringArray(data.pembiayaan?.map((p: any) => p.id) || []),
+ };
+
+ setFormData(normalized);
+ setOriginalData(normalized);
+ } catch (err) {
+ console.error('Error loading APBdesa:', err);
+ toast.error('Gagal memuat data APB Desa');
+ } finally {
+ setIsLoading(false);
}
};
loadAPBdesa();
}, [params?.id]);
+ // ==================== HANDLERS ====================
+ const handleChange = (field: keyof typeof formData, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleResetForm = () => {
+ setFormData(originalData);
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+
+ if (!formData.tahun.trim()) {
+ toast.warning('Tahun harus diisi');
+ return;
+ }
+
apbState.update.form = {
...apbState.update.form,
tahun: Number(formData.tahun),
- pendapatanIds: formData.pendapatanIds,
- belanjaIds: formData.belanjaIds,
- pembiayaanIds: formData.pembiayaanIds,
+ pendapatanIds: safeStringArray(formData.pendapatanIds),
+ belanjaIds: safeStringArray(formData.belanjaIds),
+ pembiayaanIds: safeStringArray(formData.pembiayaanIds),
};
await apbState.update.update();
- toast.success("APB Desa berhasil diperbarui!");
- router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa");
+ toast.success('APB Desa berhasil diperbarui!');
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
} catch (error) {
- console.error("Error updating APBdesa:", error);
- toast.error("Terjadi kesalahan saat memperbarui APBdesa");
+ console.error('Error updating APBdesa:', error);
+ toast.error('Terjadi kesalahan saat memperbarui APB Desa');
+ } finally {
+ setIsSubmitting(false);
}
};
+ // ==================== UI ====================
+ if (isLoading) {
+ return (
+
+
+ Memuat data APB Desa...
+
+ );
+ }
+
return (
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit APB Desa
@@ -112,38 +169,46 @@ function EditAPBDesa() {
- setFormData({ ...formData, tahun: e.target.value })
- }
+ onChange={(e) => handleChange('tahun', e.target.value)}
label={Tahun }
placeholder="Masukkan tahun anggaran"
required
/>
{/* Selects */}
-
- setFormData({ ...formData, pendapatanIds: ids })
- }
+ onSelectionChange={(ids) => handleChange('pendapatanIds', ids)}
/>
-
- setFormData({ ...formData, belanjaIds: ids })
- }
+ onSelectionChange={(ids) => handleChange('belanjaIds', ids)}
/>
-
- setFormData({ ...formData, pembiayaanIds: ids })
- }
+ onSelectionChange={(ids) => handleChange('pembiayaanIds', ids)}
/>
{/* Save Button */}
-
+
+
+ Batal
+
+
- Simpan
+ {isSubmitting ? : 'Simpan'}
);
+}
- /* --- Sub Components --- */
+// ==================== SUB COMPONENT ====================
+function SelectAPBItem({
+ label,
+ state,
+ selectedIds,
+ onSelectionChange,
+}: {
+ label: string;
+ state: any;
+ selectedIds: string[];
+ onSelectionChange: (ids: string[]) => void;
+}) {
+ const proxyState = useProxy(state);
- function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
- const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
+ useShallowEffect(() => {
+ proxyState.findMany.load();
+ }, []);
- useShallowEffect(() => {
- pendapatanState.findMany.load();
- }, []);
+ const data = proxyState.findMany.data;
+ const isLoading = !data;
- if (!pendapatanState.findMany.data) {
- return ;
- }
+ const options =
+ data
+ ?.filter((item: any) => item?.id)
+ .map((item: any) => ({
+ value: String(item.id),
+ label: String(item?.name || '(Tanpa Nama)'),
+ })) || [];
+ if (isLoading) {
return (
- Pendapatan}
- data={pendapatanState.findMany.data.map((p: any) => ({
- value: p.id,
- label: p.name,
- }))}
- value={selectedIds}
- onChange={onSelectionChange}
- searchable
- clearable
- placeholder="Pilih pendapatan..."
- nothingFoundMessage="Tidak ditemukan"
- />
+
+ {label}
+
+
);
}
- function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
- const belanjaState = useProxy(PendapatanAsliDesa.belanja);
-
- useShallowEffect(() => {
- belanjaState.findMany.load();
- }, []);
-
- if (!belanjaState.findMany.data) {
- return ;
- }
-
+ if (options.length === 0) {
return (
- Belanja}
- data={belanjaState.findMany.data.map((b: any) => ({
- value: b.id,
- label: b.name,
- }))}
- value={selectedIds}
- onChange={onSelectionChange}
- searchable
- clearable
- placeholder="Pilih belanja..."
- nothingFoundMessage="Tidak ditemukan"
- />
+
+
+ Tidak ada data {label.toLowerCase()} tersedia.
+
+
);
}
- function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
- const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
-
- useShallowEffect(() => {
- pembiayaanState.findMany.load();
- }, []);
-
- if (!pembiayaanState.findMany.data) {
- return ;
- }
-
- return (
- Pembiayaan}
- data={pembiayaanState.findMany.data.map((p: any) => ({
- value: p.id,
- label: p.name,
- }))}
- value={selectedIds}
- onChange={onSelectionChange}
- searchable
- clearable
- placeholder="Pilih pembiayaan..."
- nothingFoundMessage="Tidak ditemukan"
- />
- );
- }
+ return (
+ {label}}
+ data={options}
+ value={selectedIds}
+ onChange={(ids) => onSelectionChange(safeStringArray(ids))}
+ searchable
+ clearable
+ placeholder={`Pilih ${label.toLowerCase()}...`}
+ nothingFoundMessage="Tidak ditemukan"
+ />
+ );
}
export default EditAPBDesa;
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx
index ee57dc50..67bc460f 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx
@@ -9,8 +9,7 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -81,7 +80,7 @@ function DetailAPBDesa() {
Detail APB Desa
-
+
@@ -159,36 +158,32 @@ function DetailAPBDesa() {
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
-
- router.push(
- `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${data.id}/edit`
- )
- }
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
index 174d0c9a..ad347bc2 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx
@@ -6,23 +6,26 @@ import {
Box,
Button,
Group,
+ Loader,
MultiSelect,
Paper,
Skeleton,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateAPBDesa() {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
apbDesaState.create.form = {
@@ -34,20 +37,26 @@ function CreateAPBDesa() {
};
const handleSubmit = async () => {
- await apbDesaState.create.submit();
- resetForm();
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
+ try {
+ setIsSubmitting(true);
+ await apbDesaState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
+ } catch (error) {
+ console.error('Error creating APB Desa:', error);
+ toast.error('Gagal membuat APB Desa');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah APB Desa
@@ -97,6 +106,17 @@ function CreateAPBDesa() {
{/* Action */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx
index ff0c6464..e87b21f1 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx
@@ -5,8 +5,8 @@ import {
Button,
Center,
Group,
- Paper,
Pagination,
+ Paper,
Skeleton,
Stack,
Table,
@@ -15,8 +15,7 @@ import {
TableTh,
TableThead,
TableTr,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -82,20 +81,18 @@ function ListAPBDesa({ search }: { search: string }) {
List APB Desa
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create"
- )
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create"
+ )
+ }
+ >
+ Tambah Baru
+
@@ -138,20 +135,18 @@ function ListAPBDesa({ search }: { search: string }) {
)}
-
-
- router.push(
- `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
- )
- }
- >
-
- Detail
-
-
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
+ )
+ }
+ >
+
+ Detail
+
))
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
index ea3729d2..533a12db 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx
@@ -1,16 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
+
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -22,14 +23,24 @@ function EditBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: belanjaState.update.form.name || '',
- value: belanjaState.update.form.value || '',
+ name: '',
+ value: '',
});
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ value: '',
+ });
+
+ // format angka ke rupiah
const formatRupiah = (value: number | string) => {
- const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
+ const number =
+ typeof value === 'number'
+ ? value
+ : Number(value.replace(/\D/g, '')) || 0;
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -37,8 +48,9 @@ function EditBelanja() {
}).format(number);
};
+ // buang semua simbol jadi angka murni
const unformatRupiah = (value: string) => {
- return Number(value.replace(/\D/g, ''));
+ return Number(value.replace(/\D/g, '')) || 0;
};
useEffect(() => {
@@ -51,7 +63,11 @@ function EditBelanja() {
if (data) {
setFormData({
name: data.name || '',
- value: data.value || '',
+ value: String(data.value || ''),
+ });
+ setOriginalData({
+ name: data.name || '',
+ value: String(data.value || ''),
});
}
} catch (error) {
@@ -65,11 +81,12 @@ function EditBelanja() {
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
belanjaState.update.form = {
...belanjaState.update.form,
name: formData.name,
value: Number(formData.value),
- }
+ };
await belanjaState.update.update();
toast.success("Jenis Belanja berhasil diperbarui!");
@@ -77,23 +94,31 @@ function EditBelanja() {
} catch (error) {
console.error("Error updating jenis belanja:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis belanja");
+ } finally {
+ setIsSubmitting(false);
}
- }
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ value: originalData.value,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
return (
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Jenis Belanja
@@ -113,23 +138,36 @@ function EditBelanja() {
label="Nama Jenis Belanja"
placeholder="Masukkan nama jenis belanja"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) =>
+ setFormData({ ...formData, name: e.target.value })
+ }
required
/>
{
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
- setFormData({ ...formData, value: cleanValue });
+ setFormData({ ...formData, value: String(cleanValue) });
}}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
index 76bc332c..fee6e242 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx
@@ -6,21 +6,23 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useProxy } from 'valtio/utils';
+import { useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
function CreateBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const formatRupiah = (value: number | string) => {
const number =
@@ -48,25 +50,31 @@ function CreateBelanja() {
return toast.warn('Lengkapi semua field terlebih dahulu');
}
- await belanjaState.create.submit();
- resetForm();
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
+ try {
+ setIsSubmitting(true);
+ await belanjaState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
+ } catch (error) {
+ console.error('Error creating belanja:', error);
+ toast.error('Gagal menambahkan jenis belanja');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan back button */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Jenis Belanja
@@ -103,6 +111,17 @@ function CreateBelanja() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx
index 467c8142..1a09d875 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx
@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -96,18 +95,16 @@ function ListBelanja({ search }: { search: string }) {
Daftar Belanja
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
+ }
+ >
+ Tambah Baru
+
@@ -138,34 +135,30 @@ function ListBelanja({ search }: { search: string }) {
-
-
- router.push(
- `/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
- )
- }
- >
-
-
-
-
- {
- setSelectedId(item.id);
- setModalHapus(true);
- }}
- >
-
-
-
+
+ router.push(
+ `/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
+ )
+ }
+ >
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
index 0492b8ee..c7e642dd 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx
@@ -6,11 +6,11 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -22,14 +22,23 @@ function EditPembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: pembiayaanState.update.form.name || '',
- value: pembiayaanState.update.form.value || '',
+ name: '',
+ value: '',
+ });
+
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ value: '',
});
const formatRupiah = (value: number | string) => {
- const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
+ const number =
+ typeof value === 'number'
+ ? value
+ : Number(value.toString().replace(/\D/g, '')) || 0;
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -38,7 +47,7 @@ function EditPembiayaan() {
};
const unformatRupiah = (value: string) => {
- return Number(value.replace(/\D/g, ''));
+ return Number(value.replace(/\D/g, '')) || 0;
};
useEffect(() => {
@@ -51,7 +60,11 @@ function EditPembiayaan() {
if (data) {
setFormData({
name: data.name || '',
- value: data.value || '',
+ value: String(data.value || ''),
+ });
+ setOriginalData({
+ name: data.name || '',
+ value: String(data.value || ''),
});
}
} catch (error) {
@@ -63,12 +76,21 @@ function EditPembiayaan() {
loadPembiayaan();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ value: originalData.value,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
pembiayaanState.update.form = {
...pembiayaanState.update.form,
name: formData.name,
- value: Number(formData.value),
+ value: unformatRupiah(formData.value),
};
await pembiayaanState.update.update();
@@ -77,23 +99,23 @@ function EditPembiayaan() {
} catch (error) {
console.error('Error updating jenis pembiayaan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
- {/* Header dengan Back Button */}
+ {/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Jenis Pembiayaan
@@ -113,23 +135,36 @@ function EditPembiayaan() {
label="Nama Jenis Pembiayaan"
placeholder="Masukkan nama jenis pembiayaan"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) =>
+ setFormData((prev) => ({ ...prev, name: e.target.value }))
+ }
required
/>
{
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
- setFormData({ ...formData, value: cleanValue });
+ setFormData((prev) => ({ ...prev, value: String(cleanValue) }));
}}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
index ff6e17ff..6b808b45 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx
@@ -1,26 +1,27 @@
'use client';
-import React from 'react';
-import { useProxy } from 'valtio/utils';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
-import { useRouter } from 'next/navigation';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
- Title,
- TextInput,
Text,
- Tooltip,
+ TextInput,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
function CreatePembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const formatRupiah = (value: number | string) => {
const number =
@@ -44,29 +45,35 @@ function CreatePembiayaan() {
};
const handleSubmit = async () => {
- if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
- return toast.warn('Nama dan nilai wajib diisi');
- }
+ try {
+ setIsSubmitting(true);
+ if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
+ return toast.warn('Nama dan nilai wajib diisi');
+ }
- await pembiayaanState.create.submit();
- resetForm();
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
+ await pembiayaanState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
+ } catch (error) {
+ console.error('Error creating pembiayaan:', error);
+ toast.error('Gagal menambahkan jenis pembiayaan');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Jenis Pembiayaan
@@ -105,6 +112,17 @@ function CreatePembiayaan() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx
index 627f5894..f9c265a6 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx
@@ -1,9 +1,11 @@
'use client'
+import colors from '@/con/colors';
import {
Box,
Button,
Center,
Group,
+ Pagination,
Paper,
Skeleton,
Stack,
@@ -14,19 +16,16 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
- Pagination,
+ Title
} from '@mantine/core';
-import React, { useState } from 'react';
-import HeaderSearch from '../../../_com/header';
-import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
-import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
-import { useProxy } from 'valtio/utils';
-import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
-import colors from '@/con/colors';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function Pembiayaan() {
const [search, setSearch] = useState("");
@@ -95,18 +94,16 @@ function ListPembiayaan({ search }: { search: string }) {
Daftar Pembiayaan
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create')
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
index 1970c267..0ae681e0 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx
@@ -6,11 +6,11 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -22,14 +22,24 @@ function EditPendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
- const [formData, setFormData] = useState({
- name: pendapatanState.update.form.name || '',
- value: pendapatanState.update.form.value || '',
+ const [originalData, setOriginalData] = useState({
+ name: "",
+ value: "",
});
+ const [formData, setFormData] = useState({
+ name: '',
+ value: '',
+ });
+
+ // helper format
const formatRupiah = (value: number | string) => {
- const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
+ const number = typeof value === 'number'
+ ? value
+ : Number(value.toString().replace(/\D/g, ''));
+
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -39,6 +49,7 @@ function EditPendapatan() {
const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
+ // load data once
useEffect(() => {
const id = params?.id as string;
if (!id) return;
@@ -48,8 +59,12 @@ function EditPendapatan() {
const data = await pendapatanState.update.load(id);
if (data) {
setFormData({
- name: data.name || '',
- value: data.value || '',
+ name: data.name ?? '',
+ value: data.value?.toString() ?? '',
+ });
+ setOriginalData({
+ name: data.name ?? '',
+ value: data.value?.toString() ?? '',
});
}
} catch (error) {
@@ -61,8 +76,25 @@ function EditPendapatan() {
loadPendapatan();
}, [params?.id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ value: originalData.value,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
pendapatanState.update.form = {
...pendapatanState.update.form,
name: formData.name,
@@ -75,23 +107,24 @@ function EditPendapatan() {
} catch (error) {
console.error('Error updating jenis pendapatan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan');
+ } finally {
+ setIsSubmitting(false);
}
+
};
return (
{/* Header with Back Button */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Jenis Pendapatan
@@ -111,23 +144,34 @@ function EditPendapatan() {
label="Nama Jenis Pendapatan"
placeholder="Masukkan nama jenis pendapatan"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange('name', e.target.value)}
required
/>
{
const raw = e.currentTarget.value;
- const cleanValue = unformatRupiah(raw);
- setFormData({ ...formData, value: cleanValue });
+ const cleanValue = unformatRupiah(raw).toString();
+ handleChange('value', cleanValue);
}}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
index 9ff6882f..75fc530f 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx
@@ -5,20 +5,22 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
- Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
@@ -41,25 +43,31 @@ function CreatePendapatan() {
};
const handleSubmit = async () => {
- await pendapatanState.create.submit();
- resetForm();
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
+ try {
+ setIsSubmitting(true);
+ await pendapatanState.create.submit();
+ resetForm();
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
+ } catch (error) {
+ console.error('Error creating pendapatan:', error);
+ toast.error('Gagal menambahkan jenis pendapatan');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan tombol back + judul */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Jenis Pendapatan
@@ -80,11 +88,7 @@ function CreatePendapatan() {
onChange={(val) => {
pendapatanState.create.form.name = val.target.value;
}}
- label={
-
- Nama Jenis Pendapatan
-
- }
+ label="Nama Jenis Pendapatan"
placeholder="Masukkan nama jenis pendapatan"
required
/>
@@ -97,16 +101,23 @@ function CreatePendapatan() {
const cleanValue = unformatRupiah(raw);
pendapatanState.create.form.value = cleanValue;
}}
- label={
-
- Nilai
-
- }
+ label="Nilai"
placeholder="Masukkan nilai"
required
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx
index 47ba7b86..9c0fe175 100644
--- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx
@@ -17,8 +17,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -96,18 +95,16 @@ function ListPendapatan({ search }: { search: string }) {
Daftar Pendapatan
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create')
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
index 20e7e339..31f4eb71 100644
--- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx
@@ -6,56 +6,114 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
-function EditDemografiPekerjaan() {
+interface FormData {
+ pekerjaan: string;
+ lakiLaki: number;
+ perempuan: number;
+}
+
+export default function EditDemografiPekerjaan() {
const router = useRouter();
- const params = useParams() as { id: string };
+ const { id } = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [formData, setFormData] = useState({
+ pekerjaan: '',
+ lakiLaki: 0,
+ perempuan: 0,
+ });
+ const [originalData, setOriginalData] = useState({
+ pekerjaan: '',
+ lakiLaki: 0,
+ perempuan: 0,
+ });
- const id = params.id;
-
+ // ✅ Load data hanya sekali di awal (tidak reset form)
useEffect(() => {
if (!id) return;
- stateDemografi.update.id = id;
- stateDemografi.findUnique
- .load(id)
- .then(() => {
+
+ const loadData = async () => {
+ try {
+ setIsSubmitting(true);
+ stateDemografi.update.id = id;
+ await stateDemografi.findUnique.load(id);
+
const data = stateDemografi.findUnique.data;
if (data) {
- stateDemografi.update.form = {
- pekerjaan: String(data.pekerjaan || ''),
- lakiLaki: Number(data.lakiLaki || 0),
- perempuan: Number(data.perempuan || 0),
- };
+ setFormData({
+ pekerjaan: data.pekerjaan ?? '',
+ lakiLaki: Number(data.lakiLaki ?? 0),
+ perempuan: Number(data.perempuan ?? 0),
+ });
+ setOriginalData({
+ pekerjaan: data.pekerjaan ?? '',
+ lakiLaki: Number(data.lakiLaki ?? 0),
+ perempuan: Number(data.perempuan ?? 0),
+ });
}
- })
- .catch((error) => {
+ } catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
- });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ loadData();
}, [id]);
+ // ✅ Handler input terkontrol (tidak buat re-render berlebihan)
+ const handleChange = useCallback(
+ (field: keyof FormData) =>
+ (e: React.ChangeEvent) => {
+ const value =
+ field === 'lakiLaki' || field === 'perempuan'
+ ? Number(e.currentTarget.value)
+ : e.currentTarget.value;
+
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ const handleResetForm = () => {
+ setFormData({
+ pekerjaan: originalData.pekerjaan,
+ lakiLaki: Number(originalData.lakiLaki),
+ perempuan: Number(originalData.perempuan),
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ // ✅ Submit hanya update global state sekali
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
stateDemografi.update.id = id;
+ stateDemografi.update.form = { ...formData };
+
await stateDemografi.update.submit();
+
toast.success('Data berhasil diperbarui');
router.push('/admin/ekonomi/demografi-pekerjaan');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -63,16 +121,14 @@ function EditDemografiPekerjaan() {
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Demografi Pekerjaan
@@ -91,10 +147,8 @@ function EditDemografiPekerjaan() {
- (stateDemografi.update.form.pekerjaan = e.currentTarget.value)
- }
+ value={formData.pekerjaan}
+ onChange={handleChange('pekerjaan')}
required
/>
@@ -102,12 +156,8 @@ function EditDemografiPekerjaan() {
label="Jumlah Pekerja Laki-laki"
type="number"
placeholder="Masukkan jumlah pekerja laki-laki"
- value={stateDemografi.update.form.lakiLaki}
- onChange={(e) =>
- (stateDemografi.update.form.lakiLaki = Number(
- e.currentTarget.value
- ))
- }
+ value={formData.lakiLaki}
+ onChange={handleChange('lakiLaki')}
required
/>
@@ -115,16 +165,23 @@ function EditDemografiPekerjaan() {
label="Jumlah Pekerja Perempuan"
type="number"
placeholder="Masukkan jumlah pekerja perempuan"
- value={stateDemografi.update.form.perempuan}
- onChange={(e) =>
- (stateDemografi.update.form.perempuan = Number(
- e.currentTarget.value
- ))
- }
+ value={formData.perempuan}
+ onChange={handleChange('perempuan')}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
@@ -143,5 +200,3 @@ function EditDemografiPekerjaan() {
);
}
-
-export default EditDemografiPekerjaan;
diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
index f7ef972e..4d280abf 100644
--- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx
@@ -7,22 +7,24 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip,
+ 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 demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
+import { toast } from 'react-toastify';
function CreateDemografiPekerjaan() {
const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState([]);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateDemografi.create.form = {
@@ -33,32 +35,37 @@ function CreateDemografiPekerjaan() {
};
const handleSubmit = async () => {
- const id = await stateDemografi.create.create();
- if (id) {
- const idStr = String(id);
- await stateDemografi.findUnique.load(idStr);
- if (stateDemografi.findUnique.data) {
- setChartData([stateDemografi.findUnique.data]);
+ try {
+ const id = await stateDemografi.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stateDemografi.findUnique.load(idStr);
+ if (stateDemografi.findUnique.data) {
+ setChartData([stateDemografi.findUnique.data]);
+ }
}
+ resetForm();
+ router.push('/admin/ekonomi/demografi-pekerjaan');
+ } catch (error) {
+ console.error('Error creating demografi pekerjaan:', error);
+ toast.error('Terjadi kesalahan saat menambah demografi pekerjaan');
+ } finally {
+ setIsSubmitting(false);
}
- resetForm();
- router.push('/admin/ekonomi/demografi-pekerjaan');
};
return (
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Demografi Pekerjaan
@@ -106,6 +113,17 @@ function CreateDemografiPekerjaan() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx
index cc679025..ea4af5a2 100644
--- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
@@ -5,7 +6,9 @@ import {
Box,
Button,
Center,
+ Flex,
Group,
+ Pagination,
Paper,
Skeleton,
Stack,
@@ -16,10 +19,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
- Pagination,
- Flex,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -85,7 +85,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
useEffect(() => {
if (data) {
setChartData(
- data.map((item) => ({
+ data.map((item: any) => ({
id: item.id,
pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki),
@@ -110,25 +110,23 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
List Demografi Pekerjaan
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
+ >
+ Tambah Baru
+
- Pekerjaan
- Laki - Laki
- Perempuan
+ Pekerjaan
+ Laki - Laki
+ Perempuan
Edit
Hapus
@@ -137,9 +135,9 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
{filteredData.length > 0 ? (
filteredData.map((item) => (
- {item.pekerjaan}
- {item.lakiLaki}
- {item.perempuan}
+ {item.pekerjaan}
+ {item.lakiLaki}
+ {item.perempuan}
{
if (!id) return;
@@ -25,10 +46,14 @@ function EditJumlahPendudukMiskin() {
await stateJPM.findUnique.load(id);
const data = stateJPM.findUnique.data;
if (data) {
- stateJPM.update.form = {
+ setFormData({
year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0,
- };
+ });
+ setOriginalData({
+ year: data.year || 0,
+ totalPoorPopulation: data.totalPoorPopulation || 0,
+ });
}
} catch (error) {
console.error('Gagal memuat data:', error);
@@ -39,26 +64,52 @@ function EditJumlahPendudukMiskin() {
loadData();
}, [id]);
+ // 🔹 Handler input controlled
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: Number(value),
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ year: originalData.year,
+ totalPoorPopulation: originalData.totalPoorPopulation,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
+ // 🔹 Submit form
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
stateJPM.update.id = id;
+ // update global state cuma saat submit
+ stateJPM.update.form = { ...formData };
+
await stateJPM.update.submit();
toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
} catch (error) {
console.error('Gagal menyimpan data:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Jumlah Penduduk Miskin
@@ -78,10 +129,8 @@ function EditJumlahPendudukMiskin() {
placeholder="Masukkan tahun"
type="number"
required
- value={stateJPM.update.form.year}
- onChange={(val) => {
- stateJPM.update.form.year = Number(val.currentTarget.value);
- }}
+ value={formData.year}
+ onChange={(e) => handleChange('year', e.currentTarget.value)}
/>
{
- stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
- }}
+ value={formData.totalPoorPopulation}
+ onChange={(e) =>
+ handleChange('totalPoorPopulation', e.currentTarget.value)
+ }
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
index d045d1d6..363688f5 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx
@@ -1,18 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
-import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import colors from '@/con/colors';
+import { Box, Button, Group, Loader, 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 colors from '@/con/colors';
import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
+import { toast } from 'react-toastify';
export default function CreateJumlahPendudukMiskin() {
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState([]);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateJPM.create.form = {
@@ -22,27 +24,33 @@ export default function CreateJumlahPendudukMiskin() {
};
const handleSubmit = async () => {
- const id = await stateJPM.create.create();
- if (id) {
- const idStr = String(id);
- await stateJPM.findUnique.load(idStr);
- if (stateJPM.findUnique.data) {
- setChartData([stateJPM.findUnique.data]);
+ try {
+ setIsSubmitting(true);
+ const id = await stateJPM.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stateJPM.findUnique.load(idStr);
+ if (stateJPM.findUnique.data) {
+ setChartData([stateJPM.findUnique.data]);
+ }
}
+ resetForm();
+ router.push('/admin/ekonomi/jumlah-penduduk-miskin');
+ } catch (error) {
+ console.error(error)
+ toast.error(error instanceof Error ? error.message : "Gagal menambahkan jumlah penduduk miskin")
+ } finally {
+ setIsSubmitting(false);
}
- resetForm();
- router.push('/admin/ekonomi/jumlah-penduduk-miskin');
};
return (
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Jumlah Penduduk Miskin
@@ -82,6 +90,17 @@ export default function CreateJumlahPendudukMiskin() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx
index 498a8ce4..c588820c 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx
@@ -1,23 +1,42 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
-import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
-import HeaderSearch from '../../_com/header';
+import {
+ Box,
+ Button,
+ Center,
+ Group,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title
+} from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
-import { useShallowEffect, useMediaQuery } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
-import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
-import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts';
+import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
+import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
+
+// ✅ BarChart Mantine
+import { BarChart } from '@mantine/charts';
function JumlahPendudukMiskin() {
const [search, setSearch] = useState("");
return (
}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,7 +47,7 @@ function JumlahPendudukMiskin() {
}
function ListJumlahPendudukMiskin({ search }: { search: string }) {
- type JPMGrafik = { id: string; year: number; totalPoorPopulation: number }
+ type JPMGrafik = { year: number; totalPoorPopulation: number };
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState([]);
const [mounted, setMounted] = useState(false);
@@ -36,33 +55,27 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState(null);
const router = useRouter();
- const isTablet = useMediaQuery('(max-width:1024px)');
- const isMobile = useMediaQuery('(max-width:768px)');
+ const { data, page, loading, load, totalPages } = stateJPM.findMany;
- const {
- data,
- page,
- loading,
- load,
- totalPages,
- } = stateJPM.findMany;
- // Load data
+ // Load data awal
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
+ // Update chart data
useEffect(() => {
if (stateJPM.findMany.data) {
- setChartData(stateJPM.findMany.data.map(item => ({
- id: item.id,
- year: Number(item.year),
- totalPoorPopulation: Number(item.totalPoorPopulation)
- })));
+ setChartData(
+ stateJPM.findMany.data.map((item) => ({
+ year: Number(item.year),
+ totalPoorPopulation: Number(item.totalPoorPopulation),
+ }))
+ );
}
}, [stateJPM.findMany.data]);
- const filteredData = data || []
+ const filteredData = data || [];
const handleDelete = () => {
if (selectedId) {
@@ -71,7 +84,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
setSelectedId(null);
stateJPM.findMany.load();
}
- }
+ };
if (loading || !data) {
return (
@@ -83,19 +96,20 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
return (
+ {/* Tabel */}
Daftar Jumlah Penduduk Miskin
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')
+ }
+ >
+ Tambah Baru
+
@@ -109,22 +123,38 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
- {filteredData.length > 0 ? filteredData.map(item => (
-
- {item.year}
- {item.totalPoorPopulation}
-
- router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
-
-
-
-
- { setSelectedId(item.id); setModalHapus(true) }}>
-
-
-
-
- )) : (
+ {filteredData.length > 0 ? (
+ filteredData.map((item) => (
+
+ {item.year}
+ {item.totalPoorPopulation}
+
+
+ router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)
+ }
+ >
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
+
+
+ ))
+ ) : (
@@ -138,6 +168,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
+ {/* Pagination */}
- {/* Chart */}
+ {/* Bar Chart */}
-
- Grafik Jumlah Penduduk Miskin
- {mounted && chartData.length > 0 ? (
-
-
-
-
-
-
-
- ) : (
- Belum ada data untuk ditampilkan dalam grafik
- )}
-
+
+ Grafik Jumlah Penduduk Miskin
+
+ {mounted && chartData.length > 0 ? (
+ ({
+ name: item.year.toString(),
+ value: item.totalPoorPopulation,
+ }))}
+ dataKey="name"
+ series={[
+ { name: 'value', color: colors['blue-button'] },
+ ]}
+ withTooltip
+ valueFormatter={(v) => `${v.toLocaleString()} jiwa`}
+ />
+ ) : (
+ Belum ada data untuk ditampilkan dalam grafik
+ )}
-
{/* Modal Hapus */}
setModalHapus(false)}
onConfirm={handleDelete}
- text='Apakah anda yakin ingin menghapus data ini?'
+ text="Apakah anda yakin ingin menghapus data ini?"
/>
);
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx
index 8b884c50..ee70c101 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx
@@ -2,18 +2,17 @@
'use client'
import colors from '@/con/colors';
import {
+ ScrollArea,
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
- Title,
- Tooltip,
- ScrollArea,
+ Title
} from '@mantine/core';
+import { IconSchool, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconUsers, IconSchool } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -24,15 +23,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
label: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia",
- icon: ,
- tooltip: "Data pengangguran menurut kelompok usia",
+ icon:
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan",
- icon: ,
- tooltip: "Data pengangguran menurut tingkat pendidikan",
+ icon:
},
];
@@ -78,26 +75,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
index 5d58837e..e2991004 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx
@@ -1,11 +1,12 @@
+/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
-/* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors';
-import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() {
@@ -13,38 +14,95 @@ function EditGrafikBerdasarkanPendidikan() {
const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id;
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // state lokal untuk form
+ const [formData, setFormData] = useState({
+ SD: '',
+ SMP: '',
+ SMA: '',
+ D3: '',
+ S1: '',
+ });
+
+ const [originalData, setOriginalData] = useState({
+ SD: '',
+ SMP: '',
+ SMA: '',
+ D3: '',
+ S1: '',
+ });
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data;
if (data) {
- stategrafik.update.form = {
+ setFormData({
SD: data.SD || '',
SMP: data.SMP || '',
SMA: data.SMA || '',
D3: data.D3 || '',
S1: data.S1 || '',
- };
+ });
+ setOriginalData({
+ SD: data.SD || '',
+ SMP: data.SMP || '',
+ SMA: data.SMA || '',
+ D3: data.D3 || '',
+ S1: data.S1 || '',
+ });
}
});
}
}, [id]);
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.currentTarget;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+
+
+ const handleResetForm = () => {
+ setFormData({
+ SD: originalData.SD,
+ SMP: originalData.SMP,
+ SMA: originalData.SMA,
+ D3: originalData.D3,
+ S1: originalData.S1,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
const handleSubmit = async () => {
- stategrafik.update.id = id;
- await stategrafik.update.submit();
- router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan');
+ try {
+ setIsSubmitting(true);
+ stategrafik.update.id = id;
+ stategrafik.update.form = { ...formData }; // update global state pas submit aja
+ await stategrafik.update.submit();
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
+ );
+ } catch (error) {
+ console.error(error);
+ toast.error('Terjadi kesalahan saat memperbarui data grafik');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Grafik Pengangguran Berdasarkan Pendidikan
@@ -60,42 +118,58 @@ function EditGrafikBerdasarkanPendidikan() {
>
(stategrafik.update.form.SD = val.currentTarget.value)}
+ value={formData.SD}
+ onChange={handleChange}
/>
(stategrafik.update.form.SMP = val.currentTarget.value)}
+ value={formData.SMP}
+ onChange={handleChange}
/>
(stategrafik.update.form.SMA = val.currentTarget.value)}
+ value={formData.SMA}
+ onChange={handleChange}
/>
(stategrafik.update.form.D3 = val.currentTarget.value)}
+ value={formData.D3}
+ onChange={handleChange}
/>
(stategrafik.update.form.S1 = val.currentTarget.value)}
+ value={formData.S1}
+ onChange={handleChange}
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
index d62b2de1..c530352d 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx
@@ -1,19 +1,20 @@
'use client';
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
-import React from 'react';
-import { useRouter } from 'next/navigation';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
-import { useProxy } from 'valtio/utils';
-import { useState } from 'react';
import colors from '@/con/colors';
-import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
+import { Box, Button, Loader, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
function CreateGrafikBerdasarkanPendidikan() {
const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stategrafik.create.form = {
@@ -27,28 +28,34 @@ function CreateGrafikBerdasarkanPendidikan() {
};
const handleSubmit = async () => {
- const id = await stategrafik.create.create();
- if (id) {
- const idStr = String(id);
- await stategrafik.findUnique.load(idStr);
- if (stategrafik.findUnique.data) {
- setDonutData([stategrafik.findUnique.data]);
+ try {
+ setIsSubmitting(true);
+ const id = await stategrafik.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stategrafik.findUnique.load(idStr);
+ if (stategrafik.findUnique.data) {
+ setDonutData([stategrafik.findUnique.data]);
+ }
}
+ resetForm();
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
+ );
+ } catch (error) {
+ console.error(error);
+ toast.error('Terjadi kesalahan saat menambahkan data grafik');
+ } finally {
+ setIsSubmitting(false);
}
- resetForm();
- router.push(
- '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
- );
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Data Pengangguran Berdasarkan Pendidikan
@@ -105,6 +112,17 @@ function CreateGrafikBerdasarkanPendidikan() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx
index 328a19da..825f6358 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx
@@ -1,24 +1,41 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { DonutChart } from '@mantine/charts';
+import {
+ Box,
+ Button,
+ Center,
+ Flex,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title
+} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
-import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() {
- const [search, setSearch] = useState("");
+ const [search, setSearch] = useState('');
return (
}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +48,6 @@ function GrafikBerdasarkanPendidikan() {
function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState([]);
- const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState(null);
const router = useRouter();
@@ -45,37 +61,45 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
}
};
- const {
- data,
- page,
- totalPages,
- loading,
- load,
- } = stategrafik.findMany;
+ const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
- setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
if (stategrafik.findMany.data) {
- const SD = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SD || 0), 0);
- const SMP = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMP || 0), 0);
- const SMA = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMA || 0), 0);
- const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
- const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
+ const SD = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.SD || 0),
+ 0,
+ );
+ const SMP = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.SMP || 0),
+ 0,
+ );
+ const SMA = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.SMA || 0),
+ 0,
+ );
+ const D3 = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.D3 || 0),
+ 0,
+ );
+ const S1 = stategrafik.findMany.data.reduce(
+ (acc: number, cur: any) => acc + Number(cur.S1 || 0),
+ 0,
+ );
setDonutData([
- { name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' },
- { name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' },
- { name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' },
- { name: 'D3', value: D3, color: '#DB524D', key: 'D3' },
- { name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
+ { name: 'SD', value: SD, color: '#4b6Ef5' },
+ { name: 'SMP', value: SMP, color: '#14b885' },
+ { name: 'SMA', value: SMA, color: '#E6A03B' },
+ { name: 'D3', value: D3, color: '#DB524D' },
+ { name: 'S1', value: S1, color: '#1018A8FF' },
]);
}
}, [stategrafik.findMany.data]);
- const filteredData = data || []
+ const filteredData = data || [];
if (loading || !data) {
return (
@@ -87,21 +111,24 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
return (
+ {/* Table Data */}
- {/* Header */}
- List Pengangguran Berdasarkan Usia Kerja
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
- >
- Tambah Baru
-
-
+ List Pengangguran Berdasarkan Pendidikan
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create',
+ )
+ }
+ >
+ Tambah Baru
+
+
@@ -120,7 +147,9 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
- Belum ada data grafik responden
+
+ Belum ada data grafik responden
+
@@ -133,25 +162,30 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
{item.D3}
{item.S1}
-
- router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
-
-
-
+
+ router.push(
+ `/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`,
+ )
+ }
+ >
+
+
-
- {
- setSelectedId(item.id);
- setModalHapus(true);
- }}>
-
-
-
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
))
@@ -161,6 +195,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
+ {/* Pagination */}
- {/* Chart */}
-
-
-
- Grafik Pengangguran Berdasarkan Pendidikan
- {mounted && donutData.length > 0 ? (
-
-
-
- {donutData.map((entry, index) => (
- |
- ))}
-
-
-
- {donutData.map((entry) => (
-
-
- {entry.name} : {entry.value}
-
- ))}
-
-
+ {/* Donut Chart */}
+
+
+
+ Grafik Pengangguran Berdasarkan Pendidikan
+
+
+ {donutData.length > 0 ? (
+
) : (
- Belum ada data untuk ditampilkan dalam grafik
+
+ Belum ada data untuk ditampilkan dalam grafik
+
)}
-
-
-
+
+
+
- {/* Modal Konfirmasi Hapus */}
+ {/* Modal Hapus */}
setModalHapus(false)}
onConfirm={handleDelete}
- text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?'
+ text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?"
/>
);
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
index 8a6ee1b8..ce974a57 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx
@@ -2,39 +2,97 @@
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
-import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
+import {
+ Box,
+ Button,
+ Group,
+ Loader,
+ 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 { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter();
const params = useParams() as { id: string };
- const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
+ const stategrafik = useProxy(
+ grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur
+ );
const id = params.id;
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // ✅ state lokal, controlled
+ const [formData, setFormData] = useState({
+ usia18_25: '',
+ usia26_35: '',
+ usia36_45: '',
+ usia46_keatas: '',
+ });
+
+ const [originalData, setOriginalData] = useState({
+ usia18_25: '',
+ usia26_35: '',
+ usia36_45: '',
+ usia46_keatas: '',
+ });
+
+ // load data dari global state -> masukin ke local state
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data;
if (data) {
- stategrafik.update.form = {
+ setFormData({
usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '',
- };
+ });
+ setOriginalData({
+ usia18_25: data.usia18_25 || '',
+ usia26_35: data.usia26_35 || '',
+ usia36_45: data.usia36_45 || '',
+ usia46_keatas: data.usia46_keatas || '',
+ });
+
}
});
}
}, [id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ usia18_25: originalData.usia18_25,
+ usia26_35: originalData.usia26_35,
+ usia36_45: originalData.usia36_45,
+ usia46_keatas: originalData.usia46_keatas,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // ✅ baru update global state pas submit
stategrafik.update.id = id;
+ stategrafik.update.form = { ...formData };
+
await stategrafik.update.submit();
+
toast.success('Data grafik berhasil diperbarui!');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
@@ -42,17 +100,22 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
} catch (error) {
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui data grafik');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Grafik Pengangguran Berdasarkan Usia Kerja
@@ -71,44 +134,47 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
- value={stategrafik.update.form.usia18_25}
- onChange={(val) => {
- stategrafik.update.form.usia18_25 = val.currentTarget.value;
- }}
+ value={formData.usia18_25}
+ onChange={(e) => handleChange('usia18_25', e.currentTarget.value)}
required
/>
{
- stategrafik.update.form.usia26_35 = val.currentTarget.value;
- }}
+ value={formData.usia26_35}
+ onChange={(e) => handleChange('usia26_35', e.currentTarget.value)}
required
/>
{
- stategrafik.update.form.usia36_45 = val.currentTarget.value;
- }}
+ value={formData.usia36_45}
+ onChange={(e) => handleChange('usia36_45', e.currentTarget.value)}
required
/>
{
- stategrafik.update.form.usia46_keatas = val.currentTarget.value;
- }}
+ value={formData.usia46_keatas}
+ onChange={(e) => handleChange('usia46_keatas', e.currentTarget.value)}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
index 673256f5..677d488b 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx
@@ -2,18 +2,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
-import React, { useState } from 'react';
-import { useRouter } from 'next/navigation';
-import { useProxy } from 'valtio/utils';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
-import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stategrafik.create.form = {
@@ -26,27 +28,33 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
};
const handleSubmit = async () => {
- const id = await stategrafik.create.create();
- if (id) {
- const idStr = String(id);
- await stategrafik.findUnique.load(idStr);
- if (stategrafik.findUnique.data) {
- setDonutData([stategrafik.findUnique.data]);
+ try {
+ setIsSubmitting(true);
+ const id = await stategrafik.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stategrafik.findUnique.load(idStr);
+ if (stategrafik.findUnique.data) {
+ setDonutData([stategrafik.findUnique.data]);
+ }
}
+ resetForm();
+ router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
+ } catch (error) {
+ console.error('Error creating:', error);
+ toast.error('Terjadi kesalahan saat membuat data');
+ } finally {
+ setIsSubmitting(false);
}
- resetForm();
- router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
};
return (
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Data Pengangguran Berdasarkan Usia
@@ -97,6 +105,17 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
{/* Submit Button */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx
index 0c8b70d2..3c0fcfd6 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx
@@ -1,13 +1,30 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import {
+ Box,
+ Button,
+ Center,
+ Flex,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableTbody,
+ TableTd,
+ TableTh,
+ TableThead,
+ TableTr,
+ Text,
+ Title,
+} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
-import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
+import { DonutChart } from '@mantine/charts';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
@@ -17,8 +34,8 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
return (
}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +48,6 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState([]);
- const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState(null);
const router = useRouter();
@@ -45,17 +61,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
}
};
- const {
- data,
- page,
- totalPages,
- loading,
- load,
- } = stategrafik.findMany;
+ const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
- setMounted(true);
- load(page, 10, search)
+ load(page, 10, search);
}, [page, search]);
useEffect(() => {
@@ -64,16 +73,17 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const totalUsia26_35 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0);
const totalUsia36_45 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0);
const totalUsia46_keatas = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0);
+
setDonutData([
- { name: 'usia18_25', value: totalUsia18_25, color: colors['blue-button'], key: 'usia18_25' },
- { name: 'usia26_35', value: totalUsia26_35, color: '#10A85AFF', key: 'usia26_35' },
- { name: 'usia36_45', value: totalUsia36_45, color: '#C07B13FF', key: 'usia36_45' },
- { name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
+ { name: 'Usia 18-25', value: totalUsia18_25, color: colors['blue-button'] },
+ { name: 'Usia 26-35', value: totalUsia26_35, color: '#10A85AFF' },
+ { name: 'Usia 36-45', value: totalUsia36_45, color: '#C07B13FF' },
+ { name: 'Usia 46+', value: totalUsia46_keatas, color: '#1094A8FF' },
]);
}
}, [stategrafik.findMany.data]);
- const filteredData = data || []
+ const filteredData = data || [];
if (loading || !data) {
return (
@@ -85,24 +95,23 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
return (
+ {/* Table */}
- {/* Header */}
List Pengangguran Berdasarkan Usia Kerja
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')
+ }
+ >
+ Tambah Baru
+
- {/* Table */}
@@ -110,26 +119,38 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
Usia 18-25
Usia 26-35
Usia 36-45
- Usia 46 +
+ Usia 46+
Edit
Delete
{filteredData.length > 0 ? (
- filteredData.map(item => (
+ filteredData.map((item) => (
{item.usia18_25}
{item.usia26_35}
{item.usia36_45}
{item.usia46_keatas}
- router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
+
+ router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)
+ }
+ >
- { setSelectedId(item.id); setModalHapus(true); }}>
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
@@ -147,10 +168,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
-
+ {/* Pagination */}
- {/* Chart */}
+ {/* Donut Chart */}
- Grafik Pengangguran Berdasarkan Usia Kerja
- {mounted && donutData.length > 0 ? (
-
-
-
- {donutData.map((entry, index) => (
- |
- ))}
-
-
-
-
-
- Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}
-
-
-
- Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}
-
-
-
-
- Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}
-
-
-
-
- Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}
-
-
-
-
+
+ Grafik Pengangguran Berdasarkan Usia Kerja
+
+ {donutData.length > 0 ? (
+
+
+
) : (
Belum ada data untuk ditampilkan dalam grafik
)}
-
{/* Modal Konfirmasi Hapus */}
{
- const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
+ const [originalData, setOriginalData] = useState({
+ month: '',
+ year: new Date().getFullYear(),
+ educatedUnemployment: 0,
+ uneducatedUnemployment: 0,
+ totalUnemployment: 0,
+ percentageChange: 0,
+ });
- let percentageChange = 0;
- const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
- const currentMonthIndex = monthOrder.indexOf(formData.month);
+ // --- hitung total + persentase perubahan
+ const calculateTotalAndChange = useCallback(
+ async (data: typeof formData) => {
+ const total = data.educatedUnemployment + data.uneducatedUnemployment;
+ let percentageChange = 0;
- if (currentMonthIndex !== -1) {
- let prevMonthIndex = currentMonthIndex - 1;
- let prevYear = formData.year;
+ const currentMonthIndex = MONTHS.indexOf(data.month);
+ if (currentMonthIndex !== -1) {
+ let prevMonthIndex = currentMonthIndex - 1;
+ let prevYear = data.year;
- if (prevMonthIndex < 0) {
- prevMonthIndex = 11;
- prevYear--;
+ if (prevMonthIndex < 0) {
+ prevMonthIndex = 11;
+ prevYear--;
+ }
+
+ const prevData = await stateDetail.findByMonthYear.load({
+ month: MONTHS[prevMonthIndex],
+ year: prevYear,
+ });
+
+ if (prevData && prevData.totalUnemployment > 0) {
+ const change =
+ ((total - prevData.totalUnemployment) /
+ prevData.totalUnemployment) * 100;
+ percentageChange = parseFloat(change.toFixed(1));
+ }
}
- const prevMonth = monthOrder[prevMonthIndex];
- const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear });
-
- if (prevData && prevData.totalUnemployment > 0) {
- const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
- percentageChange = parseFloat(change.toFixed(1));
- }
- }
-
- return { total, percentageChange };
- };
+ return { total, percentageChange };
+ },
+ [stateDetail.findByMonthYear]
+ );
+ // --- update state lokal
const updateFormData = async (updates: Partial) => {
const newData = { ...formData, ...updates };
- const { total, percentageChange } = await calculateTotalAndChange();
+ const { total, percentageChange } = await calculateTotalAndChange(newData);
setFormData({ ...newData, totalUnemployment: total, percentageChange });
};
+ // --- load detail by ID (sekali)
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
if (!id) return;
try {
- await stateDetail.findUnique.load(id); // ambil by ID
+ await stateDetail.findUnique.load(id);
const data = stateDetail.findUnique.data;
+ if (!data) return;
- if (data) {
-
- // Convert year from Date to number if needed
- const yearValue = data.year && typeof data.year === 'object' && 'getFullYear' in data.year
+ const yearValue =
+ data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
- // Set the ID for update
- stateDetail.update.id = id;
+ stateDetail.update.id = id; // simpan id untuk update
- // Update Valtio state with converted year
- stateDetail.update.form = {
- ...data,
- year: yearValue,
- percentageChange: data.percentageChange || 0 // Ensure it's always a number
- };
+ setFormData({
+ month: data.month,
+ year: yearValue,
+ educatedUnemployment: data.educatedUnemployment,
+ uneducatedUnemployment: data.uneducatedUnemployment,
+ totalUnemployment: data.totalUnemployment,
+ percentageChange: data.percentageChange || 0,
+ });
- // Update local formData with converted year
- setFormData({
- month: data.month,
- year: yearValue,
- totalUnemployment: data.totalUnemployment,
- educatedUnemployment: data.educatedUnemployment,
- uneducatedUnemployment: data.uneducatedUnemployment,
- percentageChange: data.percentageChange || 0, // Ensure it's always a number
- });
- }
- } catch (error) {
- console.error("Error loading detail:", error);
- toast.error("Gagal memuat data detail");
+ setOriginalData({
+ month: data.month,
+ year: yearValue,
+ educatedUnemployment: data.educatedUnemployment,
+ uneducatedUnemployment: data.uneducatedUnemployment,
+ totalUnemployment: data.totalUnemployment,
+ percentageChange: data.percentageChange || 0,
+ });
+ } catch (err) {
+ console.error('Error loading detail:', err);
+ toast.error('Gagal memuat data detail');
}
};
loadDetail();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ month: originalData.month,
+ year: originalData.year,
+ educatedUnemployment: originalData.educatedUnemployment,
+ uneducatedUnemployment: originalData.uneducatedUnemployment,
+ totalUnemployment: originalData.totalUnemployment,
+ percentageChange: originalData.percentageChange,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ // --- submit form
const handleSubmit = async () => {
- const { total, percentageChange } = await calculateTotalAndChange();
try {
- stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange };
+ setIsSubmitting(true);
+ const { total, percentageChange } = await calculateTotalAndChange(formData);
+
+ stateDetail.update.form = {
+ ...formData,
+ totalUnemployment: total,
+ percentageChange,
+ };
+
const success = await stateDetail.update.submit();
if (success) {
toast.success('Detail data pengangguran berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-pengangguran');
}
- } catch (error) {
- console.error('Error updating:', error);
+ } catch (err) {
+ console.error('Error updating:', err);
toast.error('Terjadi kesalahan saat memperbarui data');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -129,11 +186,18 @@ function EditDetailDataPengangguran() {
-
+
updateFormData({ month: val || '' })}
/>
@@ -141,23 +205,52 @@ function EditDetailDataPengangguran() {
label="Tahun"
value={formData.year}
onChange={(val) => updateFormData({ year: Number(val) })}
+ required
/>
updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })}
+ onChange={(e) =>
+ updateFormData({
+ educatedUnemployment: Number(e.currentTarget.value) || 0,
+ })
+ }
+ required
/>
updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })}
+ onChange={(e) =>
+ updateFormData({
+ uneducatedUnemployment: Number(e.currentTarget.value) || 0,
+ })
+ }
+ required
/>
- Total Otomatis: {formData.totalUnemployment}
- Perubahan Otomatis: {formData.percentageChange !== null ? `${formData.percentageChange}%` : '-'}
+
+ Total Otomatis: {formData.totalUnemployment}
+
+
+ Perubahan Otomatis:{' '}
+ {formData.percentageChange !== null
+ ? `${formData.percentageChange}%`
+ : '-'}
+
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
@@ -178,4 +271,3 @@ function EditDetailDataPengangguran() {
}
export default EditDetailDataPengangguran;
-
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx
index a4043b22..3138479b 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx
@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
-import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -103,32 +103,28 @@ function DetailJumlahPengangguran() {
{/* Tombol Edit & Hapus */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- color="red"
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ color="red"
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
- router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
- color="green"
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
+ color="green"
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
index df0cb26a..d27bef0f 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx
@@ -7,23 +7,25 @@ import {
Box,
Button,
Group,
+ Loader,
+ NumberInput,
Paper,
+ Select,
Stack,
Text,
- NumberInput,
- Title,
- Select,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const [chartData, setChartData] = useState([]);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const monthOptions = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
@@ -73,15 +75,23 @@ function CreateJumlahPengangguran() {
};
const handleSubmit = async () => {
- await calculateTotalAndChange();
- const id = await stateDetail.create.create();
- if (id) {
- await stateDetail.findUnique.load(String(id));
- if (stateDetail.findUnique.data) {
- setChartData([stateDetail.findUnique.data]);
+ try {
+ setIsSubmitting(true);
+ await calculateTotalAndChange();
+ const id = await stateDetail.create.create();
+ if (id) {
+ await stateDetail.findUnique.load(String(id));
+ if (stateDetail.findUnique.data) {
+ setChartData([stateDetail.findUnique.data]);
+ }
+ resetForm();
+ router.push('/admin/ekonomi/jumlah-pengangguran');
}
- resetForm();
- router.push('/admin/ekonomi/jumlah-pengangguran');
+ } catch (error) {
+ console.error("Error creating jumlah pengangguran:", error);
+ toast.error("Gagal menambahkan data pengangguran");
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -89,16 +99,14 @@ function CreateJumlahPengangguran() {
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Data Pengangguran
@@ -179,7 +187,19 @@ function CreateJumlahPengangguran() {
{/* Action Button */}
-
+
+ {/* Tombol Batal */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx
index ff3a8209..698ba948 100644
--- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx
@@ -1,17 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
+import { BarChart } from '@mantine/charts';
import {
Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
- Text, Title, Tooltip
+ Text, Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
-import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
@@ -85,16 +85,14 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
Daftar Detail Data Pengangguran
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran/create')}
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
index ebe47764..0cc7b4b6 100644
--- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx
@@ -7,12 +7,12 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -24,17 +24,31 @@ function EditLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- posisi: lowonganKerjaState.update.form.posisi,
- namaPerusahaan: lowonganKerjaState.update.form.namaPerusahaan,
- lokasi: lowonganKerjaState.update.form.lokasi,
- tipePekerjaan: lowonganKerjaState.update.form.tipePekerjaan,
- gaji: lowonganKerjaState.update.form.gaji,
- deskripsi: lowonganKerjaState.update.form.deskripsi,
- kualifikasi: lowonganKerjaState.update.form.kualifikasi,
+ posisi: '',
+ namaPerusahaan: '',
+ lokasi: '',
+ tipePekerjaan: '',
+ gaji: '',
+ deskripsi: '',
+ kualifikasi: '',
+ notelp: '',
});
+ const [originalData, setOriginalData] = useState({
+ posisi: '',
+ namaPerusahaan: '',
+ lokasi: '',
+ tipePekerjaan: '',
+ gaji: '',
+ deskripsi: '',
+ kualifikasi: '',
+ notelp: '',
+ })
+
+ // load data sekali aja ketika mount / id berubah
useEffect(() => {
const loadLowongan = async () => {
const id = params?.id as string;
@@ -51,6 +65,17 @@ function EditLowonganKerja() {
gaji: data.gaji || '',
deskripsi: data.deskripsi || '',
kualifikasi: data.kualifikasi || '',
+ notelp: data.notelp || '',
+ });
+ setOriginalData({
+ posisi: data.posisi || '',
+ namaPerusahaan: data.namaPerusahaan || '',
+ lokasi: data.lokasi || '',
+ tipePekerjaan: data.tipePekerjaan || '',
+ gaji: data.gaji || '',
+ deskripsi: data.deskripsi || '',
+ kualifikasi: data.kualifikasi || '',
+ notelp: data.notelp || '',
});
}
} catch (error) {
@@ -62,14 +87,31 @@ function EditLowonganKerja() {
loadLowongan();
}, [params?.id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+ const handleResetForm = () => {
+ setFormData({
+ posisi: originalData.posisi,
+ namaPerusahaan: originalData.namaPerusahaan,
+ lokasi: originalData.lokasi,
+ tipePekerjaan: originalData.tipePekerjaan,
+ gaji: originalData.gaji,
+ deskripsi: originalData.deskripsi,
+ kualifikasi: originalData.kualifikasi,
+ notelp: originalData.notelp,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
lowonganState.update.id = params?.id as string;
-
- lowonganState.update.form = {
- ...lowonganState.update.form,
- ...formData,
- };
+ lowonganState.update.form = { ...formData };
await lowonganState.update.update();
toast.success("Lowongan kerja berhasil diperbarui!");
@@ -77,18 +119,17 @@ function EditLowonganKerja() {
} catch (error) {
console.error("Error updating lowongan kerja:", error);
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
+ } finally {
+ setIsSubmitting(false);
}
};
return (
- {/* Header dengan tombol back */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Lowongan Kerja Lokal
@@ -108,7 +149,7 @@ function EditLowonganKerja() {
label="Posisi"
placeholder="Masukkan posisi"
value={formData.posisi}
- onChange={(e) => setFormData({ ...formData, posisi: e.target.value })}
+ onChange={(e) => handleChange("posisi", e.target.value)}
required
/>
@@ -116,7 +157,7 @@ function EditLowonganKerja() {
label="Nama Perusahaan"
placeholder="Masukkan nama perusahaan"
value={formData.namaPerusahaan}
- onChange={(e) => setFormData({ ...formData, namaPerusahaan: e.target.value })}
+ onChange={(e) => handleChange("namaPerusahaan", e.target.value)}
required
/>
@@ -124,7 +165,15 @@ function EditLowonganKerja() {
label="Lokasi"
placeholder="Masukkan lokasi"
value={formData.lokasi}
- onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
+ onChange={(e) => handleChange("lokasi", e.target.value)}
+ required
+ />
+
+ handleChange("notelp", e.target.value)}
required
/>
@@ -132,7 +181,7 @@ function EditLowonganKerja() {
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"
value={formData.tipePekerjaan}
- onChange={(e) => setFormData({ ...formData, tipePekerjaan: e.target.value })}
+ onChange={(e) => handleChange("tipePekerjaan", e.target.value)}
required
/>
@@ -140,7 +189,7 @@ function EditLowonganKerja() {
label="Gaji (per bulan)"
placeholder="Masukkan gaji"
value={formData.gaji}
- onChange={(e) => setFormData({ ...formData, gaji: e.target.value })}
+ onChange={(e) => handleChange("gaji", e.target.value)}
required
/>
@@ -150,7 +199,7 @@ function EditLowonganKerja() {
setFormData({ ...formData, deskripsi: val })}
+ onChange={(val) => handleChange("deskripsi", val)}
/>
@@ -160,11 +209,22 @@ function EditLowonganKerja() {
setFormData({ ...formData, kualifikasi: val })}
+ onChange={(val) => handleChange("kualifikasi", val)}
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx
index 79af7355..c9776908 100644
--- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx
@@ -1,6 +1,6 @@
'use client'
-import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -82,6 +82,11 @@ function DetailLowonganKerjaLokal() {
{data.lokasi || '-'}
+
+ Nomor Yang Dapat Dihubungi
+ {data.notelp || '-'}
+
+
Tipe Pekerjaan
{data.tipePekerjaan || '-'}
@@ -94,41 +99,37 @@ function DetailLowonganKerjaLokal() {
Deskripsi
-
+
Kualifikasi
-
+
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
- router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
index 61adecdf..4ee31ec4 100644
--- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx
@@ -4,22 +4,25 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
function CreateLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
lowonganState.create.form = {
@@ -30,29 +33,37 @@ function CreateLowonganKerja() {
gaji: '',
deskripsi: '',
kualifikasi: '',
+ notelp: '',
};
};
const handleSubmit = async () => {
- await lowonganState.create.create();
- resetForm();
- router.push('/admin/ekonomi/lowongan-kerja-lokal');
+ try {
+ setIsSubmitting(true);
+ await lowonganState.create.create();
+ resetForm();
+ router.push('/admin/ekonomi/lowongan-kerja-lokal');
+ } catch (error) {
+ console.error('Error creating lowongan kerja:', error);
+ toast.error(
+ error instanceof Error ? error.message : 'Gagal membuat lowongan kerja'
+ );
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
- {/* Header dengan tombol kembali */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Lowongan Kerja Lokal
@@ -86,6 +97,15 @@ function CreateLowonganKerja() {
placeholder="Masukkan nama perusahaan"
required
/>
+
+ (lowonganState.create.form.notelp = val.target.value)
+ }
+ label="Nomor Yang Dapat Dihubungi"
+ placeholder="Masukkan nomor yang dapat dihubungi"
+ required
+ />
@@ -140,6 +160,17 @@ function CreateLowonganKerja() {
{/* Tombol Simpan */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx
index 2c4452ac..149bed60 100644
--- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -69,7 +68,6 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
Daftar Lowongan Kerja Lokal
-
}
color="blue"
@@ -80,7 +78,6 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
>
Tambah Baru
-
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx
index 6c4e5e2e..a4edbe61 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx
@@ -2,18 +2,17 @@
'use client'
import colors from '@/con/colors';
import {
+ ScrollArea,
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
- Title,
- Tooltip,
- ScrollArea,
+ Title
} from '@mantine/core';
+import { IconCategory, IconShoppingBag } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconShoppingBag, IconCategory } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -24,15 +23,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
label: "Produk Pasar Desa",
value: "produkpasardesa",
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa",
- icon: ,
- tooltip: "Kelola data produk yang ada di pasar desa",
+ icon:
},
{
label: "Kategori Produk",
value: "kategoriproduk",
href: "/admin/ekonomi/pasar-desa/kategori-produk",
- icon: ,
- tooltip: "Atur kategori produk pasar desa",
+ icon:
},
];
@@ -85,26 +82,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
index 310cfdfb..12c82847 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx
@@ -1,33 +1,32 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
-import React, { useEffect, useState } from 'react';
-import { useParams, useRouter } from 'next/navigation';
-import { useProxy } from 'valtio/utils';
-import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
- Title,
- TextInput,
- Tooltip,
Text,
+ TextInput,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function EditKategoriProduk() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk);
-
- const [formData, setFormData] = useState({
- nama: '',
- });
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [formData, setFormData] = useState({ nama: '' });
+ const [originalData, setOriginalData] = useState({ nama: '' });
useEffect(() => {
const loadKategoriProduk = async () => {
@@ -37,11 +36,12 @@ function EditKategoriProduk() {
const data = await statePasar.edit.load(id);
if (data) {
- // pastikan id-nya masuk ke state edit
+ // simpan id ke state global hanya untuk referensi
statePasar.edit.id = id;
- setFormData({
- nama: data.nama || '',
- });
+
+ // simpan data ke state lokal
+ setFormData({ nama: data.nama || '' });
+ setOriginalData({ nama: data.nama || '' });
}
} catch (error) {
console.error('Error loading kategori produk:', error);
@@ -52,17 +52,30 @@ function EditKategoriProduk() {
loadKategoriProduk();
}, [id]);
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData((prev) => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
if (!formData.nama.trim()) {
toast.error('Nama kategori produk tidak boleh kosong');
return;
}
- statePasar.edit.form = {
- nama: formData.nama.trim(),
- };
-
+ // update global state hanya saat submit
+ statePasar.edit.form = { nama: formData.nama.trim() };
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
@@ -76,6 +89,8 @@ function EditKategoriProduk() {
} catch (error) {
console.error('Error updating kategori produk:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori produk');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -83,11 +98,14 @@ function EditKategoriProduk() {
{/* Header dengan tombol back */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Kategori Produk
@@ -104,14 +122,26 @@ function EditKategoriProduk() {
>
Nama Kategori Produk}
placeholder="Masukkan nama kategori produk"
value={formData.nama}
- onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
+ onChange={handleChange}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
index cf153d3b..f7066fa2 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx
@@ -5,15 +5,15 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
@@ -21,6 +21,7 @@ import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function CreateKategoriProduk() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk);
+ const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
statePasar.findMany.load();
@@ -33,29 +34,34 @@ function CreateKategoriProduk() {
};
const handleSubmit = async () => {
- if (!statePasar.create.form.nama) {
- return toast.warn('Nama kategori produk wajib diisi');
+ try {
+ if (!statePasar.create.form.nama) {
+ return toast.warn('Nama kategori produk wajib diisi');
+ }
+ setIsSubmitting(true);
+ await statePasar.create.create();
+ resetForm();
+ router.push('/admin/ekonomi/pasar-desa/kategori-produk');
+ } catch (error) {
+ console.error(error)
+ toast.error('Gagal menambahkan kategori produk');
+ } finally {
+ setIsSubmitting(false);
}
-
- await statePasar.create.create();
- resetForm();
- router.push('/admin/ekonomi/pasar-desa/kategori-produk');
};
return (
{/* Header dengan tombol kembali */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Kategori Produk
@@ -80,6 +86,17 @@ function CreateKategoriProduk() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx
index 0115aefd..9c83d9b9 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx
@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -68,7 +68,6 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
Daftar Kategori Produk
-
}
color="blue"
@@ -77,7 +76,6 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
>
Tambah Baru
-
@@ -99,7 +97,6 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
-
-
-
-
-
+
))
) : (
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
index 098c500e..70cb1458 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx
@@ -1,21 +1,22 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
MultiSelect,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -24,6 +25,16 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
+type FormData = {
+ nama: string;
+ harga: number;
+ alamatUsaha: string;
+ imageId: string;
+ rating: number;
+ kategoriId: string[];
+ kontak: string;
+};
+
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState);
const router = useRouter();
@@ -31,17 +42,32 @@ function EditPasarDesa() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
- const [formData, setFormData] = useState({
- nama: pasarState.pasarDesa.edit.form.nama || '',
- harga: pasarState.pasarDesa.edit.form.harga || 0,
- alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || '',
- imageId: pasarState.pasarDesa.edit.form.imageId || '',
- rating: pasarState.pasarDesa.edit.form.rating || 0,
- kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [formData, setFormData] = useState({
+ nama: '',
+ harga: 0,
+ alamatUsaha: '',
+ imageId: '',
+ rating: 0,
+ kategoriId: [],
+ kontak: '',
});
+ const [originalData, setOriginalData] = useState({
+ nama: '',
+ harga: 0,
+ alamatUsaha: '',
+ imageId: '',
+ imageUrl: "",
+ rating: 0,
+ kategoriId: [],
+ kontak: '',
+ });
+
+ // load data awal
useEffect(() => {
- pasarState.kategoriProduk.findMany.load();
+ pasarState.kategoriProduk.findManyAll.load();
+
const loadPasarDesa = async () => {
const id = params?.id as string;
if (!id) return;
@@ -56,6 +82,17 @@ function EditPasarDesa() {
imageId: data.imageId || '',
rating: data.rating || 0,
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
+ kontak: data.kontak || '',
+ });
+ setOriginalData({
+ nama: data.nama || '',
+ harga: data.harga || 0,
+ alamatUsaha: data.alamatUsaha || '',
+ imageId: data.imageId || '',
+ imageUrl: data.image?.link || "",
+ rating: data.rating || 0,
+ kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
+ kontak: data.kontak || '',
});
if (data.image?.link) setPreviewImage(data.image.link);
}
@@ -70,36 +107,61 @@ function EditPasarDesa() {
loadPasarDesa();
}, [params?.id]);
+ const handleChange = (key: keyof FormData, value: any) => {
+ setFormData((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ harga: originalData.harga,
+ alamatUsaha: originalData.alamatUsaha,
+ imageId: originalData.imageId,
+ rating: originalData.rating,
+ kategoriId: (originalData as any)?.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
+ kontak: originalData.kontak,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+
const handleSubmit = async () => {
try {
- pasarState.pasarDesa.edit.form = { ...pasarState.pasarDesa.edit.form, ...formData };
-
+ setIsSubmitting(true);
+ // upload image kalau ada file baru
+ let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
-
if (!uploaded?.id) return toast.error('Gagal upload gambar');
-
- pasarState.pasarDesa.edit.form.imageId = uploaded.id;
+ imageId = uploaded.id;
}
+ // update global state hanya saat submit
+ pasarState.pasarDesa.edit.form = {
+ ...formData,
+ imageId,
+ };
+
await pasarState.pasarDesa.edit.update();
toast.success('Pasar desa berhasil diperbarui!');
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
} catch (error) {
console.error('Error updating pasar desa:', error);
toast.error('Terjadi kesalahan saat memperbarui pasar desa');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Pasar Desa
@@ -114,6 +176,7 @@ function EditPasarDesa() {
style={{ border: '1px solid #e0e0e0' }}
>
+ {/* Dropzone upload */}
Gambar Produk
@@ -128,7 +191,7 @@ function EditPasarDesa() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -147,34 +210,55 @@ function EditPasarDesa() {
Seret gambar atau klik untuk memilih file
- Maksimal 5MB, format gambar wajib
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
+ {/* Controlled Inputs */}
setFormData({ ...formData, nama: e.target.value })}
+ onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -183,7 +267,7 @@ function EditPasarDesa() {
label="Harga Produk"
placeholder="Masukkan harga produk"
value={formData.harga}
- onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
+ onChange={(e) => handleChange('harga', Number(e.target.value))}
required
/>
@@ -195,7 +279,7 @@ function EditPasarDesa() {
label="Rating Produk"
placeholder="Masukkan rating produk (0-5)"
value={formData.rating}
- onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
+ onChange={(e) => handleChange('rating', Number(e.target.value))}
required
/>
@@ -203,7 +287,15 @@ function EditPasarDesa() {
label="Alamat Usaha"
placeholder="Masukkan alamat usaha"
value={formData.alamatUsaha}
- onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
+ onChange={(e) => handleChange('alamatUsaha', e.target.value)}
+ required
+ />
+
+ handleChange('kontak', e.target.value)}
required
/>
@@ -211,9 +303,9 @@ function EditPasarDesa() {
label="Kategori Produk"
placeholder="Pilih kategori produk"
value={formData.kategoriId}
- onChange={(val) => setFormData({ ...formData, kategoriId: val })}
+ onChange={(val) => handleChange('kategoriId', val)}
data={
- pasarState.kategoriProduk.findMany.data?.map((v) => ({
+ pasarState.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
@@ -225,6 +317,17 @@ function EditPasarDesa() {
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx
index c6881f1a..a36b7d9d 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx
@@ -1,13 +1,13 @@
'use client'
+import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import colors from '@/con/colors';
-import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
-import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react';
-import { useRouter, useParams } from 'next/navigation';
-import React, { useState } from 'react';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
+import { useParams, useRouter } from 'next/navigation';
+import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
-import { useShallowEffect } from '@mantine/hooks';
-import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const statePasar = useProxy(pasarDesaState);
@@ -85,6 +85,11 @@ function DetailPasarDesa() {
{data.alamatUsaha || '-'}
+
+ Kontak
+ {data.kontak || '-'}
+
+
Gambar
{data.image?.link ? (
@@ -118,32 +123,28 @@ function DetailPasarDesa() {
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
- router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${data.id}/edit`)}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
index 861b1069..10202db1 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx
@@ -3,17 +3,18 @@
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
MultiSelect,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -28,9 +29,10 @@ export default function CreatePasarDesa() {
const statePasar = useProxy(pasarDesaState);
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
- statePasar.kategoriProduk.findMany.load();
+ statePasar.kategoriProduk.findManyAll.load();
}, []);
const resetForm = () => {
@@ -41,42 +43,49 @@ export default function CreatePasarDesa() {
imageId: '',
rating: 0,
kategoriId: [],
+ kontak: '',
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
- if (!file) {
- return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true);
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ statePasar.pasarDesa.create.form.imageId = uploaded.id;
+ await statePasar.pasarDesa.create.create();
+
+ resetForm();
+ router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
+ } catch (error) {
+ console.error('Error creating kategori produk:', error);
+ toast.error('Gagal membuat kategori produk');
+ } finally {
+ setIsSubmitting(false);
}
-
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
-
- const uploaded = res.data?.data;
- if (!uploaded?.id) {
- return toast.error('Gagal mengunggah gambar, silakan coba lagi');
- }
-
- statePasar.pasarDesa.create.form.imageId = uploaded.id;
- await statePasar.pasarDesa.create.create();
-
- resetForm();
- router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
};
return (
{/* Header dengan tombol kembali */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Produk Pasar Desa
@@ -107,7 +116,7 @@ export default function CreatePasarDesa() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -128,7 +137,7 @@ export default function CreatePasarDesa() {
{previewImage && (
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -184,6 +211,15 @@ export default function CreatePasarDesa() {
onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
/>
+ {/* Kontak */}
+ (statePasar.pasarDesa.create.form.kontak = e.target.value)}
+ />
+
{/* Kategori Produk */}
(statePasar.pasarDesa.create.form.kategoriId = val)}
data={
- statePasar.kategoriProduk.findMany.data?.map((v) => ({
+ statePasar.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
@@ -200,6 +236,17 @@ export default function CreatePasarDesa() {
{/* Tombol Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx
index fc14e3f3..e7e1b444 100644
--- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -68,18 +67,16 @@ function ListPasarDesa({ search }: { search: string }) {
Daftar Produk Pasar Desa
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa/create')
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
index e51f0058..5eae8178 100644
--- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -10,73 +9,150 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ 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 { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+
+type Statistik = {
+ tahun: string;
+ jumlah: string;
+};
+
+type FormData = {
+ nama: string;
+ deskripsi: string;
+ icon: string;
+ statistik: Statistik;
+};
+
+const initialForm: FormData = {
+ nama: '',
+ deskripsi: '',
+ icon: '',
+ statistik: {
+ tahun: '',
+ jumlah: '',
+ },
+};
function EditProgramKemiskinan() {
const router = useRouter();
- const params = useParams() as { id: string };
+ const { id } = useParams() as { id: string };
const stateProgram = useProxy(programKemiskinanState);
- const id = params.id;
+ const [formData, setFormData] = useState(initialForm);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [originalData, setOriginalData] = useState(initialForm);
+
+ // Load data 1x dari global state → isi local state
useEffect(() => {
- if (id) {
- stateProgram.findUnique.load(id).then(() => {
+ if (!id) return;
+
+ const loadData = async () => {
+ try {
+ await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data;
if (data) {
- stateProgram.update.form = {
- nama: data.nama || '',
- deskripsi: data.deskripsi || '',
- icon: data.icon || '',
+ setFormData({
+ nama: data.nama ?? '',
+ deskripsi: data.deskripsi ?? '',
+ icon: data.icon ?? '',
statistik: {
- tahun: data.statistik?.tahun?.toString() || '',
- jumlah: data.statistik?.jumlah?.toString() || '',
+ tahun: data.statistik?.tahun?.toString() ?? '',
+ jumlah: data.statistik?.jumlah?.toString() ?? '',
},
- };
+ });
+ setOriginalData({
+ nama: data.nama ?? '',
+ deskripsi: data.deskripsi ?? '',
+ icon: data.icon ?? '',
+ statistik: {
+ tahun: data.statistik?.tahun?.toString() ?? '',
+ jumlah: data.statistik?.jumlah?.toString() ?? '',
+ },
+ });
}
- }).catch((err) => {
- console.error("Error load data:", err);
- toast.error("Gagal mengambil data program");
- });
- }
- }, [id]);
+ } catch (err) {
+ console.error('Error load data:', err);
+ toast.error('Gagal mengambil data program');
+ }
+ };
+
+ loadData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [id]); // ✅ hanya trigger saat id berubah
+
+
+ // generic handler untuk field top-level
+ const handleChange = useCallback(
+ (field: keyof FormData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ // khusus nested statistik
+ const handleStatistikChange = useCallback(
+ (field: keyof Statistik, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ statistik: { ...prev.statistik, [field]: value },
+ }));
+ },
+ []
+ );
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ deskripsi: originalData.deskripsi,
+ icon: originalData.icon,
+ statistik: {
+ tahun: originalData.statistik.tahun,
+ jumlah: originalData.statistik.jumlah,
+ },
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
stateProgram.update.id = id;
+ stateProgram.update.form = formData;
await stateProgram.update.update();
- toast.success("Program berhasil diperbarui!");
+
+ toast.success('Program berhasil diperbarui!');
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {
- console.error("Error update program:", error);
- toast.error("Terjadi kesalahan saat memperbarui program");
+ console.error('Error update program:', error);
+ toast.error('Terjadi kesalahan saat memperbarui program');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
- {/* Header dengan tombol kembali */}
+ {/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Program Kemiskinan
@@ -92,10 +168,8 @@ function EditProgramKemiskinan() {
>
{
- stateProgram.update.form.nama = e.target.value;
- }}
+ value={formData.nama}
+ onChange={(e) => handleChange('nama', e.target.value)}
label={Judul Program }
placeholder="Masukkan judul program"
required
@@ -106,10 +180,8 @@ function EditProgramKemiskinan() {
Deskripsi
{
- stateProgram.update.form.deskripsi = val;
- }}
+ value={formData.deskripsi}
+ onChange={(val) => handleChange('deskripsi', val)}
/>
@@ -118,10 +190,8 @@ function EditProgramKemiskinan() {
Ikon Program Kreatif Desa
{
- stateProgram.update.form.icon = value;
- }}
+ value={formData.icon as IconKey}
+ onChange={(val) => handleChange('icon', val)}
/>
@@ -131,10 +201,8 @@ function EditProgramKemiskinan() {
{
- stateProgram.update.form.statistik.jumlah = e.target.value;
- }}
+ value={formData.statistik.jumlah}
+ onChange={(e) => handleStatistikChange('jumlah', e.target.value)}
label="Jumlah Masyarakat Miskin"
placeholder="Masukkan jumlah masyarakat miskin"
required
@@ -142,10 +210,8 @@ function EditProgramKemiskinan() {
{
- stateProgram.update.form.statistik.tahun = e.target.value;
- }}
+ value={formData.statistik.tahun}
+ onChange={(e) => handleStatistikChange('tahun', e.target.value)}
label="Tahun"
placeholder="Masukkan tahun"
required
@@ -154,6 +220,17 @@ function EditProgramKemiskinan() {
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx
index 2d87c9bc..25f58c09 100644
--- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx
@@ -8,17 +8,16 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
+import { IconKey, IconMapper } from '../../../_com/iconMap';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
-import { IconKey, IconMapper } from '../../../_com/iconMap';
function DetailProgramKemiskinan() {
const programState = useProxy(programKemiskinanState);
@@ -90,6 +89,7 @@ function DetailProgramKemiskinan() {
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
@@ -122,33 +122,29 @@ function DetailProgramKemiskinan() {
{/* Action Buttons */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- disabled={programState.delete.loading}
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={programState.delete.loading}
+ >
+
+
-
- router.push(`/admin/ekonomi/program-kemiskinan/${data.id}/edit`)}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ router.push(`/admin/ekonomi/program-kemiskinan/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
index 37d93d55..2c00adf1 100644
--- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx
@@ -7,27 +7,28 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useProxy } from 'valtio/utils';
-import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
-import CreateEditor from '../../../_com/createEditor';
-import SelectIconProgram from '../../../_com/selectIcon';
import { useState } from 'react';
import { toast } from 'react-toastify';
+import { useProxy } from 'valtio/utils';
+import CreateEditor from '../../../_com/createEditor';
+import SelectIconProgram from '../../../_com/selectIcon';
+import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
function CreateProgramKemiskinan() {
const programState = useProxy(programKemiskinanState);
const router = useRouter();
const [lineChart, setLineChart] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
programState.create.form = {
nama: '',
@@ -41,40 +42,46 @@ function CreateProgramKemiskinan() {
};
const handleSubmit = async () => {
- if (!programState.create.form.nama || !programState.create.form.deskripsi) {
- return toast.warn('Judul dan deskripsi wajib diisi');
- }
-
- const id = await programState.create.create();
- if (id) {
- const idStr = String(id);
- await programState.findUnique.load(idStr);
- if (programState.findUnique.data) {
- setLineChart([programState.findUnique.data]);
+ try {
+ setIsSubmitting(true);
+ if (!programState.create.form.nama || !programState.create.form.deskripsi) {
+ return toast.warn('Judul dan deskripsi wajib diisi');
}
- toast.success('Program berhasil ditambahkan');
- } else {
- toast.error('Gagal menambahkan program, coba lagi');
- }
- resetForm();
- router.push('/admin/ekonomi/program-kemiskinan');
+ const id = await programState.create.create();
+ if (id) {
+ const idStr = String(id);
+ await programState.findUnique.load(idStr);
+ if (programState.findUnique.data) {
+ setLineChart([programState.findUnique.data]);
+ }
+ toast.success('Program berhasil ditambahkan');
+ } else {
+ toast.error('Gagal menambahkan program, coba lagi');
+ }
+
+ resetForm();
+ router.push('/admin/ekonomi/program-kemiskinan');
+ } catch (error) {
+ console.error('Gagal menyimpan data:', error);
+ toast.error('Terjadi kesalahan saat menyimpan data');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan tombol back */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Program Kemiskinan
@@ -150,6 +157,17 @@ function CreateProgramKemiskinan() {
{/* Tombol Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx
index def7ec5e..36935bee 100644
--- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx
@@ -1,7 +1,7 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors';
-import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -69,11 +69,9 @@ function ListProgramKemiskinan({ search }: { search: string }) {
Daftar Program Kemiskinan
-
- } color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}>
- Tambah Baru
-
-
+ } color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}>
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
index 603c194e..51122258 100644
--- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx
@@ -1,77 +1,116 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
+import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ 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 { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
-import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import { useProxy } from 'valtio/utils';
function EditSektorUnggulanDesa() {
const router = useRouter();
const params = useParams() as { id: string };
const stateGrafik = useProxy(grafikSektorUnggulan);
-
+ const [isSubmitting, setIsSubmitting] = useState(false);
const id = params.id;
+ // state lokal buat form
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ value: 0,
+ });
+
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ description: '',
+ value: 0,
+ });
+
// Load data saat komponen mount
useEffect(() => {
if (id) {
- stateGrafik.findUnique.load(id).then(() => {
- const data = stateGrafik.findUnique.data;
- if (data) {
- stateGrafik.update.form = {
- name: data.name || '',
- description: data.description || '',
- value: data.value || 0,
- };
- }
- }).catch((err) => {
- console.error('Error load sektor unggulan:', err);
- toast.error('Gagal mengambil data sektor unggulan');
- });
+ stateGrafik.findUnique
+ .load(id)
+ .then(() => {
+ const data = stateGrafik.findUnique.data;
+ if (data) {
+ setFormData({
+ name: data.name || '',
+ description: data.description || '',
+ value: data.value || 0,
+ });
+ setOriginalData({
+ name: data.name || '',
+ description: data.description || '',
+ value: data.value || 0,
+ });
+ }
+ })
+ .catch((err) => {
+ console.error('Error load sektor unggulan:', err);
+ toast.error('Gagal mengambil data sektor unggulan');
+ });
}
}, [id]);
+ const handleChange =
+ (field: keyof typeof formData) =>
+ (e: React.ChangeEvent) => {
+ const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value;
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
stateGrafik.update.id = id;
+ stateGrafik.update.form = { ...formData }; // update global pas submit
await stateGrafik.update.submit();
toast.success('Sektor unggulan berhasil diperbarui!');
router.push('/admin/ekonomi/sektor-unggulan-desa');
} catch (error) {
console.error('Error update sektor unggulan:', error);
toast.error('Terjadi kesalahan saat memperbarui sektor unggulan');
+ } finally {
+ setIsSubmitting(false);
}
};
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ description: originalData.description,
+ value: originalData.value,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
return (
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Sektor Unggulan Desa
@@ -89,10 +128,8 @@ function EditSektorUnggulanDesa() {
{
- stateGrafik.update.form.name = val.currentTarget.value;
- }}
+ value={formData.name}
+ onChange={handleChange('name')}
required
/>
@@ -100,24 +137,33 @@ function EditSektorUnggulanDesa() {
Konten
{
- stateGrafik.update.form.description = htmlContent;
- }}
+ value={formData.description}
+ onChange={(htmlContent) =>
+ setFormData((prev) => ({ ...prev, description: htmlContent }))
+ }
/>
{
- stateGrafik.update.form.value = Number(val.currentTarget.value);
- }}
+ value={formData.value}
+ onChange={handleChange('value')}
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx
index f89351a9..ee8c1561 100644
--- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx
@@ -8,8 +8,7 @@ import {
Paper,
Skeleton,
Stack,
- Text,
- Tooltip,
+ Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -82,7 +81,7 @@ function DetailSektorUnggulanDesa() {
Deskripsi
-
+
@@ -92,36 +91,32 @@ function DetailSektorUnggulanDesa() {
{/* Tombol Aksi */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
-
- router.push(
- `/admin/ekonomi/sektor-unggulan-desa/${data.id}/edit`
- )
- }
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+
+ router.push(
+ `/admin/ekonomi/sektor-unggulan-desa/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
index 507cb066..09021f87 100644
--- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx
@@ -6,24 +6,26 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ 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 grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
import CreateEditor from '../../../_com/createEditor';
+import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
+import { toast } from 'react-toastify';
function CreateSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState([]);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateGrafik.create.form = {
@@ -34,32 +36,38 @@ function CreateSektorUnggulanDesa() {
};
const handleSubmit = async () => {
- const id = await stateGrafik.create.create();
- if (id) {
- const idStr = String(id);
- await stateGrafik.findUnique.load(idStr);
- if (stateGrafik.findUnique.data) {
- setChartData([stateGrafik.findUnique.data]);
+ try {
+ setIsSubmitting(true);
+ const id = await stateGrafik.create.create();
+ if (id) {
+ const idStr = String(id);
+ await stateGrafik.findUnique.load(idStr);
+ if (stateGrafik.findUnique.data) {
+ setChartData([stateGrafik.findUnique.data]);
+ }
}
+ resetForm();
+ router.push('/admin/ekonomi/sektor-unggulan-desa');
+ } catch (error) {
+ console.error('Error creating sektor unggulan:', error);
+ toast.error('Terjadi kesalahan saat menambahkan sektor unggulan');
+ } finally {
+ setIsSubmitting(false);
}
- resetForm();
- router.push('/admin/ekonomi/sektor-unggulan-desa');
};
return (
{/* Header dengan back button */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Sektor Unggulan Desa
@@ -109,6 +117,17 @@ function CreateSektorUnggulanDesa() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx
index 3a7cc1e8..74d1153d 100644
--- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -92,11 +91,9 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
List Sektor Unggulan Desa
-
- } color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}>
- Tambah Baru
-
-
+ } color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}>
+ Tambah Baru
+
{loading ? (
@@ -127,16 +124,14 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
-
- router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
- >
-
- Detail
-
-
+ router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
+ >
+
+ Detail
+
))
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx
index e920da92..c85ac68f 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx
@@ -2,49 +2,45 @@
'use client'
import colors from '@/con/colors';
import {
+ ScrollArea,
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
- Title,
- Tooltip,
- ScrollArea,
+ Title
} from '@mantine/core';
+import {
+ IconBuildingCommunity,
+ IconHierarchy,
+ IconUsers
+} from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import {
- IconHierarchy,
- IconUsers,
- IconGitBranch,
-} from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
- {
- label: "Posisi Organisasi",
- value: "posisiorganisasi",
- href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
- icon: ,
- tooltip: "Kelola daftar posisi organisasi",
- },
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai",
- icon: ,
- tooltip: "Kelola data pegawai BUMDesa",
+ icon:
},
{
- label: "Hubungan Organisasi",
- value: "hubunganorganisasi",
- href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi",
- icon: ,
- tooltip: "Atur hubungan antar posisi organisasi",
+ label: "Posisi Organisasi",
+ value: "posisiorganisasi",
+ href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
+ icon:
},
+ {
+ label: "Struktur Organisasi",
+ value: "strukturorganisasi",
+ href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi",
+ icon:
+ }
];
const currentTab = tabs.find((tab) => tab.href === pathname);
@@ -96,26 +92,19 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
@@ -135,7 +124,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
))}
-
+
);
}
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/[id]/page.tsx
deleted file mode 100644
index d063805e..00000000
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/[id]/page.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/* 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 strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
-
-export default function EditHubunganOrganisasi() {
- const router = useRouter();
- const { id } = useParams<{ id: string }>();
- const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
- const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
-
- const [form, setForm] = useState({
- atasanId: '',
- bawahanId: '',
- tipe: '',
- });
-
- useEffect(() => {
- strukturorganisasiState.pegawai.findMany.load();
-
- if (id) {
- state.edit.load(id).then(data => {
- if (data) {
- setForm({
- atasanId: data.atasanId,
- bawahanId: data.bawahanId,
- tipe: data.tipe || '',
- });
- }
- });
- }
- }, [id]);
-
- const handleSubmit = async () => {
- if (!form.atasanId || !form.bawahanId) {
- toast.warn("Atasan dan bawahan 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/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
- }
- };
-
- return (
-
-
-
- Edit Hubungan Organisasi
- ({
- value: p.id,
- label: p.namaLengkap,
- })) || []}
- value={form.atasanId}
- onChange={(val) => setForm({ ...form, atasanId: val || '' })}
- />
- ({
- value: p.id,
- label: p.namaLengkap,
- })) || []}
- value={form.bawahanId}
- onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
- />
- setForm({ ...form, tipe: e.currentTarget.value })}
- />
- Simpan
-
-
-
- );
-}
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/create/page.tsx
deleted file mode 100644
index 39e0a23a..00000000
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/create/page.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import { 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 strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
-
-export default function CreateHubunganOrganisasi() {
- const router = useRouter();
- const state = useProxy(strukturorganisasiState.hubunganOrganisasi);
- const pegawaiList = strukturorganisasiState.pegawai.findMany.data;
-
- const [form, setForm] = useState({
- atasanId: '',
- bawahanId: '',
- tipe: '',
- });
-
- useEffect(() => {
- strukturorganisasiState.pegawai.findMany.load();
- }, []);
-
- const handleSubmit = async () => {
- if (!form.atasanId || !form.bawahanId) {
- toast.warn("Atasan dan bawahan harus diisi");
- return;
- }
-
- state.create.form = form;
- const result = await state.create.create();
-
- if (result) {
- toast.success("Hubungan Organisasi berhasil ditambahkan");
- router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
- }
- };
-
- return (
-
-
-
- Create Hubungan Organisasi
- ({
- value: p.id,
- label: p.namaLengkap,
- })) || []}
- value={form.atasanId}
- onChange={(val) => setForm({ ...form, atasanId: val || '' })}
- />
- ({
- value: p.id,
- label: p.namaLengkap,
- })) || []}
- value={form.bawahanId}
- onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
- />
- setForm({ ...form, tipe: e.currentTarget.value })}
- />
- Simpan
-
-
-
- );
-}
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/page.tsx
deleted file mode 100644
index a4018cba..00000000
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/page.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-'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, IconTrash } from '@tabler/icons-react';
-import { useRouter } from 'next/navigation';
-import { useProxy } from 'valtio/utils';
-import HeaderSearch from '../../../_com/header';
-import JudulList from '../../../_com/judulList';
-import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
-import { useState } from 'react';
-import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
-
-function HubunganOrganisasi() {
- const [search, setSearch] = useState("");
-
- return (
-
- }
- value={search}
- onChange={(e) => setSearch(e.currentTarget.value)}
- />
-
-
- );
-}
-
-
-function ListHubunganOrganisasi({ search }: { search: string }) {
- const stateOrganisasi = useProxy(strukturorganisasiState.hubunganOrganisasi);
- const [modalHapus, setModalHapus] = useState(false);
- const [selectedId, setSelectedId] = useState(null);
- const router = useRouter();
-
- useShallowEffect(() => {
- stateOrganisasi.findMany.load();
- }, []);
-
- const handleHapus = () => {
- if (selectedId) {
- stateOrganisasi.delete.byId(selectedId);
- setModalHapus(false);
- setSelectedId(null);
- }
- };
-
- const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
- const keyword = search.toLowerCase();
- return (
- item.atasan?.namaLengkap?.toLowerCase().includes(keyword) ||
- item.bawahan?.namaLengkap?.toLowerCase().includes(keyword) ||
- item.tipe?.toLowerCase().includes(keyword)
- );
- });
-
- if (!stateOrganisasi.findMany.data) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
-
-
- Atasan
- Bawahan
- Tipe
- Edit
- Hapus
-
-
-
- {filteredData
- .sort((a, b) =>
- a.atasan?.namaLengkap.localeCompare(b.atasan?.namaLengkap)
- )
- .map((item) => (
-
- {item.atasan?.namaLengkap}
- {item.bawahan?.namaLengkap}
- {item.tipe}
-
- router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/${item.id}`)}
- >
-
-
-
-
- {
- setSelectedId(item.id);
- setModalHapus(true);
- }}
- >
-
-
-
-
- ))}
-
-
-
-
- setModalHapus(false)}
- onConfirm={handleHapus}
- text='Apakah anda yakin ingin menghapus hubungan organisasi ini?'
- />
-
- );
-}
-
-
-export default HubunganOrganisasi;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx
index 7cf73d5d..89046ef6 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx
@@ -1,12 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
-import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
+import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Select,
Stack,
@@ -21,42 +25,38 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
-interface PreviewImage {
- file?: File;
- preview: string;
-}
-
-interface PegawaiFormData {
- namaLengkap: string;
- gelarAkademik: string;
- imageId: string | null;
- tanggalMasuk: string;
- email: string;
- telepon: string;
- alamat: string;
- posisiId: string;
- isActive: boolean;
-}
-
-export default function EditPegawai() {
+export default function EditPegawaiBumDes() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
- const [previewImage, setPreviewImage] = useState(null);
- const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
- const [formData, setFormData] = useState({
- namaLengkap: "",
- gelarAkademik: "",
- imageId: "",
- tanggalMasuk: "",
- email: "",
- telepon: "",
- alamat: "",
- posisiId: "",
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [formData, setFormData] = useState({
+ namaLengkap: '',
+ gelarAkademik: '',
+ imageId: '',
+ tanggalMasuk: '',
+ email: '',
+ telepon: '',
+ alamat: '',
+ posisiId: '',
isActive: true,
});
+ const [originalData, setOriginalData] = useState({
+ namaLengkap: '',
+ gelarAkademik: '',
+ imageId: '',
+ tanggalMasuk: '',
+ email: '',
+ telepon: '',
+ alamat: '',
+ posisiId: '',
+ isActive: true,
+ imageUrl: ''
+ });
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
-
- // Format date to YYYY-MM-DD for date input
+ // Format date for
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
@@ -64,155 +64,189 @@ export default function EditPegawai() {
};
useEffect(() => {
- strukturorganisasiState.posisiOrganisasi.findMany.load();
const loadPegawai = async () => {
try {
+ await stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
+
const data = await stateOrganisasi.edit.load(id);
if (data) {
setFormData({
- 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 ?? true, // pakai nullish coalescing
+ 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 ?? true,
+ });
+ setOriginalData({
+ 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 ?? true,
+ imageUrl: data.image?.link || '',
});
- if (data.image?.link) {
- setPreviewImage(data.image.link);
- } else {
- setPreviewImage(null);
- }
+ setPreviewImage(data.image?.link || null);
}
} catch (error) {
- console.error("Error loading pegawai:", error);
- toast.error(
- error instanceof Error ? error.message : "Gagal mengambil data pegawai"
- );
+ console.error('Error loading pegawai:', error);
+ toast.error(error instanceof Error ? error.message : 'Gagal mengambil data pegawai');
}
};
loadPegawai();
}, [id]);
+ const handleResetForm = () => {
+ setFormData({
+ namaLengkap: originalData.namaLengkap,
+ gelarAkademik: originalData.gelarAkademik,
+ imageId: originalData.imageId,
+ tanggalMasuk: originalData.tanggalMasuk,
+ email: originalData.email,
+ telepon: originalData.telepon,
+ alamat: originalData.alamat,
+ posisiId: originalData.posisiId,
+ isActive: originalData.isActive,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
if (!formData.namaLengkap.trim()) {
- toast.error('Nama lengkap tidak boleh kosong');
- return;
+ return toast.error('Nama lengkap tidak boleh kosong');
}
- stateOrganisasi.edit.form = {
- namaLengkap: formData.namaLengkap.trim(),
- gelarAkademik: formData.gelarAkademik.trim(),
- imageId: formData.imageId ? formData.imageId.trim() : "",
- tanggalMasuk: formData.tanggalMasuk.trim(),
- email: formData.email.trim(),
- telepon: formData.telepon.trim(),
- alamat: formData.alamat.trim(),
- posisiId: formData.posisiId.trim(),
- isActive: formData.isActive,
- };
-
+ // Update global state only on submit
+ const updatedForm = { ...formData };
-
- if (id && !stateOrganisasi.edit.id) {
- stateOrganisasi.edit.id = id;
+ if (file) {
+ const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
+ updatedForm.imageId = uploaded.id;
}
+ stateOrganisasi.edit.form = updatedForm;
+ if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id;
+
const success = await stateOrganisasi.edit.submit();
-
- if (success) {
- router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
- }
+ if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai');
} catch (error) {
- console.error("Error updating pegawai:", error);
- toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pegawai");
+ console.error('Error updating pegawai:', error);
+ toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
+ } finally {
+ setIsSubmitting(false);
}
-
-
-
};
return (
-
-
- router.back()} variant='subtle' color={'blue'}>
-
+
+
+ router.back()} p="xs" radius="md">
+
-
+ Edit Data Pegawai PPID
+
-
-
- Edit Data Pegawai
+
+
+ {/* Nama Lengkap */}
setFormData({ ...formData, namaLengkap: e.target.value })}
+ required
/>
+
+ {/* Gelar Akademik */}
setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
-
- Gambar
-
- {
- const file = files[0]; // Hanya ambil file pertama
- if (file) {
- setPreviewImage({
- file,
- preview: URL.createObjectURL(file)
- });
- }
- }}
- maxSize={5 * 1024 ** 2} // 5MB
- accept={{
- 'image/*': ['.jpeg', '.jpg', '.png', '.webp']
- }}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- Drag images here or click to select files
-
-
- Attach as many files as you like, each file should not exceed 5mb
-
-
-
-
- {previewImage && (
+ {/* Foto Profil */}
+
+ Foto Profil
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
+
+
+
+
+ {previewImage && (
+
- )}
-
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
+
+ )}
+
+ {/* Tanggal Masuk */}
setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
+
+ {/* Email */}
(formData.email = e.currentTarget.value)}
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
+
+ {/* Telepon */}
(formData.telepon = e.currentTarget.value)}
+ onChange={(e) => setFormData({ ...formData, telepon: e.target.value })}
/>
+
+ {/* Alamat */}
(formData.alamat = e.currentTarget.value)}
- />
- ({
- value: p.id, // harus string
- label: p.nama,
- })) || []
- }
- value={formData.posisiId}
- onChange={(value) => {
- if (value !== null) {
- setFormData({ ...formData, posisiId: value }); // value harus string
- }
- }}
- />
- {
- setFormData({ ...formData, isActive: val === 'true' });
- }}
+ onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
/>
+ {/* Posisi */}
+
+ Posisi
+ ({ value: p.id, label: p.nama })) || []}
+ value={formData.posisiId}
+ onChange={(value) => value && setFormData({ ...formData, posisiId: value })}
+ searchable
+ clearable
+ />
+
-
+ {/* Status Pegawai */}
+
+ Status Pegawai
+ setFormData({ ...formData, isActive: val === 'true' })}
+ clearable
+ />
+
+
+ {/* Submit Button */}
+
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
-
+
);
}
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx
index 261a61de..0e0f0dd7 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx
@@ -1,42 +1,56 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
-import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
import colors from '@/con/colors';
-import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
-import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailPegawai() {
- const statePegawai = useProxy(strukturorganisasiState.pegawai)
- const [modalHapus, setModalHapus] = useState(false)
- const [selectedId, setSelectedId] = useState(null)
- const params = useParams()
+ const statePegawai = useProxy(stateStrukturBumDes.pegawai);
+ const [modalHapus, setModalHapus] = useState(false);
+ const [modalNonActive, setModalNonActive] = useState(false);
+ const [selectedId, setSelectedId] = useState(null);
+ const params = useParams();
const router = useRouter();
useShallowEffect(() => {
- statePegawai.findUnique.load(params?.id as string)
- }, [])
+ stateStrukturBumDes.posisiOrganisasi.findMany.load();
+ statePegawai.findUnique.load(params?.id as string);
+ }, []);
const handleHapus = () => {
if (selectedId) {
- statePegawai.delete.byId(selectedId)
- setModalHapus(false)
- setSelectedId(null)
- router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
+ statePegawai.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
- }
+ };
+
+ const handleNonActive = () => {
+ if (selectedId) {
+ statePegawai.nonActive.byId(selectedId);
+ setModalNonActive(false);
+ setSelectedId(null);
+ router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
+ }
+ };
if (!statePegawai.findUnique.data) {
return (
-
+
- )
+ );
}
+ const data = statePegawai.findUnique.data;
+
return (
@@ -44,91 +58,121 @@ function DetailPegawai() {
-
-
- Detail Pegawai
-
-
+
+
+
+ Detail Pegawai PPID
+
+
+
+
- Nama Lengkap
- {statePegawai.findUnique.data?.namaLengkap}
-
-
- Gelar Akademik
- {statePegawai.findUnique.data?.gelarAkademik}
-
-
- Image
- {statePegawai.findUnique.data?.image?.link ? (
-
- ) : (
- Tidak ada gambar
- )}
-
-
- Tanggal Masuk
-
- {statePegawai.findUnique.data?.tanggalMasuk
- ? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString()
- : "-"}
+ Nama Lengkap
+
+ {data.namaLengkap || '-'} {data.gelarAkademik || ''}
-
- Email
- {statePegawai.findUnique.data?.email}
-
-
- Telepon
- {statePegawai.findUnique.data?.telepon}
-
-
- Alamat
- {statePegawai.findUnique.data?.alamat}
-
-
- Posisi
-
- {statePegawai.findUnique.data?.posisi ? (
-
- {statePegawai.findUnique.data.posisi.nama}
-
- ) : (
-
- Tidak ada posisi
-
- )}
-
-
-
- Aktif
- {statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"}
-
-
- {
- if (statePegawai.findUnique.data) {
- setSelectedId(statePegawai.findUnique.data.id);
- setModalHapus(true);
- }
- }}
- disabled={!statePegawai.findUnique.data}
- color="red">
-
-
- {
- if (statePegawai.findUnique.data) {
- router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
- }
- }}
- disabled={!statePegawai.findUnique.data}
- color="green">
-
-
-
+ Posisi
+ {data.posisi?.nama || '-'}
+
+
+ Email
+ {data.email || '-'}
+
+
+
+ Telepon
+ {data.telepon || '-'}
+
+
+
+ Alamat
+ {data.alamat || '-'}
+
+
+
+ Tanggal Masuk
+
+ {data.tanggalMasuk ? new Date(data.tanggalMasuk).toLocaleDateString() : '-'}
+
+
+
+
+ Status
+
+ {data.isActive ? 'Aktif' : 'Tidak Aktif'}
+
+
+
+
+ Foto Profil
+ {data.image?.link ? (
+
+ ) : (
+ Tidak ada foto profil
+ )}
+
+
+ {
+ setSelectedId(data.id || null);
+ setModalNonActive(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+ {data.isActive ? "Aktif" : "Nonaktif"}
+
+
+
+ {
+ setSelectedId(data.id || null);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+ router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
@@ -139,7 +183,15 @@ function DetailPegawai() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
- text="Apakah anda yakin ingin menghapus produk ini?"
+ text="Apakah Anda yakin ingin menghapus data pegawai ini?"
+ />
+
+ {/* Modal NonActive */}
+ setModalNonActive(false)}
+ onConfirm={handleNonActive}
+ text="Apakah Anda yakin ingin menonaktifkan pegawai ini?"
/>
);
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx
index 540a3daf..c0f86fb0 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx
@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
-import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
-import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
+import { ActionIcon, Box, Button, Group, Image, Loader, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -11,17 +12,19 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
-function CreatePegawai() {
+function CreatePegawaiBumDes() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
- const stateOrganisasi = useProxy(strukturorganisasiState)
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai)
useEffect(() => {
- stateOrganisasi.posisiOrganisasi.findMany.load();
+ stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
resetForm();
}, []);
+ const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
- stateOrganisasi.pegawai.create.form = {
+ stateOrganisasi.create.form = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
@@ -30,8 +33,10 @@ function CreatePegawai() {
telepon: "",
alamat: "",
posisiId: "",
- isActive: true,
+ isActive: true,
};
+ setPreviewImage(null);
+ setFile(null);
};
const handleSubmit = async () => {
@@ -40,27 +45,30 @@ function CreatePegawai() {
}
try {
+ setIsSubmitting(true);
// Upload gambar dulu
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
const res = await ApiFetch.api.fileStorage.create.post({
- file: previewImage.file,
- name: previewImage.file.name,
+ file,
+ name: file.name,
});
-
const uploaded = res.data?.data;
if (!uploaded?.id) {
- return toast.error("Gagal upload gambar");
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
// Set status aktif secara otomatis
- stateOrganisasi.pegawai.create.form.isActive = true;
-
+ stateOrganisasi.create.form.isActive = true;
+
// Simpan ID gambar ke form
- stateOrganisasi.pegawai.create.form.imageId = uploaded.id;
-
+ stateOrganisasi.create.form.imageId = uploaded.id;
+
// Submit form
- await stateOrganisasi.pegawai.create.submit();
-
-
+ await stateOrganisasi.create.submit();
+
+
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
@@ -68,134 +76,227 @@ function CreatePegawai() {
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
-
- router.back()}>
-
+
+
+ router.back()} p="xs" radius="md">
+
-
-
-
- Create Pegawai
- (stateOrganisasi.pegawai.create.form.namaLengkap = e.currentTarget.value)}
- />
- (stateOrganisasi.pegawai.create.form.gelarAkademik = e.currentTarget.value)}
- />
-
- Gambar
-
- {
- const file = files[0]; // Hanya ambil file pertama
- if (file) {
- setPreviewImage({
- file,
- preview: URL.createObjectURL(file)
- });
- }
- }}
- maxSize={5 * 1024 ** 2} // 5MB
- accept={{
- 'image/*': ['.jpeg', '.jpg', '.png', '.webp']
- }}
- >
-
-
-
-
-
-
-
-
-
-
+
+ Tambah Pegawai BUMDesa
+
+
-
-
- Drag images here or click to select files
-
-
- Attach as many files as you like, each file should not exceed 5mb
-
-
-
-
- {previewImage && (
+
+
+
+ (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
+ required
+ />
+
+
+ (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
+ />
+
+
+
+ Foto Profil
+
+ {
+ const file = files[0];
+ if (file) {
+ setPreviewImage({
+ file,
+ preview: URL.createObjectURL(file)
+ });
+ }
+ }}
+ maxSize={5 * 1024 ** 2} // 5MB
+ accept={{
+ 'image/*': ['.jpeg', '.jpg', '.png', '.webp']
+ }}
+ styles={{
+ root: {
+ border: '2px dashed #ced4da',
+ borderRadius: '8px',
+ padding: '20px',
+ textAlign: 'center',
+ cursor: 'pointer',
+ '&:hover': {
+ borderColor: '#228be6',
+ },
+ },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar ke sini atau klik untuk memilih file
+
+
+ Format yang didukung: JPG, PNG, WebP. Maksimal 5MB
+
+
+
+
+
+ {previewImage && (
+
+
+ Preview Gambar
+
- )}
-
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
+
+ )}
+
+
+ (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
+ />
+
+
+
+ (stateOrganisasi.create.form.email = e.currentTarget.value)}
+ />
+
+
+
+ (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
+ />
+
+
+
+ (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
+ />
+
+
+
+
+ Posisi
+
+ ({
+ value: p.id,
+ label: p.nama
+ })) || []}
+ value={stateOrganisasi.create.form.posisiId}
+ onChange={(value) => {
+ if (value) stateOrganisasi.create.form.posisiId = value;
+ }}
+ searchable
+ clearable
+ />
- (stateOrganisasi.pegawai.create.form.tanggalMasuk = e.currentTarget.value)}
- />
- (stateOrganisasi.pegawai.create.form.email = e.currentTarget.value)}
- />
- (stateOrganisasi.pegawai.create.form.telepon = e.currentTarget.value)}
- />
- (stateOrganisasi.pegawai.create.form.alamat = e.currentTarget.value)}
- />
- ({
- value: p.id,
- label: p.nama
- })) || []}
- value={stateOrganisasi.pegawai.create.form.posisiId}
- onChange={(value) => {
- if (value) stateOrganisasi.pegawai.create.form.posisiId = value;
- }}
- searchable
- />
-
- Simpan
-
+
+
+ Reset
+
+
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
);
}
-export default CreatePegawai;
+export default CreatePegawaiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx
index 2a140fc7..fd17d099 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx
@@ -1,33 +1,33 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
-import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
+import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core';
+import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
-import JudulList from '../../../_com/judulList';
-import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
-function Pegawai() {
+
+function PegawaiBumDes() {
const [search, setSearch] = useState("");
return (
}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
-
+
);
}
-function ListPegawai({ search }: { search: string }) {
- const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
+function ListPegawaiBumdes({ search }: { search: string }) {
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const router = useRouter();
const {
@@ -39,21 +39,10 @@ function ListPegawai({ search }: { search: string }) {
} = stateOrganisasi.findMany;
useEffect(() => {
- load(page, 10);
- }, [page]);
+ load(page, 10, search);
+ }, [page, search]);
- const filteredData = useMemo(() => {
- if (!data) return [];
- return data.filter(item => {
- const keyword = search.toLowerCase();
- return (
- item.namaLengkap?.toLowerCase().includes(keyword) ||
- item.gelarAkademik?.toLowerCase().includes(keyword) ||
- item.telepon?.toLowerCase().includes(keyword) ||
- item.posisi?.nama?.toLowerCase().includes(keyword)
- );
- });
- }, [data, search]);
+ const filteredData = data || []
// Handle loading state
if (loading || !data) {
@@ -67,29 +56,47 @@ function ListPegawai({ search }: { search: string }) {
if (data.length === 0) {
return (
-
- Tidak ada data pegawai yang tersedia
+
+
+ Daftar Pegawai BUMDesa
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
+ >
+ Tambah Baru
+
+
+
+ Tidak ada data pegawai yang ditemukan
+
);
}
return (
-
-
+
+
+ Daftar Pegawai BUMDesa
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create')}
+ >
+ Tambah Baru
+
+
-
+
- Nama
- Gelar Akademik
- Telepon
- Posisi
- Aktif
- Detail
+ Nama Lengkap
+ Posisi
+ Status
+ Aksi
@@ -106,10 +113,20 @@ function ListPegawai({ search }: { search: string }) {
}) // Aktif di atas
).map((item) => (
- {item.namaLengkap}
- {item.gelarAkademik}
- {item.telepon}
- {item.posisi?.nama}
+
+
+
+ {item.namaLengkap}
+
+
+
+
+
+
+ {item.posisi?.nama || 'Belum diatur'}
+
+
+
@@ -131,8 +148,15 @@ function ListPegawai({ search }: { search: string }) {
- router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
-
+ }
+ onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}
+ >
+ Detail
@@ -140,21 +164,22 @@ function ListPegawai({ search }: { search: string }) {
+
+ {
+ load(newPage, 10);
+ window.scrollTo(0, 0);
+ }}
+ total={totalPages}
+ withEdges
+ withControls
+ radius="md"
+ />
+
-
- {
- load(newPage, 10);
- window.scrollTo(0, 0);
- }}
- total={totalPages}
- mt="md"
- mb="md"
- />
-
);
}
-export default Pegawai;
+export default PegawaiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx
index 508647a3..a58d838c 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx
@@ -1,30 +1,23 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
-'use client'
+'use client';
+
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
-import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
-import {
- Box,
- Button,
- Group,
- Paper,
- Stack,
- Text,
- TextInput,
- Title,
- Tooltip,
-} from '@mantine/core';
+import { Box, Button, Group, Loader, 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 EditPosisiOrganisasi() {
+function EditPosisiOrganisasiBumDes() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
- const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
+ const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
nama: '',
@@ -32,13 +25,23 @@ function EditPosisiOrganisasi() {
hierarki: 0,
});
- useEffect(() => {
- const loadPosisiOrganisasi = async () => {
- if (!id) return;
+ const [originalData, setOriginalData] = useState({
+ nama: '',
+ deskripsi: '',
+ hierarki: 0,
+ });
+ // Fungsi generik untuk update formData
+ const handleChange = (field: keyof typeof formData, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ useEffect(() => {
+ if (!id) return;
+
+ const loadPosisiOrganisasi = async () => {
try {
const data = await stateOrganisasi.edit.load(id);
-
if (data) {
stateOrganisasi.edit.id = id;
setFormData({
@@ -46,9 +49,14 @@ function EditPosisiOrganisasi() {
deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0,
});
+ setOriginalData({
+ nama: data.nama || '',
+ deskripsi: data.deskripsi || '',
+ hierarki: data.hierarki || 0,
+ });
}
- } catch (error) {
- console.error('Error loading posisi organisasi:', error);
+ } catch (err) {
+ console.error('Error loading posisi organisasi:', err);
toast.error('Gagal memuat data posisi organisasi');
}
};
@@ -56,13 +64,24 @@ function EditPosisiOrganisasi() {
loadPosisiOrganisasi();
}, [id]);
- const handleSubmit = async () => {
- try {
- if (!formData.nama.trim()) {
- toast.error('Nama posisi organisasi tidak boleh kosong');
- return;
- }
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ deskripsi: originalData.deskripsi,
+ hierarki: originalData.hierarki,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+ const handleSubmit = async () => {
+ if (!formData.nama.trim()) {
+ toast.error('Nama posisi organisasi tidak boleh kosong');
+ return;
+ }
+
+ try {
+ setIsSubmitting(true);
+ // Update global state hanya saat submit
stateOrganisasi.edit.form = {
nama: formData.nama.trim(),
deskripsi: formData.deskripsi.trim(),
@@ -70,37 +89,30 @@ function EditPosisiOrganisasi() {
};
if (!stateOrganisasi.edit.id) {
- stateOrganisasi.edit.id = id; // fallback
+ stateOrganisasi.edit.id = id;
}
const success = await stateOrganisasi.edit.update();
if (success) {
- toast.success('Posisi organisasi berhasil diperbarui!');
- 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);
+ } catch (err) {
+ console.error('Error updating posisi organisasi:', err);
+ // toast error biasanya sudah ada di update
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()} p="xs" radius="md">
+
+
- Edit Posisi Organisasi
+ Edit Posisi Organisasi BUMDes
@@ -114,42 +126,49 @@ function EditPosisiOrganisasi() {
>
- setFormData({ ...formData, nama: e.target.value })
- }
label="Nama Posisi Organisasi"
placeholder="Masukkan nama posisi organisasi"
+ value={formData.nama}
+ onChange={(e) => handleChange('nama', e.target.value)}
required
/>
-
+
Deskripsi
- setFormData({ ...formData, deskripsi: htmlContent })
- }
+ onChange={(html) => handleChange('deskripsi', html)}
/>
- setFormData({
- ...formData,
- hierarki: parseInt(e.target.value) || 0,
- })
- }
label="Hierarki"
- placeholder="Masukkan hierarki"
+ type="number"
+ min={0}
+ placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
+ value={formData.hierarki}
+ onChange={(e) => {
+ const value = parseInt(e.target.value, 10);
+ handleChange('hierarki', isNaN(value) ? 0 : value);
+ }}
required
/>
-
+
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
@@ -169,4 +188,4 @@ function EditPosisiOrganisasi() {
);
}
-export default EditPosisiOrganisasi;
+export default EditPosisiOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx
index ded7678a..1b267c36 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx
@@ -1,128 +1,133 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
-import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
-import {
- Box,
- Button,
- Group,
- Paper,
- Stack,
- Text,
- TextInput,
- Title,
- Tooltip,
-} from '@mantine/core';
+import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
-function CreatePosisiOrganisasi() {
- const router = useRouter();
- const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
+function CreatePosisiOrganisasiBumDes() {
+ const router = useRouter();
+ const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
+ const [isSubmitting, setIsSubmitting] = useState(false);
- useEffect(() => {
- stateOrganisasi.findMany.load();
- }, []);
+ useEffect(() => {
+ stateOrganisasi.findMany.load();
+ }, []);
- const resetForm = () => {
- stateOrganisasi.create.form = {
- nama: '',
- deskripsi: '',
- hierarki: 0,
- };
+ const resetForm = () => {
+ stateOrganisasi.create.form = {
+ nama: "",
+ deskripsi: "",
+ hierarki: 0,
};
+ }
- const handleSubmit = async () => {
- await stateOrganisasi.create.submit();
- resetForm();
- router.push(
- '/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
- );
- };
+ const handleSubmit = async () => {
+ setIsSubmitting(true);
+ try {
+ if (!stateOrganisasi.create.form.nama.trim()) {
+ return toast.error('Nama posisi tidak boleh kosong');
+ }
- return (
-
- {/* Header Back + Title */}
-
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
-
- Tambah Posisi Organisasi
-
-
+ await stateOrganisasi.create.submit();
+ toast.success('Posisi organisasi berhasil ditambahkan');
+ router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi');
+ } catch (error) {
+ toast.error('Gagal menambahkan posisi organisasi');
+ console.error('Error:', error);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
- {/* Form Card */}
-
+
+ router.back()} p="xs" radius="md">
+
+
+
+ Tambah Posisi Organisasi BUMDes
+
+
+
+
+
+ (stateOrganisasi.create.form.nama = e.target.value)}
+ required
+ />
+
+
+
+ Deskripsi
+
+ {
+ stateOrganisasi.create.form.deskripsi = htmlContent;
+ }}
+ />
+
+
+ {
+ const value = parseInt(e.target.value, 10);
+ stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
+ }}
+ required
+ />
+
+
+ {/* Tombol Batal */}
+
-
- (stateOrganisasi.create.form.nama = e.currentTarget.value)}
- required
- />
+ Reset
+
-
- Deskripsi
- {
- stateOrganisasi.create.form.deskripsi = htmlContent;
- }}
- />
-
-
- {
- const value = parseInt(e.currentTarget.value, 10);
- if (!isNaN(value)) {
- stateOrganisasi.create.form.hierarki = value;
- }
- }}
- required
- />
-
- {/* Action Button */}
-
-
- Simpan
-
-
-
-
-
- );
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
+
+
+
+ );
}
-export default CreatePosisiOrganisasi;
+export default CreatePosisiOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx
index 33f0cfd0..bf9dc0a2 100644
--- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx
@@ -1,58 +1,37 @@
'use client'
import colors from '@/con/colors';
-import {
- Box,
- Button,
- Center,
- Group,
- Paper,
- Pagination,
- Skeleton,
- Stack,
- Table,
- TableTbody,
- TableTd,
- TableTh,
- TableThead,
- TableTr,
- Text,
- Title,
- Tooltip,
-} from '@mantine/core';
+import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
+import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
-import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
+import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
-function PosisiOrganisasi() {
+
+function PosisiOrganisasiBumDes() {
const [search, setSearch] = useState("");
return (
- {/* Search Bar */}
}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
-
- {/* List Table */}
-
+
);
}
-function ListPosisiOrganisasi({ search }: { search: string }) {
- const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
+function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
+ const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi)
const router = useRouter();
-
- const [modalHapus, setModalHapus] = useState(false);
- const [selectedId, setSelectedId] = useState(null);
+ const [modalHapus, setModalHapus] = useState(false)
+ const [selectedId, setSelectedId] = useState(null)
const {
data,
@@ -66,21 +45,20 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
load(page, 10, search);
}, [page, search]);
- const filteredData = data || [];
-
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
- setModalHapus(false);
- setSelectedId(null);
- load(page, 10, search); // refresh
+ setModalHapus(false)
+ setSelectedId(null)
}
- };
+ }
+
+ const filteredData = data || []
if (loading || !data) {
return (
-
+
);
}
@@ -89,71 +67,64 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
- Daftar Posisi Organisasi
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- '/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
- )
- }
- >
- Tambah Baru
-
-
+ Daftar Posisi Organisasi BumDes
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create')}
+ >
+ Tambah Baru
+
-
- Nama Posisi
- Hierarki
- Edit
- Hapus
+ Nama Posisi
+ Deskripsi
+ Hierarki
+ Edit
+ Hapus
{filteredData.length > 0 ? (
filteredData.map((item) => (
-
+
{item.nama}
-
- {item.hierarki ?? '-'}
+
+
+
+
-
-
-
- router.push(
- `/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`
- )
- }
- >
-
-
-
+
+ {item.hierarki || '-'}
-
-
- {
- setSelectedId(item.id);
- setModalHapus(true);
- }}
- >
-
-
-
+
+ router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`)}
+ >
+
+
+
+
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
))
@@ -170,8 +141,6 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
-
- {/* Pagination */}
-
{/* Modal Hapus */}
setModalHapus(false)}
onConfirm={handleHapus}
- text="Apakah anda yakin ingin menghapus posisi organisasi ini?"
+ text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?"
/>
);
}
-export default PosisiOrganisasi;
+export default PosisiOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx
new file mode 100644
index 00000000..89ab5a7e
--- /dev/null
+++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable prefer-const */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable react-hooks/exhaustive-deps */
+'use client'
+import { Box, Center, Image, Loader, Paper, Stack, Text } from '@mantine/core';
+import { IconUsers } from '@tabler/icons-react';
+import { OrganizationChart } from 'primereact/organizationchart';
+import { useEffect } from 'react';
+import { useProxy } from 'valtio/utils';
+import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
+
+function StrukturOrganisasiBumDes() {
+ return (
+
+
+
+ );
+}
+
+function ListStrukturOrganisasiBumDes() {
+ const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
+
+ useEffect(() => {
+ stateOrganisasi.findMany.load();
+ }, []);
+
+ if (stateOrganisasi.findMany.loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
+ return (
+
+
+ Belum ada struktur organisasi yang ditambahkan
+
+ );
+ }
+
+ const posisiMap = new Map();
+
+ const aktifPegawai = stateOrganisasi.findMany.data.filter(p => p.isActive);
+
+ for (const pegawai of aktifPegawai) {
+ const posisiId = pegawai.posisi.id;
+ if (!posisiMap.has(posisiId)) {
+ posisiMap.set(posisiId, {
+ ...pegawai.posisi,
+ pegawaiList: [],
+ children: [],
+ });
+ }
+ posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
+ }
+
+ let root: any[] = [];
+ posisiMap.forEach((posisi) => {
+ if (posisi.parentId) {
+ const parent = posisiMap.get(posisi.parentId);
+ if (parent) {
+ parent.children.push(posisi);
+ }
+ } else {
+ root.push(posisi);
+ }
+ });
+
+ function toOrgChartFormat(node: any): any {
+ return {
+ expanded: true,
+ type: 'person',
+ styleClass: 'p-person',
+ data: {
+ name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
+ status: node.nama,
+ image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
+ },
+ children: node.children.map(toOrgChartFormat),
+ };
+ }
+
+ const chartData = root.map(toOrgChartFormat);
+
+ return (
+
+
+
+
+
+ );
+}
+
+function nodeTemplate(node: any) {
+ const imageSrc = node?.data?.image || '/img/default.png';
+ const name = node?.data?.name || 'Tanpa Nama';
+ const status = node?.data?.status || 'Tidak ada deskripsi';
+
+ return (
+
+
+ {name}
+ {status}
+
+ );
+}
+
+export default StrukturOrganisasiBumDes;
diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx
index c00259b1..51a1ab7f 100644
--- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx
@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -67,21 +67,19 @@ function DetailAjukanIdeInofativDesa() {
Detail Ajukan Ide Inovatif Desa
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- disabled={state.delete.loading}
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={state.delete.loading}
+ >
+
+
{/* Detail Data */}
@@ -94,7 +92,7 @@ function DetailAjukanIdeInofativDesa() {
Alamat
-
+
@@ -104,12 +102,12 @@ function DetailAjukanIdeInofativDesa() {
Deskripsi
-
+
Masalah
- {data?.masalah || '-'}
+ {data?.masalah || '-'}
diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx
index 51ba6814..24bdbd06 100644
--- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx
@@ -4,7 +4,6 @@ import {
Box,
Button,
Center,
- Group,
Pagination,
Paper,
Skeleton,
@@ -16,11 +15,10 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
-import { IconDeviceImac, IconSearch, IconPlus } from '@tabler/icons-react';
+import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -72,20 +70,7 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
return (
-
- Daftar Ide Inovatif
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
- >
- Tambah Baru
-
-
-
-
+ Daftar Ide Inovatif
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
index 715e3c0d..f22190da 100644
--- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx
@@ -1,10 +1,22 @@
-'use client'
+'use client';
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
-import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
+import {
+ ActionIcon,
+ Box,
+ Button,
+ Group,
+ Image,
+ Loader,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title
+} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -12,20 +24,28 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
-function EditPenghargaan() {
- const stateDesaDigital = useProxy(desaDigitalState)
- const router = useRouter()
- const params = useParams()
- const [previewImage, setPreviewImage] = useState(null)
- const [file, setFile] = useState(null)
+function EditDigitalSmartVillage() {
+ const stateDesaDigital = useProxy(desaDigitalState);
+ const router = useRouter();
+ const params = useParams();
+
+ const [previewImage, setPreviewImage] = useState(null);
+ const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: stateDesaDigital.findUnique.data?.name || '',
- deskripsi: stateDesaDigital.findUnique.data?.deskripsi || '',
- imageId: stateDesaDigital.findUnique.data?.imageId || '',
- })
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ });
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ imageUrl: '',
+ });
useEffect(() => {
- const loadPenghargaan = async () => {
+ const loadData = async () => {
const id = params?.id as string;
if (!id) return;
@@ -37,137 +57,210 @@ function EditPenghargaan() {
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
+ setOriginalData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ imageUrl: data.image?.link || '',
+ });
- if (data?.image?.link) {
- setPreviewImage(data.image.link);
- }
+ if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
- console.error("Error loading desa digital smart village:", error);
- toast.error("Gagal memuat data desa digital smart village");
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data desa digital smart village');
}
};
- loadPenghargaan();
+ loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
- stateDesaDigital.edit.form = {
- ...stateDesaDigital.edit.form,
- name: formData.name,
- deskripsi: formData.deskripsi,
- imageId: formData.imageId,
- }
+ setIsSubmitting(true);
+ stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
-
- if (!uploaded?.id) {
- return toast.error("Gagal upload gambar");
- }
+ if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateDesaDigital.edit.form.imageId = uploaded.id;
}
await stateDesaDigital.edit.update();
- toast.success("Desa digital smart village berhasil diperbarui!");
- router.push("/admin/inovasi/desa-digital-smart-village");
+ toast.success('Desa digital smart village berhasil diperbarui!');
+ router.push('/admin/inovasi/desa-digital-smart-village');
} catch (error) {
- console.error("Error updating desa digital smart village:", error);
- toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
+ console.error('Error updating desa digital:', error);
+ toast.error('Terjadi kesalahan saat memperbarui data');
+ } finally {
+ setIsSubmitting(false);
}
- }
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ imageId: originalData.imageId,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info('Form dikembalikan ke data awal');
+ };
return (
-
-
- router.back()}>
-
+
+ {/* Header */}
+
+ router.back()} p="xs" radius="md">
+
-
-
-
- Edit Desa Digital Smart Village
+
+ Edit Desa Digital Smart Village
+
+
+
+ {/* Form Card */}
+
+
+ {/* Dropzone Upload */}
+
+
+ Gambar Desa Digital
+
+ {
+ const selectedFile = files[0];
+ if (selectedFile) {
+ setFile(selectedFile);
+ setPreviewImage(URL.createObjectURL(selectedFile));
+ }
+ }}
+ onReject={() => toast.error('File tidak valid, gunakan format gambar')}
+ maxSize={5 * 1024 ** 2}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
+ radius="md"
+ p="xl"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Seret gambar atau klik untuk memilih file
+
+
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
+
+
+
+
+
+ {previewImage && (
+
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
+
+ )}
+
+
+ {/* Input Judul */}
setFormData({ ...formData, name: e.target.value })}
- label={Judul }
- placeholder="masukkan judul"
+ required
/>
-
- Gambar
-
- {
- const selectedFile = files[0]; // Ambil file pertama
- if (selectedFile) {
- setFile(selectedFile);
- setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
- }
- }}
- onReject={() => toast.error('File tidak valid.')}
- maxSize={5 * 1024 ** 2} // Maks 5MB
- accept={{ 'image/*': [] }}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- Drag gambar ke sini atau klik untuk pilih file
-
-
- Maksimal 5MB dan harus format gambar
-
-
-
-
-
- {/* Tampilkan preview kalau ada */}
- {previewImage && (
-
-
-
- )}
-
-
+ {/* Editor Deskripsi */}
- Deskripsi
+
+ Deskripsi
+
{
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- stateDesaDigital.edit.form.deskripsi = htmlContent;
- }}
+ onChange={(htmlContent) =>
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
/>
- Simpan
+ {/* Tombol Simpan */}
+
+
+ Batal
+
+
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
);
}
-export default EditPenghargaan;
+export default EditDigitalSmartVillage;
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx
index 1fcbdbeb..236f2b17 100644
--- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx
@@ -1,8 +1,17 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
+import {
+ Box,
+ Button,
+ Group,
+ Image,
+ Paper,
+ Skeleton,
+ Stack,
+ Text
+} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
-import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
+import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,95 +19,132 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function DetailDesaDigital() {
- const stateDesaDigital = useProxy(desaDigitalState)
+ const stateDesaDigital = useProxy(desaDigitalState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState(null);
- const router = useRouter()
- const params = useParams()
+ const router = useRouter();
+ const params = useParams();
useShallowEffect(() => {
- stateDesaDigital.findUnique.load(params?.id as string)
- }, [])
+ stateDesaDigital.findUnique.load(params?.id as string);
+ }, []);
const handleHapus = () => {
if (selectedId) {
- stateDesaDigital.delete.byId(selectedId)
- setModalHapus(false)
- setSelectedId(null)
- router.push("/admin/inovasi/desa-digital-smart-village")
+ stateDesaDigital.delete.byId(selectedId);
+ setModalHapus(false);
+ setSelectedId(null);
+ router.push("/admin/inovasi/desa-digital-smart-village");
}
- }
+ };
if (!stateDesaDigital.findUnique.data) {
return (
-
+
- )
+ );
}
+ const data = stateDesaDigital.findUnique.data;
+
return (
-
-
- router.back()}>
-
-
-
-
-
- Detail Desa Digital Smart Village
- {stateDesaDigital.findUnique.data ? (
-
-
-
- Judul
- {stateDesaDigital.findUnique.data?.name}
-
-
- Deskripsi
-
-
-
- Gambar
-
-
-
- {
- if (stateDesaDigital.findUnique.data) {
- setSelectedId(stateDesaDigital.findUnique.data.id);
- setModalHapus(true);
- }
- }}
- disabled={stateDesaDigital.delete.loading || !stateDesaDigital.findUnique.data}
- color={"red"}
- >
-
-
- {
- if (stateDesaDigital.findUnique.data) {
- router.push(`/admin/inovasi/desa-digital-smart-village/${stateDesaDigital.findUnique.data.id}/edit`);
- }
- }}
- disabled={!stateDesaDigital.findUnique.data}
- color={"green"}
- >
-
-
-
-
-
- ) : null}
+
+ {/* Tombol Kembali */}
+ router.back()}
+ leftSection={ }
+ mb={15}
+ >
+ Kembali
+
+
+ {/* Card Utama */}
+
+
+
+ Detail Desa Digital Smart Village
+
+
+ {/* Sub Card Detail */}
+
+
+
+ Judul
+ {data?.name || '-'}
+
+
+
+ Deskripsi
+
+
+
+
+ Gambar
+ {data?.image?.link ? (
+
+ ) : (
+ Tidak ada gambar
+ )}
+
+
+ {/* Tombol Aksi */}
+
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+ router.push(`/admin/inovasi/desa-digital-smart-village/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
+
+
+
- {/* Modal Konfirmasi Hapus */}
+ {/* Modal Konfirmasi */}
setModalHapus(false)}
onConfirm={handleHapus}
- text='Apakah anda yakin ingin menghapus desa digital smart village ini?'
+ text="Apakah Anda yakin ingin menghapus desa digital smart village ini?"
/>
);
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
index 289651f0..74e1bd82 100644
--- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx
@@ -2,31 +2,33 @@
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
- Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
+import ExifOrientationImg from 'react-exif-orientation-img';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
-import { Dropzone } from '@mantine/dropzone';
-function CreateDesaDigital() {
+export default function CreateDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState);
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateDesaDigital.create.form = {
@@ -44,7 +46,7 @@ function CreateDesaDigital() {
}
try {
- // Upload gambar dulu
+ setIsSubmitting(true);
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
@@ -55,10 +57,8 @@ function CreateDesaDigital() {
return toast.error('Gagal mengunggah gambar');
}
- // Set imageId ke form
stateDesaDigital.create.form.imageId = uploaded.id;
- // Submit form
const success = await stateDesaDigital.create.create();
if (success) {
resetForm();
@@ -67,45 +67,55 @@ function CreateDesaDigital() {
} catch (error) {
console.error('Error in handleSubmit:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
- {/* Header */}
-
-
- router.back()} p="xs" radius="md">
-
-
-
+ {/* Header dengan tombol kembali */}
+
+ router.back()}
+ p="xs"
+ radius="md"
+ style={{ transition: 'background 0.2s ease' }}
+ >
+
+
Tambah Desa Digital Smart Village
- {/* Card */}
+ {/* Card Form */}
-
- {/* Nama */}
+
+ {/* Input Nama */}
(stateDesaDigital.create.form.name = e.target.value)}
- required
+ radius="md"
+ withAsterisk
/>
{/* Deskripsi */}
-
+
Deskripsi
-
- Gambar
+
+ Gambar Desa Digital
{
@@ -131,9 +141,14 @@ function CreateDesaDigital() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
+ style={{
+ border: '2px dashed #cfd8dc',
+ backgroundColor: '#fafafa',
+ transition: 'background-color 0.2s ease, border-color 0.2s ease',
+ }}
>
@@ -153,24 +168,61 @@ function CreateDesaDigital() {
{/* Preview */}
{previewImage && (
-
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
{/* Tombol Submit */}
+ {/* Tombol Batal */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
@@ -189,5 +241,3 @@ function CreateDesaDigital() {
);
}
-
-export default CreateDesaDigital;
diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx
index f1c1f484..72d12073 100644
--- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -68,18 +67,16 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
List Desa Digital Smart Village
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/inovasi/desa-digital-smart-village/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/inovasi/desa-digital-smart-village/create')
+ }
+ >
+ Tambah Baru
+
@@ -97,17 +94,21 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
filteredData.map((item) => (
-
- {item.name}
-
+
+
+ {item.name}
+
+
-
+
+
+
(null);
const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: stateInfoTekno.findUnique.data?.name || '',
- deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '',
- imageId: stateInfoTekno.findUnique.data?.imageId || '',
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ });
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ deskripsi: '',
+ imageId: '',
+ imageUrl: '',
});
+ // Load data pertama kali
useEffect(() => {
const id = params?.id as string;
if (!id) return;
- const loadPenghargaan = async () => {
+ const loadData = async () => {
try {
const data = await stateInfoTekno.edit.load(id);
if (data) {
@@ -49,7 +58,12 @@ function EditInfoTeknologiTepatGuna() {
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
-
+ setOriginalData({
+ name: data.name || '',
+ deskripsi: data.deskripsi || '',
+ imageId: data.imageId || '',
+ imageUrl: data.image?.link || '',
+ });
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
@@ -58,16 +72,31 @@ function EditInfoTeknologiTepatGuna() {
}
};
- loadPenghargaan();
+ loadData();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ imageId: originalData.imageId,
+ });
+ setFile(null);
+ setPreviewImage(originalData.imageUrl);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ // Submit form
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // sync local → global pas submit
stateInfoTekno.edit.form = {
...stateInfoTekno.edit.form,
...formData,
};
+ // upload file kalau ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -88,6 +117,8 @@ function EditInfoTeknologiTepatGuna() {
} catch (error) {
console.error('Error updating info teknologi tepat guna:', error);
toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -95,11 +126,9 @@ function EditInfoTeknologiTepatGuna() {
{/* Tombol back + title */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Info Teknologi Tepat Guna
@@ -120,7 +149,7 @@ function EditInfoTeknologiTepatGuna() {
label="Judul"
placeholder="Masukkan judul info teknologi tepat guna"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
@@ -139,7 +168,7 @@ function EditInfoTeknologiTepatGuna() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -158,25 +187,45 @@ function EditInfoTeknologiTepatGuna() {
Seret gambar atau klik untuk memilih file
- Maksimal 5MB, format gambar wajib
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && (
-
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -188,15 +237,25 @@ function EditInfoTeknologiTepatGuna() {
{
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- stateInfoTekno.edit.form.deskripsi = htmlContent;
- }}
+ onChange={(htmlContent) =>
+ setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
+ }
/>
{/* Tombol submit */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx
index 4313fcaa..1ddee92a 100644
--- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx
@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -54,7 +54,7 @@ function DetailInfoTeknologiTepatGuna() {
{/* Card Utama */}
-
+
Judul
@@ -78,6 +78,7 @@ function DetailInfoTeknologiTepatGuna() {
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
@@ -100,35 +101,31 @@ function DetailInfoTeknologiTepatGuna() {
{/* Action Buttons */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- disabled={stateInfoTekno.delete.loading}
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={stateInfoTekno.delete.loading}
+ >
+
+
-
-
- router.push(`/admin/inovasi/info-teknologi-tepat-guna/${data.id}/edit`)
- }
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+
+ router.push(`/admin/inovasi/info-teknologi-tepat-guna/${data.id}/edit`)
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
index d2d09028..644e1e8d 100644
--- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx
@@ -2,17 +2,19 @@
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
+import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -20,13 +22,13 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno';
-import { Dropzone } from '@mantine/dropzone';
function CreateInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState);
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateInfoTekno.create.form = {
@@ -44,6 +46,7 @@ function CreateInfoTeknologiTepatGuna() {
}
try {
+ setIsSubmitting(true);
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name,
@@ -65,6 +68,8 @@ function CreateInfoTeknologiTepatGuna() {
} catch (error) {
console.error('Error in handleSubmit:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -72,11 +77,9 @@ function CreateInfoTeknologiTepatGuna() {
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Info Teknologi Tepat Guna
@@ -131,7 +134,7 @@ function CreateInfoTeknologiTepatGuna() {
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -152,7 +155,7 @@ function CreateInfoTeknologiTepatGuna() {
{previewImage && (
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
{/* Submit Button */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx
index 3fcb8f8a..fe766900 100644
--- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx
@@ -4,6 +4,7 @@ import {
Box,
Button,
Center,
+ Group,
Pagination,
Paper,
Skeleton,
@@ -15,9 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Group,
- Tooltip,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -68,18 +67,16 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
Daftar Info Teknologi Tepat Guna
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/inovasi/info-teknologi-tepat-guna/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/inovasi/info-teknologi-tepat-guna/create')
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx
index 781f1ec5..c208cdac 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
-import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
+import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
+import { IconListDetails, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
-import { IconListDetails, IconUsers } from '@tabler/icons-react';
function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -15,14 +15,12 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi",
- tooltip: "Lihat daftar kolaborasi inovasi",
icon: ,
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi",
- tooltip: "Kelola mitra kolaborasi",
icon: ,
}
];
@@ -73,25 +71,18 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
index 24827165..531aca12 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx
@@ -8,13 +8,14 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from "@mantine/core";
+import { YearPickerInput } from "@mantine/dates";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -25,16 +26,25 @@ function EditKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState);
const router = useRouter();
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: kolaborasiState.update.form.name || "",
- deskripsi: kolaborasiState.update.form.deskripsi || "",
- tahun: kolaborasiState.update.form.tahun || "",
- slug: kolaborasiState.update.form.slug || "",
- kolaborator: kolaborasiState.update.form.kolaborator || "",
+ name: "",
+ deskripsi: "",
+ tahun: "",
+ slug: "",
+ kolaborator: "",
});
- // Load data
+ const [originalData, setOriginalData] = useState({
+ name: "",
+ deskripsi: "",
+ tahun: "",
+ slug: "",
+ kolaborator: "",
+ });
+
+ // Load data awal dari server
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
@@ -44,12 +54,19 @@ function EditKolaborasiInovasi() {
const data = await kolaborasiState.update.load(id);
if (data) {
setFormData({
- name: data.name || "",
- deskripsi: data.deskripsi || "",
- tahun: data.tahun || "",
- slug: data.slug || "",
- kolaborator: data.kolaborator || "",
+ name: data.name ?? "",
+ deskripsi: data.deskripsi ?? "",
+ tahun: data.tahun?.toString() ?? "",
+ slug: data.slug ?? "",
+ kolaborator: data.kolaborator ?? "",
});
+ setOriginalData({
+ name: data.name ?? "",
+ deskripsi: data.deskripsi ?? "",
+ tahun: data.tahun?.toString() ?? "",
+ slug: data.slug ?? "",
+ kolaborator: data.kolaborator ?? "",
+ })
}
} catch (error) {
console.error("Error loading kolaborasi:", error);
@@ -60,8 +77,21 @@ function EditKolaborasiInovasi() {
loadKolaborasi();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ tahun: originalData.tahun?.toString(),
+ slug: originalData.slug,
+ kolaborator: originalData.kolaborator,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
+ // Handler submit → baru update global state
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
kolaborasiState.update.form = {
...kolaborasiState.update.form,
name: formData.name,
@@ -70,23 +100,29 @@ function EditKolaborasiInovasi() {
slug: formData.slug,
kolaborator: formData.kolaborator,
};
+
await kolaborasiState.update.submit();
toast.success("Kolaborasi inovasi berhasil diperbarui!");
- router.push("/admin/inovasi/kolaborasi-inovasi");
+ router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
} catch (error) {
console.error("Error updating kolaborasi:", error);
toast.error("Terjadi kesalahan saat memperbarui data");
+ } finally {
+ setIsSubmitting(false);
}
};
+ // Handler input (biar lebih DRY)
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Kolaborasi Inovasi
@@ -105,7 +141,7 @@ function EditKolaborasiInovasi() {
label="Nama Kolaborasi"
placeholder="Masukkan nama kolaborasi"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange("name", e.target.value)}
required
/>
@@ -113,23 +149,26 @@ function EditKolaborasiInovasi() {
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={formData.slug}
- onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
+ onChange={(e) => handleChange("slug", e.target.value)}
required
/>
- setFormData({ ...formData, tahun: e.target.value })}
- required
+ Tahun}
+ placeholder="Pilih tahun"
+ onChange={(date) => {
+ const year = date ? new Date(date).getFullYear() : 0;
+ handleChange("tahun", year.toString());
+ }}
/>
setFormData({ ...formData, kolaborator: e.target.value })}
+ onChange={(e) => handleChange("kolaborator", e.target.value)}
required
/>
@@ -139,25 +178,34 @@ function EditKolaborasiInovasi() {
{
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- kolaborasiState.update.form.deskripsi = htmlContent;
- }}
+ onChange={(htmlContent) => handleChange("deskripsi", htmlContent)}
/>
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx
index cc4d06f5..577f963f 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx
@@ -1,5 +1,5 @@
'use client'
-import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -81,12 +81,12 @@ function DetailKolaborasiInovasi() {
Deskripsi Singkat
- {data?.slug || '-'}
+ {data?.slug || '-'}
Deskripsi
-
+
@@ -96,33 +96,29 @@ function DetailKolaborasiInovasi() {
{/* Tombol aksi */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- disabled={kolaborasiState.delete.loading}
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={kolaborasiState.delete.loading}
+ >
+
+
-
- router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${data.id}/edit`)}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
index a45d50ad..deca2518 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx
@@ -3,17 +3,18 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors';
-import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { YearPickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateCreate.create.form = {
@@ -40,6 +41,7 @@ function CreateProgramKreatifDesa() {
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
@@ -47,6 +49,8 @@ function CreateProgramKreatifDesa() {
} catch (error) {
console.error("Error creating kolaborasi inovasi:", error);
toast.error("Terjadi kesalahan saat menyimpan data");
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -54,11 +58,9 @@ function CreateProgramKreatifDesa() {
{/* Back Button */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Kolaborasi Inovasi
@@ -111,6 +113,17 @@ function CreateProgramKreatifDesa() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx
index f0899d36..576ea0c4 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx
@@ -17,15 +17,14 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
-import HeaderSearch from '../../../_com/header';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
-import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
import { useProxy } from 'valtio/utils';
+import HeaderSearch from '../../../_com/header';
+import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
function KolaborasiInovasi() {
const [search, setSearch] = useState('');
@@ -68,16 +67,14 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
Daftar Kolaborasi Inovasi
-
- }
- color="blue"
- variant="light"
- onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
index ac9c1ea2..ce4516a7 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx
@@ -4,17 +4,18 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Center,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import {
@@ -33,17 +34,29 @@ function EditMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const params = useParams();
+
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Local form state (controlled)
const [formData, setFormData] = useState({
- name: state.update.form.name || '',
- imageId: state.update.form.imageId || '',
+ name: '',
+ imageId: '',
});
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ imageId: '',
+ imageUrl: '',
+ });
+
+ // Load data ke state lokal sekali saja
useEffect(() => {
- const loadFoto = async () => {
+ const loadData = async () => {
const id = params?.id as string;
if (!id) return;
+
try {
const data = await state.update.load(id);
if (data) {
@@ -51,25 +64,47 @@ function EditMitraKolaborasi() {
name: data.name || '',
imageId: data.imageId || '',
});
+ setOriginalData({
+ name: data.name || '',
+ imageId: data.imageId || '',
+ imageUrl: data.image?.link || '',
+ });
+
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
- console.error('Error loading foto:', error);
- toast.error('Gagal memuat data foto');
+ console.error('Error loading data:', error);
+ toast.error('Gagal memuat data mitra');
}
};
- loadFoto();
+
+ loadData();
}, [params?.id]);
+ const handleChange = (key: string, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [key]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ imageId: originalData.imageId,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
- state.update.form = {
- ...state.update.form,
- name: formData.name,
- imageId: formData.imageId,
- };
+ setIsSubmitting(true);
+ // upload file jika ada
+ let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -79,14 +114,24 @@ function EditMitraKolaborasi() {
if (!uploaded?.id) {
return toast.error('Gagal upload gambar');
}
- state.update.form.imageId = uploaded.id;
+ imageId = uploaded.id;
}
+
+ // update global state hanya saat submit
+ state.update.form = {
+ ...state.update.form,
+ name: formData.name,
+ imageId,
+ };
+
await state.update.update();
toast.success('Mitra berhasil diperbarui!');
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
} catch (error) {
console.error('Error updating mitra:', error);
toast.error('Terjadi kesalahan saat memperbarui mitra');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -94,11 +139,9 @@ function EditMitraKolaborasi() {
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Edit Mitra
@@ -119,7 +162,7 @@ function EditMitraKolaborasi() {
label="Nama Mitra"
placeholder="Masukkan nama mitra"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange('name', e.target.value)}
required
/>
@@ -138,7 +181,7 @@ function EditMitraKolaborasi() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -157,7 +200,7 @@ function EditMitraKolaborasi() {
Seret gambar atau klik untuk memilih file
- Maksimal 5MB, format gambar wajib
+ Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
@@ -165,7 +208,7 @@ function EditMitraKolaborasi() {
{/* Preview Foto */}
{previewImage ? (
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
) : (
@@ -187,6 +248,17 @@ function EditMitraKolaborasi() {
{/* Submit */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
index 2c9a077c..195c1ab2 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx
@@ -3,16 +3,17 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -26,6 +27,7 @@ function CreateMitraKolaborasi() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
state.create.form = {
@@ -37,35 +39,41 @@ function CreateMitraKolaborasi() {
};
const handleSubmit = async () => {
- if (!file) {
- return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true);
+ if (!file) {
+ return toast.warn('Silakan pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengunggah gambar, silakan coba lagi');
+ }
+
+ state.create.form.imageId = uploaded.id;
+ await state.create.create();
+ resetForm();
+ router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
+ } catch (error) {
+ console.error("Error creating mitra kolaborasi:", error);
+ toast.error("Terjadi kesalahan saat menambahkan mitra kolaborasi");
+ } finally {
+ setIsSubmitting(false);
}
-
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
-
- const uploaded = res.data?.data;
- if (!uploaded?.id) {
- return toast.error('Gagal mengunggah gambar, silakan coba lagi');
- }
-
- state.create.form.imageId = uploaded.id;
- await state.create.create();
- resetForm();
- router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
};
return (
{/* Back Button + Title */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Mitra Kolaborasi
@@ -105,7 +113,7 @@ function CreateMitraKolaborasi() {
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
@@ -126,7 +134,7 @@ function CreateMitraKolaborasi() {
{previewImage && (
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
{/* Submit Button */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx
index a2d6cbbb..9fde4fb1 100644
--- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx
@@ -18,16 +18,15 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
-import { IconEdit, IconSearch, IconX, IconPlus } from '@tabler/icons-react';
+import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
-import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
+import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
function MitraKolaborasi() {
const [search, setSearch] = useState('');
@@ -81,20 +80,18 @@ function ListMitraKolaborasi({ search }: { search: string }) {
Daftar Mitra Kolaborasi
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- '/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
- )
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
+ )
+ }
+ >
+ Tambah Baru
+
@@ -135,39 +132,35 @@ function ListMitraKolaborasi({ search }: { search: string }) {
-
- {
- setSelectedId(item.id);
- setModalHapus(true);
- }}
- >
-
-
-
+ {
+ setSelectedId(item.id);
+ setModalHapus(true);
+ }}
+ >
+
+
-
-
- router.push(
- `/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
- )
- }
- >
-
-
-
+
+ router.push(
+ `/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
+ )
+ }
+ >
+
+
))
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx
index 55a8cdb2..b12c50be 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx
@@ -8,17 +8,16 @@ import {
TabsList,
TabsPanel,
TabsTab,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
-import { usePathname, useRouter } from 'next/navigation';
-import React, { useEffect, useState } from 'react';
import {
+ IconAlertCircle,
IconFileText,
IconListDetails,
- IconMessage,
- IconAlertCircle
+ IconMessage
} from '@tabler/icons-react';
+import { usePathname, useRouter } from 'next/navigation';
+import React, { useEffect, useState } from 'react';
function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -30,29 +29,25 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }
label: "Administrasi Online",
value: "administrasionline",
href: "/admin/inovasi/layanan-online-desa/administrasi-online",
- icon: ,
- tooltip: "Kelola administrasi online desa"
+ icon:
},
{
label: "Jenis Layanan",
value: "jenislayanan",
href: "/admin/inovasi/layanan-online-desa/jenis-layanan",
- icon: ,
- tooltip: "Daftar jenis layanan desa"
+ icon:
},
{
label: "Pengaduan Masyarakat",
value: "pengaduanmasyarakat",
href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat",
- icon: ,
- tooltip: "Laporan pengaduan masyarakat"
+ icon:
},
{
label: "Jenis Pengaduan",
value: "jenispengaduan",
href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan",
- icon: ,
- tooltip: "Kategori/jenis pengaduan masyarakat"
+ icon:
}
];
@@ -103,25 +98,18 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx
index b08d0582..72f2ddd8 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/administrasi-online/[id]/page.tsx
@@ -1,6 +1,6 @@
'use client'
-import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -53,20 +53,18 @@ function DetailAdministrasiOnline() {
Kembali
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
{/* Konten Detail */}
{
const loadJenisLayanan = async () => {
const id = params?.id as string;
if (!id) return;
+
try {
const data = await state.edit.load(id);
if (data) {
setFormData({
- nama: data.nama,
- deskripsi: data.deskripsi,
+ nama: data.nama ?? '',
+ deskripsi: data.deskripsi ?? '',
+ });
+ setOriginalData({
+ nama: data.nama ?? '',
+ deskripsi: data.deskripsi ?? '',
});
}
} catch (error) {
- console.error("Error loading jenis layanan:", error);
- toast.error("Gagal memuat data jenis layanan");
+ console.error('Error loading jenis layanan:', error);
+ toast.error('Gagal memuat data jenis layanan');
}
};
+
loadJenisLayanan();
}, [params?.id]);
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ deskripsi: originalData.deskripsi,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
state.edit.form = {
...state.edit.form,
- nama: formData.nama,
- deskripsi: formData.deskripsi,
- }
- await state.edit.update()
- toast.success("Jenis layanan berhasil diperbarui!")
- router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
+ ...formData,
+ };
+
+ await state.edit.update();
+ toast.success('Jenis layanan berhasil diperbarui!');
+ router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
} catch (error) {
- console.error("Error updating jenis layanan:", error);
- toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
+ console.error('Error updating jenis layanan:', error);
+ toast.error('Terjadi kesalahan saat memperbarui jenis layanan');
+ } finally {
+ setIsSubmitting(false);
}
- }
+ };
return (
-
-
- router.back()}>
-
+
+ {/* Header */}
+
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
-
-
-
- Edit Jenis Layanan
+
+ Edit Jenis Layanan
+
+
+
+ {/* Form Container */}
+
+
+ {/* Input: Nama Jenis Layanan */}
{
- setFormData({ ...formData, nama: val.target.value });
- }}
- label={Nama Jenis Layanan }
- placeholder="masukkan nama jenis layanan"
+ onChange={(e) =>
+ setFormData((prev) => ({ ...prev, nama: e.target.value }))
+ }
+ required
/>
+
+ {/* Input: Deskripsi (Rich Text Editor) */}
- Deskripsi
+
+ Deskripsi
+
{
- setFormData({ ...formData, deskripsi: htmlContent });
- }}
+ onChange={(htmlContent) =>
+ setFormData((prev) => ({
+ ...prev,
+ deskripsi: htmlContent,
+ }))
+ }
/>
- Simpan
+
+ {/* Tombol Submit */}
+
+
+ Batal
+
+
+ {/* Tombol Simpan */}
+
+ {isSubmitting ? : 'Simpan'}
+
+
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx
index 427aaada..babc24f3 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx
@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
-import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -79,38 +79,35 @@ function DetailJenisLayanan() {
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
+ style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
{/* Tombol aksi */}
-
- {
- setSelectedId(data.id)
- setModalHapus(true)
- }}
- variant="light"
- radius="md"
- size="md"
- disabled={state.delete.loading}
- >
-
-
-
+ {
+ setSelectedId(data.id)
+ setModalHapus(true)
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={state.delete.loading}
+ >
+
+
-
- router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${data.id}/edit`)}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${data.id}/edit`)}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx
index 50d76bc1..4dc93703 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx
@@ -6,21 +6,23 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateJenisLayanan() {
const router = useRouter();
const statePasar = useProxy(layananonlineDesa.jenisLayanan);
+ const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
statePasar.findMany.load();
@@ -34,25 +36,31 @@ function CreateJenisLayanan() {
};
const handleSubmit = async () => {
- await statePasar.create.create();
- resetForm();
- router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
+ try {
+ setIsSubmitting(true);
+ await statePasar.create.create();
+ resetForm();
+ router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
+ } catch (error) {
+ console.error('Error creating jenis layanan:', error);
+ toast.error('Terjadi kesalahan saat menambahkan jenis layanan');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header dengan tombol back */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Jenis Layanan
@@ -88,6 +96,17 @@ function CreateJenisLayanan() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx
index ba176cd6..a1ea1da1 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -68,20 +67,18 @@ function ListJenisLayanan({ search }: { search: string }) {
Daftar Jenis Layanan
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- '/admin/inovasi/layanan-online-desa/jenis-layanan/create'
- )
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/layanan-online-desa/jenis-layanan/create'
+ )
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx
index 6444952c..5331bc9e 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx
@@ -6,11 +6,11 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,11 +23,17 @@ function EditJenisPengaduan() {
const params = useParams();
const id = params?.id as string;
const state = useProxy(layananonlineDesa.jenisPengaduan);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- nama: "",
+ nama: '',
});
+ const [originalData, setOriginalData] = useState({
+ nama: '',
+ });
+
+ // Load data sekali aja
useEffect(() => {
const loadJenisPengaduan = async () => {
if (!id) return;
@@ -36,68 +42,86 @@ function EditJenisPengaduan() {
const data = await state.edit.load(id);
if (data) {
- // pastikan id-nya masuk ke state edit
- state.edit.id = id;
+ state.edit.id = id; // inject id ke state global (hanya sekali)
setFormData({
nama: data.nama || '',
});
+ setOriginalData({
+ nama: data.nama || '',
+ });
}
} catch (error) {
- console.error("Error loading jenis pengaduan:", error);
- toast.error("Gagal memuat data jenis pengaduan");
+ console.error('Error loading jenis pengaduan:', error);
+ toast.error('Gagal memuat data jenis pengaduan');
}
};
loadJenisPengaduan();
}, [id]);
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ nama: originalData.nama,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
+ if (!formData.nama.trim()) {
+ toast.error('Nama jenis pengaduan tidak boleh kosong');
+ return;
+ }
+
+ // Update ke global state HANYA pas submit
+ state.edit.form = {
+ nama: formData.nama.trim(),
+ };
+
+ // Safety fallback kalau ID belum ada
+ if (!state.edit.id) {
+ state.edit.id = id;
+ }
+
try {
- if (!formData.nama.trim()) {
- toast.error('Nama jenis pengaduan tidak boleh kosong');
- return;
- }
-
- state.edit.form = {
- nama: formData.nama.trim(),
- };
-
- // Safety check tambahan: pastikan ID tidak kosong
- if (!state.edit.id) {
- state.edit.id = id; // fallback
- }
-
+ setIsSubmitting(true);
const success = await state.edit.update();
if (success) {
- router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan");
+ router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
}
} catch (error) {
- console.error("Error updating jenis pengaduan:", error);
- // toast akan ditampilkan dari fungsi update
+ console.error('Error updating jenis pengaduan:', error);
+ // toast ditangani di dalam state.update
+ } finally {
+ setIsSubmitting(false);
}
};
return (
- {/* Header + tombol back */}
+ {/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Jenis Pengaduan
- {/* Card Form */}
+ {/* Form */}
setFormData({ ...formData, nama: e.target.value })}
+ onChange={(e) => handleChange('nama', e.target.value)}
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
required
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx
index e3175ede..9167a525 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx
@@ -6,21 +6,22 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
TextInput,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
+import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateJenisPengaduan() {
const router = useRouter();
const state = useProxy(layananonlineDesa.jenisPengaduan);
-
+ const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
state.findMany.load();
}, []);
@@ -32,20 +33,26 @@ function CreateJenisPengaduan() {
};
const handleSubmit = async () => {
- await state.create.create();
- resetForm();
- router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
+ try {
+ setIsSubmitting(true);
+ await state.create.create();
+ resetForm();
+ router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
+ } catch (error) {
+ console.error('Error creating jenis pengaduan:', error);
+ toast.error('Terjadi kesalahan saat menambahkan jenis pengaduan');
+ } finally {
+ setIsSubmitting(false);
+ }
};
return (
{/* Header */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Jenis Pengaduan
@@ -70,6 +77,17 @@ function CreateJenisPengaduan() {
/>
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx
index 4bbee3b6..ecab5a98 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -79,20 +78,18 @@ function ListJenisPengaduan({ search }: { search: string }) {
Daftar Jenis Pengaduan
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- '/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
- )
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
+ )
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx
index abcbb91f..9ab1109e 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx
@@ -1,10 +1,10 @@
'use client'
-import { useProxy } from 'valtio/utils';
-import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
+import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
@@ -102,7 +102,7 @@ function DetailPengaduanMasyarakat() {
Deskripsi Pengaduan
-
+
@@ -129,21 +129,19 @@ function DetailPengaduanMasyarakat() {
{/* Action Button */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- disabled={pengaduanState.pengaduanMasyarakat.delete.loading}
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ disabled={pengaduanState.pengaduanMasyarakat.delete.loading}
+ >
+
+
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
index 11140fff..429bc44f 100644
--- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
@@ -99,18 +98,16 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
{item.nomorTelepon}
-
- }
- onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
- >
- Detail
-
-
+ }
+ onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
+ >
+ Detail
+
))
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
index fc9b335f..294d25b0 100644
--- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx
@@ -1,18 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
+import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -20,7 +21,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
-import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
interface FormProgramKreatif {
name: string;
@@ -33,6 +33,7 @@ function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState);
const params = useParams();
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
@@ -41,6 +42,16 @@ function EditProgramKreatifDesa() {
icon: '',
});
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ deskripsi: '',
+ slug: '',
+ icon: '',
+ });
+
+ const [isDataChanged, setIsDataChanged] = useState(false);
+
+ // Load data hanya sekali berdasarkan params.id
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
@@ -50,18 +61,14 @@ function EditProgramKreatifDesa() {
const data = await stateProgramKreatif.update.load(id);
if (data) {
stateProgramKreatif.update.id = id;
- stateProgramKreatif.update.form = {
- name: data.name,
- slug: data.slug,
- deskripsi: data.deskripsi,
- icon: data.icon,
+ const loadedData = {
+ name: data.name || '',
+ slug: data.slug || '',
+ deskripsi: data.deskripsi || '',
+ icon: data.icon || '',
};
- setFormData({
- name: data.name,
- slug: data.slug,
- deskripsi: data.deskripsi,
- icon: data.icon,
- });
+ setFormData(loadedData);
+ setOriginalData(loadedData);
}
} catch (error) {
console.error('Error loading program kreatif:', error);
@@ -72,31 +79,97 @@ function EditProgramKreatifDesa() {
loadProgramKreatif();
}, [params?.id]);
+ // Deteksi perubahan data
+ useEffect(() => {
+ const hasChanged =
+ formData.name !== originalData.name ||
+ formData.slug !== originalData.slug ||
+ formData.deskripsi !== originalData.deskripsi ||
+ formData.icon !== originalData.icon;
+
+ setIsDataChanged(hasChanged);
+ }, [formData, originalData]);
+
+ // Prevent browser back/refresh jika ada perubahan
+ useEffect(() => {
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ if (isDataChanged) {
+ e.preventDefault();
+ e.returnValue = '';
+ }
+ };
+
+ window.addEventListener('beforeunload', handleBeforeUnload);
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload);
+ }, [isDataChanged]);
+
+ const handleChange =
+ (field: keyof FormProgramKreatif) =>
+ (value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleBackClick = () => {
+ if (isDataChanged) {
+ const confirmed = window.confirm(
+ 'Anda memiliki perubahan yang belum disimpan. Apakah Anda yakin ingin keluar dari halaman ini? Semua perubahan akan hilang.'
+ );
+ if (confirmed) {
+ router.back();
+ }
+ } else {
+ router.back();
+ }
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ slug: originalData.slug,
+ deskripsi: originalData.deskripsi,
+ icon: originalData.icon,
+ });
+ setOriginalData({
+ ...originalData,
+ });
+ toast.info("Form dikembalikan ke data awal");
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
stateProgramKreatif.update.form = {
- ...stateProgramKreatif.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
icon: formData.icon.trim(),
};
await stateProgramKreatif.update.submit();
+
+ // Reset isDataChanged agar tidak muncul konfirmasi setelah save
+ setOriginalData(formData);
+ setIsDataChanged(false);
+
router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) {
console.error('Error updating program kreatif:', error);
- toast.error('Gagal memuat data program kreatif');
+ toast.error('Gagal menyimpan program kreatif');
+ } finally {
+ setIsSubmitting(false);
}
};
return (
-
- router.back()} p="xs" radius="md">
-
-
-
+
+
+
Edit Program Kreatif Desa
@@ -115,7 +188,7 @@ function EditProgramKreatifDesa() {
label="Nama Program Kreatif Desa"
placeholder="Masukkan nama program kreatif desa"
value={formData.name}
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
+ onChange={(e) => handleChange('name')(e.target.value)}
required
/>
@@ -123,7 +196,7 @@ function EditProgramKreatifDesa() {
label="Deskripsi Singkat Program Kreatif Desa"
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={formData.slug}
- onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
+ onChange={(e) => handleChange('slug')(e.target.value)}
required
/>
@@ -133,10 +206,7 @@ function EditProgramKreatifDesa() {
{
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- stateProgramKreatif.update.form.deskripsi = htmlContent;
- }}
+ onChange={handleChange('deskripsi')}
/>
@@ -146,14 +216,22 @@ function EditProgramKreatifDesa() {
{
- setFormData((prev) => ({ ...prev, icon: value }));
- stateProgramKreatif.update.form.icon = value;
- }}
+ onChange={handleChange('icon')}
/>
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
@@ -173,4 +251,4 @@ function EditProgramKreatifDesa() {
);
}
-export default EditProgramKreatifDesa;
+export default EditProgramKreatifDesa;
\ No newline at end of file
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx
index 94dd1015..f681a429 100644
--- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx
@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
-import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -86,49 +86,45 @@ function DetailProgramKreatifDesa() {
Deskripsi Singkat
- {data?.slug || '-'}
+ {data?.slug || '-'}
Deskripsi
-
+
-
- {
- if (data) {
- setSelectedId(data.id);
- setModalHapus(true);
- }
- }}
- disabled={stateProgramKreatif.delete.loading || !data}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ if (data) {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }
+ }}
+ disabled={stateProgramKreatif.delete.loading || !data}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
- {
- if (data) {
- router.push(`/admin/inovasi/program-kreatif-desa/${data.id}/edit`);
- }
- }}
- disabled={!data}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ if (data) {
+ router.push(`/admin/inovasi/program-kreatif-desa/${data.id}/edit`);
+ }
+ }}
+ disabled={!data}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
index 883529be..b1505836 100644
--- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx
@@ -4,23 +4,26 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
-import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../../../_com/selectIcon';
+import programKreatifState from '../../../_state/inovasi/program-kreatif';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
stateCreate.create.form = {
@@ -32,20 +35,29 @@ function CreateProgramKreatifDesa() {
};
const handleSubmit = async () => {
- await stateCreate.create.create();
- resetForm();
- router.push("/admin/inovasi/program-kreatif-desa");
+ try {
+ const success = await stateCreate.create.create();
+
+ if (success) {
+ resetForm();
+ router.push("/admin/inovasi/program-kreatif-desa");
+ }
+ } catch (error) {
+ console.error("Error creating program kreatif desa:", error);
+ toast.error("Gagal menambahkan program kreatif desa");
+ } finally {
+ setIsSubmitting(false);
+ }
};
+
return (
{/* Tombol kembali */}
-
- router.back()} p="xs" radius="md">
-
-
-
+ router.back()} p="xs" radius="md">
+
+
Tambah Program Kreatif Desa
@@ -100,6 +112,17 @@ function CreateProgramKreatifDesa() {
{/* Tombol Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx
index 5ead6694..7b7a647b 100644
--- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx
+++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx
@@ -18,8 +18,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip
+ Title
} from '@mantine/core';
import {
IconCash,
@@ -117,20 +116,18 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
Daftar Program Kreatif Desa
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- '/admin/inovasi/program-kreatif-desa/create'
- )
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/program-kreatif-desa/create'
+ )
+ }
+ >
+ Tambah Baru
+
@@ -164,20 +161,18 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
>
Daftar Program Kreatif Desa
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push(
- '/admin/inovasi/program-kreatif-desa/create'
- )
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push(
+ '/admin/inovasi/program-kreatif-desa/create'
+ )
+ }
+ >
+ Tambah Baru
+
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
index d11e7981..898470c5 100644
--- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx
@@ -1,18 +1,18 @@
-/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import {
+ ActionIcon,
Box,
Button,
Center,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from "@mantine/core";
import {
IconArrowBack,
@@ -39,13 +39,21 @@ function EditKeamananLingkungan() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: keamananState.edit.form.name || "",
- deskripsi: keamananState.edit.form.deskripsi || "",
- imageId: keamananState.edit.form.imageId || "",
+ name: "",
+ deskripsi: "",
+ imageId: "",
});
- // Load data by id
+ const [originalData, setOriginalData] = useState({
+ name: "",
+ deskripsi: "",
+ imageId: "",
+ imageUrl: "",
+ });
+
+ // Load data sekali pas mount
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
@@ -59,6 +67,12 @@ function EditKeamananLingkungan() {
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
});
+ setOriginalData({
+ name: data.name || "",
+ deskripsi: data.deskripsi || "",
+ imageId: data.imageId || "",
+ imageUrl: data.image?.link || "",
+ });
if (data?.image?.link) {
setPreviewImage(data.image.link);
@@ -71,16 +85,28 @@ function EditKeamananLingkungan() {
};
loadData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]);
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ deskripsi: originalData.deskripsi,
+ imageId: originalData.imageId,
+ });
+ setPreviewImage(originalData.imageUrl || null);
+ setFile(null);
+ toast.info("Form dikembalikan ke data awal");
+ };
+
const handleSubmit = async () => {
try {
- keamananState.edit.form = {
- ...keamananState.edit.form,
- name: formData.name,
- deskripsi: formData.deskripsi,
- imageId: formData.imageId,
- };
+ setIsSubmitting(true);
+ let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
@@ -93,15 +119,25 @@ function EditKeamananLingkungan() {
return toast.error("Gagal upload gambar");
}
- keamananState.edit.form.imageId = uploaded.id;
+ imageId = uploaded.id;
}
+ // update global state hanya sekali pas submit
+ keamananState.edit.form = {
+ ...keamananState.edit.form,
+ name: formData.name,
+ deskripsi: formData.deskripsi,
+ imageId,
+ };
+
await keamananState.edit.update();
toast.success("Keamanan Lingkungan berhasil diperbarui!");
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal");
} catch (error) {
console.error("Error updating keamananLingkungan:", error);
toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan");
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -109,16 +145,14 @@ function EditKeamananLingkungan() {
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Edit Keamanan Lingkungan
@@ -186,7 +220,39 @@ function EditKeamananLingkungan() {
{previewImage ? (
-
+
+
+
+ {/* Tombol hapus (pojok kanan atas) */}
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
+
) : (
@@ -195,9 +261,7 @@ function EditKeamananLingkungan() {
- setFormData({ ...formData, name: e.target.value })
- }
+ onChange={(e) => handleChange("name", e.target.value)}
label="Judul Keamanan Lingkungan"
placeholder="Masukkan judul"
required
@@ -209,25 +273,34 @@ function EditKeamananLingkungan() {
{
- setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
- keamananState.edit.form.deskripsi = htmlContent;
- }}
+ onChange={(htmlContent) => handleChange("deskripsi", htmlContent)}
/>
+ {/* Tombol Batal */}
+
+ Batal
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx
index 50f49d8c..55b8629b 100644
--- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx
@@ -1,7 +1,7 @@
'use client'
import { useProxy } from 'valtio/utils';
-import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
+import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -88,41 +88,37 @@ function DetailKeamananLingkungan() {
Deskripsi
-
+
{/* Aksi */}
-
- {
- setSelectedId(data.id);
- setModalHapus(true);
- }}
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+ {
+ setSelectedId(data.id);
+ setModalHapus(true);
+ }}
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
-
-
- router.push(
- `/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${data.id}/edit`
- )
- }
- variant="light"
- radius="md"
- size="md"
- >
-
-
-
+
+ router.push(
+ `/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${data.id}/edit`
+ )
+ }
+ variant="light"
+ radius="md"
+ size="md"
+ >
+
+
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
index ea5c2604..bdf82527 100644
--- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx
@@ -2,16 +2,17 @@
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
+ ActionIcon,
Box,
Button,
Group,
Image,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import {
@@ -32,6 +33,7 @@ function CreateKeamananLingkungan() {
const [previewImage, setPreviewImage] = useState(null);
const [file, setFile] = useState(null);
const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false)
const resetForm = () => {
keamananState.create.form = {
@@ -44,43 +46,49 @@ function CreateKeamananLingkungan() {
};
const handleSubmit = async () => {
- if (!file) {
- return toast.warn('Pilih file gambar terlebih dahulu');
+ try {
+ setIsSubmitting(true)
+ if (!file) {
+ return toast.warn('Pilih file gambar terlebih dahulu');
+ }
+
+ const res = await ApiFetch.api.fileStorage.create.post({
+ file,
+ name: file.name,
+ });
+
+ const uploaded = res.data?.data;
+
+ if (!uploaded?.id) {
+ return toast.error('Gagal mengupload file');
+ }
+
+ keamananState.create.form.imageId = uploaded.id;
+
+ await keamananState.create.create();
+
+ resetForm();
+ router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal');
+ } catch (error) {
+ console.error(error);
+ toast.error('Gagal menambahkan data keamanan lingkungan');
+ } finally {
+ setIsSubmitting(false)
}
-
- const res = await ApiFetch.api.fileStorage.create.post({
- file,
- name: file.name,
- });
-
- const uploaded = res.data?.data;
-
- if (!uploaded?.id) {
- return toast.error('Gagal mengupload file');
- }
-
- keamananState.create.form.imageId = uploaded.id;
-
- await keamananState.create.create();
-
- resetForm();
- router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal');
};
return (
{/* Header */}
-
- router.back()}
- p="xs"
- radius="md"
- >
-
-
-
+ router.back()}
+ p="xs"
+ radius="md"
+ >
+
+
Tambah Data Keamanan Lingkungan
@@ -111,7 +119,7 @@ function CreateKeamananLingkungan() {
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
- accept={{ 'image/*': [] }}
+ accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
>
{previewImage && (
-
+
+ {
+ setPreviewImage(null);
+ setFile(null);
+ }}
+ style={{
+ boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
+ }}
+ >
+
+
)}
@@ -196,6 +222,17 @@ function CreateKeamananLingkungan() {
{/* Tombol Submit */}
+
+ Reset
+
+
+ {/* Tombol Simpan */}
- Simpan
+ {isSubmitting ? : 'Simpan'}
diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx
index f4bef5c6..e9791329 100644
--- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx
@@ -16,8 +16,7 @@ import {
TableThead,
TableTr,
Text,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -78,18 +77,16 @@ function ListKeamananLingkungan({ search }: { search: string }) {
{/* Judul + Tombol Tambah */}
Daftar Keamanan Lingkungan
-
- }
- color="blue"
- variant="light"
- onClick={() =>
- router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create')
- }
- >
- Tambah Baru
-
-
+ }
+ color="blue"
+ variant="light"
+ onClick={() =>
+ router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create')
+ }
+ >
+ Tambah Baru
+
{/* Tabel */}
@@ -107,12 +104,16 @@ function ListKeamananLingkungan({ search }: { search: string }) {
filteredData.map((item) => (
-
- {item.name}
-
+
+
+ {item.name}
+
+
-
+
+
+
,
- tooltip: "Lihat dan kelola kontak darurat keamanan",
+ icon:
},
{
label: "Kontak Darurat Item",
value: "kontak-darurat-item",
href: "/admin/keamanan/kontak-darurat/kontak-darurat-item",
- icon: ,
- tooltip: "Kelola data kontak darurat item",
+ icon:
}
];
@@ -73,19 +71,18 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}}
>
{tabs.map((tab, i) => (
-
-
- {tab.label}
-
-
+
+ {tab.label}
+
))}
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
index df6c56a8..b348d8ab 100644
--- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
+++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx
@@ -8,12 +8,12 @@ import {
Box,
Button,
Group,
+ Loader,
Paper,
Stack,
Text,
TextInput,
- Title,
- Tooltip,
+ Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -25,13 +25,21 @@ function EditKontakItem() {
const router = useRouter();
const kontakState = useProxy(kontakDarurat.kontakDaruratItem);
const params = useParams();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
- name: kontakState.update.form.nama || '',
- nomorTelepon: kontakState.update.form.nomorTelepon || '',
- icon: kontakState.update.form.icon || '',
+ name: '',
+ nomorTelepon: '',
+ icon: '',
});
+ const [originalData, setOriginalData] = useState({
+ name: '',
+ nomorTelepon: '',
+ icon: '',
+ });
+
+ // Load data sekali dari global state
useEffect(() => {
const loadKontakDarurat = async () => {
const id = params?.id as string;
@@ -45,6 +53,11 @@ function EditKontakItem() {
nomorTelepon: data.nomorTelepon || '',
icon: data.icon || '',
});
+ setOriginalData({
+ name: data.nama || '',
+ nomorTelepon: data.nomorTelepon || '',
+ icon: data.icon || '',
+ });
}
} catch (error) {
console.error('Error loading kontak darurat:', error);
@@ -55,20 +68,41 @@ function EditKontakItem() {
loadKontakDarurat();
}, [params?.id]);
+ const handleChange = (field: keyof typeof formData, value: string) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleResetForm = () => {
+ setFormData({
+ name: originalData.name,
+ nomorTelepon: originalData.nomorTelepon,
+ icon: originalData.icon,
+ });
+ toast.info('Form dikembalikan ke data awal');
+ };
+
const handleSubmit = async () => {
try {
+ setIsSubmitting(true);
+ // Update global state sekali pas submit
kontakState.update.form = {
...kontakState.update.form,
nama: formData.name,
nomorTelepon: formData.nomorTelepon,
icon: formData.icon,
};
+
await kontakState.update.update();
toast.success('Kontak Darurat berhasil diperbarui!');
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item');
} catch (error) {
console.error('Error updating kontak darurat:', error);
toast.error('Terjadi kesalahan saat memperbarui kontak darurat');
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -76,11 +110,9 @@ function EditKontakItem() {
{/* Header */}
-
- router.back()} p="xs" radius="md">
-