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..e571b0b1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 791da768..472efe96 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.5", "private": true, "scripts": { - "dev": "bun --bun next dev --hostname 0.0.0.0", + "dev": "bun --bun next dev", "build": "bun --bun next build", "start": "bun --bun next start" }, @@ -39,19 +39,25 @@ "@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", "dayjs": "^1.11.13", + "dotenv": "^17.2.3", "elysia": "^1.3.5", "embla-carousel-autoplay": "^8.5.2", "embla-carousel-react": "^7.1.0", + "extract-zip": "^2.0.1", "form-data": "^4.0.2", "framer-motion": "^12.23.5", "get-port": "^7.1.0", @@ -67,6 +73,7 @@ "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", @@ -78,6 +85,7 @@ "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..b79f3928 100644 --- a/prisma/data/user/roles.json +++ b/prisma/data/user/roles.json @@ -1,29 +1,23 @@ [ { - "id": "1", + "id": "role-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" + "isActive": true }, { - "id": "2", + "id": "role-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" + "isActive": true }, { - "id": "3", + "id": "role-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" + "isActive": true } ] \ No newline at end of file diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json index 2f44c667..eea2a98a 100644 --- a/prisma/data/user/users.json +++ b/prisma/data/user/users.json @@ -1,32 +1,23 @@ [ - { - "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": "user-1", + "nama": "Admin Desa", + "nomor": "089647037426", + "roleId": "role-1", + "isActive": true + }, + { + "id": "user-2", + "nama": "Admin Kesehatan", + "nomor": "082339004198", + "roleId": "role-2", + "isActive": true + }, + { + "id": "user-3", + "nama": "Admin Sekolah", + "nomor": "085237157222", + "roleId": "role-3", + "isActive": true + } +] 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..a52a0cb1 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 @@ -286,49 +287,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 +853,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 +1170,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 +1344,7 @@ model PasarDesa { harga Int rating Float alamatUsaha String + kontak String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1382,6 +1387,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 +1397,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 +1606,7 @@ model Pembiayaan { ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan") } -// ========================================= INOVASI ========================================= // +// ========================================= MENU INOVASI ========================================= // // ========================================= DESA DIGITAL / SMART VILLAGE ========================================= // model DesaDigital { id String @id @default(cuid()) @@ -2093,6 +2087,9 @@ model DataPerpustakaan { updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) isActive Boolean @default(true) + + // relasi baru ke peminjaman + peminjamanBuku PeminjamanBuku[] } model KategoriBuku { @@ -2105,6 +2102,31 @@ 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 { diff --git a/prisma/seed.ts b/prisma/seed.ts index ab793335..52de4324 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -31,14 +31,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"; @@ -56,6 +56,9 @@ import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-prog 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 { safeSeedUnique } from "./safeseedUnique"; (async () => { // =========== USER & ROLE =========== @@ -63,23 +66,14 @@ import fileStorage from "./data/file-storage.json"; // =========== 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, - }, + await safeSeedUnique("role", { id: r.id }, { + name: r.name, + description: r.description, + permissions: r.permissions, + isActive: r.isActive, }); } + console.log("βœ… Roles seeded"); // =========== USERS =========== @@ -95,22 +89,12 @@ import fileStorage from "./data/file-storage.json"; continue; } - await prisma.user.upsert({ - where: { id: u.id }, - update: { - username: u.nama, - nomor: u.nomor, - roleId: u.roleId, + await safeSeedUnique("user", { id: u.id }, { + 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"); @@ -364,6 +348,7 @@ import fileStorage from "./data/file-storage.json"; jumlah: l.jumlah, }, create: { + id: l.id, name: l.name, jumlah: l.jumlah, }, @@ -823,28 +808,34 @@ 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 +864,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 +887,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 +1164,26 @@ 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)/_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/ekonomi/lowongan-kerja.ts b/src/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja.ts index 0481bbca..227c6b8c 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({ @@ -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/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/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/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts index b2d28e0e..be6b2e7f 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts @@ -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(() => ({})); diff --git a/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts b/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts index d8866b37..7337d64c 100644 --- a/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts +++ b/src/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa.ts @@ -332,7 +332,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/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/permohonan_informasi_publik/permohonanInformasiPublik.ts b/src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts index 363998cf..56b734d9 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 @@ -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..92373c3e 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 @@ -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/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)/desa/berita/kategori-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx index e42a0071..4ffdd64a 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 + Tooltip, } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,7 +25,7 @@ function EditKategoriBerita() { const params = useParams(); const [formData, setFormData] = useState({ - name: editState.update.form.name || '', + name: '', }); useEffect(() => { @@ -48,8 +49,16 @@ function EditKategoriBerita() { loadKategori(); }, [params?.id]); + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ + ...prev, + [e.target.name]: e.target.value, + })); + }; + const handleSubmit = async () => { try { + // update global state hanya saat submit editState.update.form = { ...editState.update.form, name: formData.name, @@ -94,10 +103,11 @@ function EditKategoriBerita() { > setFormData({ ...formData, name: e.target.value })} + onChange={handleChange} required /> 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..db9e2b6a 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,10 +7,9 @@ import { Group, Paper, Stack, - Text, TextInput, Title, - Tooltip, + Tooltip } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -62,9 +61,9 @@ function CreateKategoriBerita() { > Nama Kategori Berita} + label="Nama Kategori Berita" placeholder="Masukkan nama kategori berita" - value={createState.create.form.name || ''} + defaultValue={createState.create.form.name || ''} onChange={(e) => (createState.create.form.name = e.target.value)} required /> 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..c34049cc 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 @@ -19,7 +19,12 @@ import { Tooltip, } 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 +38,17 @@ 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 + // Load kategori + berita useEffect(() => { beritaState.kategoriBerita.findMany.load(); + const loadBerita = async () => { const id = params?.id as string; if (!id) return; @@ -71,8 +77,13 @@ function EditBerita() { loadBerita(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { + // Update global state hanya sekali di sini beritaState.berita.edit.form = { ...beritaState.berita.edit.form, ...formData, @@ -103,6 +114,7 @@ function EditBerita() { return ( + {/* Header */} @@ -103,7 +117,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,7 +126,7 @@ 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 && ( @@ -135,7 +149,7 @@ function EditVideo() { setFormData({ ...formData, deskripsi: val })} + onChange={(val) => handleChange('deskripsi', val)} /> 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..65a579a0 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/page.tsx @@ -102,6 +102,7 @@ function DetailVideo() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> ) : ( Tidak ada deskripsi 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..9badd230 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx @@ -80,7 +80,7 @@ function CreateVideo() { { videoState.create.form.name = e.currentTarget.value; }} @@ -91,7 +91,7 @@ function CreateVideo() { setLink(e.currentTarget.value)} required /> 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..bd56c1c3 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, + Paper, + Select, + Stack, + TextInput, + Title, + Tooltip, } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -20,159 +20,159 @@ 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, - }); + // State lokal form + const [formData, setFormData] = useState({ + nama: '', + nik: '', + alamat: '', + nomorKk: '', + kategoriId: '', + }); - useEffect(() => { - stateLayananDesa.suratKeterangan.findManyAll.load(); - const loadAjukan = async () => { - const id = params?.id as string; - if (!id) return; + // Load data awal + useEffect(() => { + stateLayananDesa.suratKeterangan.findManyAll.load(); - 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'); - } - }; + const loadAjukan = async () => { + const id = params?.id as string; + if (!id) return; - loadAjukan(); - }, [params?.id]); - - 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'); + 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'); + } }; - return ( - - {/* Back Button */} - - - - - - Edit Ajukan Permohonan - - + loadAjukan(); + }, [params?.id]); - { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + 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'); + } + }; + + return ( + + {/* Back Button */} + + + + + + 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: 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 + + + + + + ); } 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..7d10ba08 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,14 @@ 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, + Tooltip } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -79,7 +79,7 @@ function DetailAjukanPermohonan() { Nama - + {data?.nama || '-'} @@ -97,7 +97,7 @@ function DetailAjukanPermohonan() { Alamat - + {data?.alamat || '-'} 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/[id]/page.tsx similarity index 65% rename from src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/edit/page.tsx rename to src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx index eed1a9a3..7f26b666 100644 --- 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/[id]/page.tsx @@ -1,9 +1,20 @@ -'use client' /* 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, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { + Box, + Button, + Group, + Paper, + Stack, + Text, + TextInput, + Title, + Tooltip, +} from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -12,17 +23,22 @@ 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 || '', - }) + const params = useParams(); + const statePendudukNonPermanent = useProxy( + stateLayananDesa.pelayananPendudukNonPermanen + ); + const [formData, setFormData] = useState({ + name: '', + deskripsi: '', + }); + + // Load data sekali dari backend useEffect(() => { - const loadPelayananPerizinan = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; + try { const data = await statePendudukNonPermanent.update.load(id); if (data) { @@ -32,27 +48,48 @@ function EditPelayananPendudukNonPermanent() { }); } } catch (error) { - console.error("Error loading pelayanan perizinan berusaha:", error); - toast.error("Gagal memuat data pelayanan perizinan berusaha"); + console.error('Error loading data:', error); + toast.error('Gagal memuat data pelayanan penduduk non permanent'); } }; - loadPelayananPerizinan(); + + loadData(); }, [params?.id]); + const handleChange = + (field: keyof typeof formData) => + (value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + 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') - } + 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'); + }; + return ( - @@ -62,7 +99,7 @@ function EditPelayananPendudukNonPermanent() { - setFormData({ ...formData, name: e.target.value }) - } + onChange={(e) => handleChange('name')(e.target.value)} required /> - {/* Posisi Field */} + {/* Deskripsi Field */} Deskripsi { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - }} + onChange={handleChange('deskripsi')} /> @@ -104,7 +137,9 @@ function EditPelayananPendudukNonPermanent() { loading={statePendudukNonPermanent.update.loading} disabled={!formData.name} > - {statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'} + {statePendudukNonPermanent.update.loading + ? 'Menyimpan...' + : 'Simpan Perubahan'} @@ -67,9 +115,9 @@ function EditPelayananPerizinanBerusaha() { - {/* Form Section */} + {/* Form */} Edit Pelayanan Perizinan Berusaha - {/* Nama Field */} setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleChange('name')(e.target.value)} required /> - {/* Link Field */} setFormData({ ...formData, link: e.target.value })} + onChange={(e) => handleChange('link')(e.target.value)} /> - {/* Deskripsi Field */} Deskripsi setFormData({ ...formData, deskripsi: val })} + onChange={handleChange('deskripsi')} /> - {/* Action Buttons */} 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..3edb28cd 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 { @@ -19,35 +20,58 @@ import { Tooltip, } from '@mantine/core'; import { IconEdit } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import stateLayananDesa from '../../../_state/desa/layananDesa'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; +import { useRouter } from 'next/navigation'; 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 ( @@ -69,7 +93,7 @@ function PerizinanBerusaha() { radius="md" onClick={() => router.push( - '/admin/desa/layanan/pelayanan_perizinan_berusaha/edit' + `/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}` ) } > @@ -101,6 +125,7 @@ function PerizinanBerusaha() { ta="justify" fz={{ base: '1rem', md: '1.2rem' }} dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> (null); - const [previewImage2, setPreviewImage2] = useState(null); - const [file, setFile] = useState(null); - const [file2, setFile2] = useState(null); + // state lokal untuk form const [formData, setFormData] = useState({ - name: stateSurat.edit.form.name, - deskripsi: stateSurat.edit.form.deskripsi, - imageId: stateSurat.edit.form.imageId, - image2Id: stateSurat.edit.form.image2Id, + name: '', + deskripsi: '', + imageId: '', + image2Id: '', }); + // state file upload + const [file, setFile] = useState(null); + const [file2, setFile2] = useState(null); + + // state preview gambar + const [previewImage, setPreviewImage] = useState(null); + const [previewImage2, setPreviewImage2] = useState(null); + + // load data awal useEffect(() => { const loadSurat = async () => { const id = params?.id as string; @@ -46,33 +51,39 @@ function EditSuratKeterangan() { try { const data = await stateSurat.edit.load(id); - if (data) { - setFormData({ - name: data.name || '', - deskripsi: data.deskripsi || '', - imageId: data.imageId || '', - image2Id: data.image2Id || '', - }); + if (!data) return; - setPreviewImage(data.image?.link || null); - setPreviewImage2(data.image2?.link || null); - } + setFormData((prev) => ({ + ...prev, + ...{ + name: prev.name || data.name || "", + deskripsi: prev.deskripsi || data.deskripsi || "", + imageId: prev.imageId || data.imageId || "", + image2Id: prev.image2Id || data.image2Id || "", + }, + })); + + if (data.image?.link && !previewImage) setPreviewImage(data.image.link); + if (data.image2?.link && !previewImage2) setPreviewImage2(data.image2.link); } catch (error) { - console.error('Error loading surat:', error); - toast.error('Gagal memuat data surat'); + console.error("Error loading surat:", error); + toast.error("Gagal memuat data surat"); } }; loadSurat(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [params?.id]); - const handleSubmit = async () => { - try { - stateSurat.edit.form = { - ...stateSurat.edit.form, - ...formData, - }; + + // handler untuk submit + const handleSubmit = useCallback(async () => { + try { + // update form global hanya saat submit + stateSurat.edit.form = { ...stateSurat.edit.form, ...formData }; + + // upload file 1 if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const uploaded = res.data?.data; @@ -80,6 +91,7 @@ function EditSuratKeterangan() { stateSurat.edit.form.imageId = uploaded.id; } + // upload file 2 if (file2) { const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name }); const uploaded = res.data?.data; @@ -94,7 +106,7 @@ function EditSuratKeterangan() { console.error('Error updating surat:', error); toast.error('Terjadi kesalahan saat memperbarui surat'); } - }; + }, [formData, file, file2, router, stateSurat.edit]); return ( @@ -119,28 +131,32 @@ function EditSuratKeterangan() { style={{ border: '1px solid #e0e0e0' }} > + {/* Input nama */} setFormData({ ...formData, name: e.target.value })} + onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))} required /> + {/* Input deskripsi */} Konten setFormData({ ...formData, deskripsi: htmlContent })} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> {/* Upload Gambar 1 */} - Gambar Konten Pelayanan + Gambar Konten Pelayanan { 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..59d992ac 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 @@ -95,6 +95,7 @@ function DetailSuratKeterangan() { dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-', }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> 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..ba52a647 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 @@ -106,9 +106,9 @@ function CreateSuratKeterangan() { {/* Nama Surat */} (stateSurat.create.form.name = val.target.value)} - label={Nama Surat Keterangan} + label="Nama Surat Keterangan" placeholder="Masukkan nama surat keterangan" required /> 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..e4948edf 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 @@ -119,6 +119,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..27b89bdb 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 @@ -8,17 +8,15 @@ import { Group, Paper, Stack, - Text, TextInput, Title, - Tooltip, + Tooltip } 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); @@ -26,22 +24,24 @@ function EditPelayananTelunjukSakti() { const params = useParams(); const [formData, setFormData] = useState({ - name: stateTelunjukDesa.edit.form.name, - deskripsi: stateTelunjukDesa.edit.form.deskripsi, - link: stateTelunjukDesa.edit.form.link, + 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 ?? '', }); } } catch (error) { @@ -49,9 +49,19 @@ 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 })); + }, + [] + ); + + // Submit: update global state hanya saat simpan const handleSubmit = async () => { try { stateTelunjukDesa.edit.form = { @@ -95,27 +105,25 @@ 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 */} 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..04b7a37f 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 @@ -103,6 +103,7 @@ function DetailPelayananTelunjukSakti() { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', + wordBreak: "break-word", }} > {data.link} @@ -124,6 +125,7 @@ function DetailPelayananTelunjukSakti() { dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-', }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> 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..2fd23ace 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 @@ -8,15 +8,14 @@ import { Group, Paper, Stack, - Text, TextInput, Title, - Tooltip, + Tooltip } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreatePelayananTelunjukDesa() { const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); @@ -68,33 +67,35 @@ function CreatePelayananTelunjukDesa() { {/* Nama */} { stateTelunjukDesa.create.form.name = val.target.value; }} - label={Nama Pelayanan} + label="Nama Pelayanan" placeholder="Masukkan nama pelayanan telunjuk sakti desa" required /> {/* Deskripsi */} { stateTelunjukDesa.create.form.deskripsi = val.target.value; }} - label={Deskripsi} - placeholder="Masukkan deskripsi pelayanan" + label="Judul Link" + placeholder="Masukkan judul link" + required /> {/* Link */} { stateTelunjukDesa.create.form.link = val.target.value; }} - label={Link} + label="Link" placeholder="Masukkan link pelayanan" + required /> {/* Tombol 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..03752f68 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 @@ -261,7 +261,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..2f68bc4e 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/edit/page.tsx @@ -24,18 +24,22 @@ 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); + + // Lokal formData + const [formData, setFormData] = useState({ + name: '', + juara: '', + deskripsi: '', + imageId: '', + }); + + // Load data pertama kali useEffect(() => { const loadPenghargaan = async () => { const id = params?.id as string; @@ -56,43 +60,43 @@ function EditPenghargaan() { } } } 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]); + // Submit const handleSubmit = async () => { try { + // Sync ke global state saat submit statePenghargaan.edit.form = { ...statePenghargaan.edit.form, - name: formData.name, - juara: formData.juara, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - } + ...formData, + }; + // Upload file baru (kalau ada) 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"); + 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'); } - } + }; return ( @@ -123,7 +127,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 +136,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 /> @@ -182,7 +186,11 @@ function EditPenghargaan() { src={previewImage} alt="Preview Gambar" radius="md" - style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} + style={{ + maxHeight: 220, + objectFit: 'contain', + border: `1px solid ${colors['blue-button']}`, + }} loading="lazy" /> @@ -196,10 +204,9 @@ function EditPenghargaan() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - statePenghargaan.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } />
diff --git a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx index a8d902e4..05a5774a 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx @@ -122,6 +122,7 @@ function DetailPenghargaan() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx index 94a3c4e4..dbfb717c 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx @@ -87,17 +87,17 @@ function CreatePenghargaan() { > (statePenghargaan.create.form.name = val.target.value)} - label={Nama Penghargaan} + label="Nama Penghargaan" placeholder="Masukkan nama penghargaan" required /> (statePenghargaan.create.form.juara = val.target.value)} - label={Juara} + label="Juara" placeholder="Masukkan juara" required /> diff --git a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx index 4e54e837..ac4de300 100644 --- a/src/app/admin/(dashboard)/desa/penghargaan/page.tsx +++ b/src/app/admin/(dashboard)/desa/penghargaan/page.tsx @@ -108,6 +108,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/kategori-pengumuman/[id]/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx index fe07bfd4..c2eeb624 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 { @@ -11,7 +12,6 @@ import { TextInput, Title, Tooltip, - Text, } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,10 +24,9 @@ function EditKategoriPengumuman() { const router = useRouter(); const params = useParams(); - const [formData, setFormData] = useState({ - name: editState.update.form.name || '', - }); + const [formData, setFormData] = useState({ name: '' }); + // Load data awal sekali aja useEffect(() => { const loadKategori = async () => { const id = params?.id as string; @@ -36,9 +35,7 @@ function EditKategoriPengumuman() { try { const data = await editState.update.load(id); if (data) { - setFormData({ - name: data.name || '', - }); + setFormData({ name: data.name || '' }); } } catch (error) { console.error('Error loading kategori Pengumuman:', error); @@ -49,8 +46,16 @@ function EditKategoriPengumuman() { loadKategori(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + const handleSubmit = async () => { try { + // Update global state hanya di sini editState.update.form = { ...editState.update.form, name: formData.name, @@ -95,16 +100,10 @@ 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 /> 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..c5e4bac0 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,10 +7,9 @@ import { Group, Paper, Stack, - Text, TextInput, Title, - Tooltip, + Tooltip } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -62,9 +61,9 @@ function CreateKategoriPengumuman() { > Nama Kategori Pengumuman
} + label="Nama Kategori Pengumuman" placeholder="Masukkan nama kategori pengumuman" - value={createState.create.form.name || ''} + defaultValue={createState.create.form.name || ''} onChange={(e) => (createState.create.form.name = e.target.value)} required /> 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..9e7a622f 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 @@ -28,16 +28,16 @@ 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 + // 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; @@ -61,9 +61,13 @@ 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 + // update global state hanya sekali pas submit editState.pengumuman.edit.form = { ...editState.pengumuman.edit.form, ...formData, @@ -109,7 +113,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 +121,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 /> Kategori} + label="Kategori" placeholder="Pilih kategori" value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""} onChange={(val) => { @@ -93,9 +93,9 @@ function CreatePengumuman() { {/* Deskripsi Singkat */} (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)} - label={Deskripsi Singkat} + label="Deskripsi Singkat" placeholder="Masukkan deskripsi singkat" required /> 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..16d0d951 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 @@ -24,9 +24,10 @@ function EditKategoriPotensi() { const params = useParams(); const [formData, setFormData] = useState({ - nama: editState.update.form.nama || '', + nama: '', }); + // Load data dari backend -> isi ke formData lokal useEffect(() => { const loadKategori = async () => { const id = params?.id as string; @@ -48,8 +49,16 @@ function EditKategoriPotensi() { loadKategori(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + const handleSubmit = async () => { try { + // Update global state hanya pas submit editState.update.form = { ...editState.update.form, nama: formData.nama, @@ -68,7 +77,12 @@ function EditKategoriPotensi() { - @@ -90,11 +104,11 @@ 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 /> - + @@ -123,21 +150,25 @@ 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) + } + /> + Kategori} + label="Kategori" placeholder="Pilih kategori" value={potensiState.create.form.kategoriId || ""} onChange={(val) => { 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..0599c55b 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx @@ -102,19 +102,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/profile-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx index c0ee0456..03660631 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 @@ -115,7 +115,7 @@ function Page() { Judul} placeholder="Judul lambang" - value={lambangState.update.form.judul} + defaultValue={lambangState.update.form.judul} onChange={(e) => lambangState.update.form.judul = e.currentTarget.value} error={!lambangState.update.form.judul && "Judul wajib diisi"} /> 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..8ead63e3 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 @@ -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: '', @@ -55,6 +57,7 @@ function Page() { file: null, preview: img.image.link, label: img.label, + imageId: img.image.id, // simpan id lama }))); } } @@ -88,25 +91,24 @@ function Page() { // 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); @@ -175,7 +177,7 @@ function Page() { Judul} placeholder="Masukkan judul maskot" - value={formData.judul} + defaultValue={formData.judul} onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })} error={!formData.judul && "Judul wajib diisi"} /> 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..c941dd4f 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 @@ -116,7 +116,7 @@ function Page() { Judul} placeholder="Judul sejarah" - value={sejarahState.update.form.judul} + defaultValue={sejarahState.update.form.judul} onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} error={!sejarahState.update.form.judul && "Judul wajib diisi"} /> 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..0dee1b80 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx @@ -70,7 +70,7 @@ function Page() {
- + @@ -118,9 +118,9 @@ function Page() { Visi Desa - + Misi Desa - + @@ -162,12 +162,12 @@ function Page() { style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }} > - Lambang Desa + {lambang.judul} - + @@ -214,9 +214,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..56f2c2e2 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 @@ -26,15 +26,17 @@ 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: '' }); + // load data pertama kali useEffect(() => { const loadFoto = async () => { const id = params?.id as string; @@ -48,7 +50,9 @@ function EditPerbekelDariMasaKeMasa() { periode: data.periode || '', imageId: data.imageId || '' }); - if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link); + if (data?.imageGalleryFoto?.link) { + setPreviewImage(data.imageGalleryFoto.link); + } } } catch (error) { console.error('Error loading foto:', error); @@ -58,8 +62,17 @@ function EditPerbekelDariMasaKeMasa() { loadFoto(); }, [params?.id]); + // helper ubah state formData + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + const handleSubmit = async () => { try { + // update global state hanya sekali pas submit state.update.form = { ...state.update.form, ...formData }; if (file) { @@ -107,7 +120,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 /> @@ -161,7 +174,7 @@ function EditPerbekelDariMasaKeMasa() { objectFit: 'contain', border: `1px solid ${colors['blue-button']}`, }} - loading='lazy' + loading="lazy" /> )} @@ -171,7 +184,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,7 +192,7 @@ 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 /> 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..d2c5ca5e 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 @@ -70,23 +70,23 @@ function CreatePerbekelDariMasaKeMasa() { > Nama Perbekel} + label="Nama Perbekel" placeholder="Masukkan nama perbekel" - value={state.create.form.nama} + defaultValue={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} + defaultValue={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} + defaultValue={state.create.form.periode} onChange={(e) => (state.create.form.periode = e.target.value)} required /> 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..6957d8f5 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 @@ -75,22 +75,28 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) { filteredData.map((item) => ( - {item.nama} + + {item.nama} + - {item.periode} + + {item.periode} + - + + + )) 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..73da958e 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx +++ b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx @@ -97,16 +97,16 @@ function Page() { {/* Biodata & Info */} Biodata - + Pengalaman - + Pengalaman Organisasi - + Program Kerja Unggulan - + 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..956f84e1 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,6 +1,6 @@ /* 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 { @@ -29,13 +29,13 @@ function EditAPBDesa() { 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 || [], + tahun: '', + pendapatanIds: [] as string[], + belanjaIds: [] as string[], + pembiayaanIds: [] as string[], }); - // Load APB desa by id + // Load APB desa by id β†’ hanya update formData, bukan global state useEffect(() => { const loadAPBdesa = async () => { const id = params?.id as string; @@ -45,7 +45,7 @@ function EditAPBDesa() { const data = await apbState.update.load(id); if (data) { setFormData({ - tahun: data.tahun || 0, + tahun: String(data.tahun || ''), pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [], belanjaIds: data.belanja?.map((b: any) => b.id) || [], pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [], @@ -60,8 +60,13 @@ function EditAPBDesa() { loadAPBdesa(); }, [params?.id]); + const handleChange = (field: keyof typeof formData, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { + // update global state cuma pas submit apbState.update.form = { ...apbState.update.form, tahun: Number(formData.tahun), @@ -112,9 +117,7 @@ function EditAPBDesa() { - setFormData({ ...formData, tahun: e.target.value }) - } + onChange={(e) => handleChange("tahun", e.target.value)} label={Tahun} placeholder="Masukkan tahun anggaran" required @@ -123,23 +126,17 @@ function EditAPBDesa() { {/* 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 */} @@ -164,7 +161,13 @@ function EditAPBDesa() { /* --- Sub Components --- */ - function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) { + function SelectPendapatan({ + selectedIds, + onSelectionChange, + }: { + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; + }) { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); useShallowEffect(() => { @@ -192,7 +195,13 @@ function EditAPBDesa() { ); } - function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) { + function SelectBelanja({ + selectedIds, + onSelectionChange, + }: { + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; + }) { const belanjaState = useProxy(PendapatanAsliDesa.belanja); useShallowEffect(() => { @@ -220,7 +229,13 @@ function EditAPBDesa() { ); } - function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) { + function SelectPembiayaan({ + selectedIds, + onSelectionChange, + }: { + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; + }) { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); useShallowEffect(() => { 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..797cc92f 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 @@ -65,7 +65,7 @@ function CreateAPBDesa() { { apbDesaState.create.form.tahun = Number(val.target.value); }} 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..7c03d4a8 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,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' + import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import colors from '@/con/colors'; import { @@ -24,12 +25,16 @@ function EditBelanja() { const params = useParams(); const [formData, setFormData] = useState({ - name: belanjaState.update.form.name || '', - value: belanjaState.update.form.value || '', + 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 +42,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 +57,7 @@ function EditBelanja() { if (data) { setFormData({ name: data.name || '', - value: data.value || '', + value: String(data.value || ''), }); } } catch (error) { @@ -69,7 +75,7 @@ function EditBelanja() { ...belanjaState.update.form, name: formData.name, value: Number(formData.value), - } + }; await belanjaState.update.update(); toast.success("Jenis Belanja berhasil diperbarui!"); @@ -78,7 +84,7 @@ function EditBelanja() { console.error("Error updating jenis belanja:", error); toast.error("Terjadi kesalahan saat memperbarui jenis belanja"); } - } + }; return ( @@ -113,18 +119,20 @@ 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 /> 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..7cc80cf9 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 @@ -85,7 +85,7 @@ function CreateBelanja() { Nama Jenis Belanja} placeholder="Masukkan nama jenis belanja" - value={belanjaState.create.form.name} + defaultValue={belanjaState.create.form.name} onChange={(e) => (belanjaState.create.form.name = e.target.value)} required /> @@ -94,7 +94,7 @@ function CreateBelanja() { type="text" label={Nilai} placeholder="Masukkan nilai belanja" - value={formatRupiah(belanjaState.create.form.value)} + defaultValue={formatRupiah(belanjaState.create.form.value)} onChange={(e) => { const raw = e.currentTarget.value; belanjaState.create.form.value = unformatRupiah(raw); 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..25df9c5b 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 @@ -24,12 +24,15 @@ function EditPembiayaan() { const params = useParams(); const [formData, setFormData] = useState({ - name: pembiayaanState.update.form.name || '', - value: pembiayaanState.update.form.value || '', + 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 +41,7 @@ function EditPembiayaan() { }; const unformatRupiah = (value: string) => { - return Number(value.replace(/\D/g, '')); + return Number(value.replace(/\D/g, '')) || 0; }; useEffect(() => { @@ -51,7 +54,7 @@ function EditPembiayaan() { if (data) { setFormData({ name: data.name || '', - value: data.value || '', + value: String(data.value || ''), }); } } catch (error) { @@ -68,7 +71,7 @@ function EditPembiayaan() { pembiayaanState.update.form = { ...pembiayaanState.update.form, name: formData.name, - value: Number(formData.value), + value: unformatRupiah(formData.value), }; await pembiayaanState.update.update(); @@ -82,7 +85,7 @@ function EditPembiayaan() { return ( - {/* Header dengan Back Button */} + {/* Header */} - + + + + + + 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 wajib + + + + + {previewImage && ( + Preview - )} - + + )}
+ + {/* 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)} - /> - { - setFormData({ ...formData, isActive: val === 'true' }); - }} + onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} /> + {/* Posisi */} + + Posisi + setFormData({ ...formData, isActive: val === 'true' })} + clearable + /> + + + {/* Submit Button */} +
-
+
); } 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..7a0626e4 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, Tooltip } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; function 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,125 @@ 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"} - - - + + + + + + + - - + + @@ -139,7 +187,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..dbf697ee 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 { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -11,17 +12,17 @@ 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 resetForm = () => { - stateOrganisasi.pegawai.create.form = { + stateOrganisasi.create.form = { namaLengkap: "", gelarAkademik: "", imageId: "", @@ -30,7 +31,7 @@ function CreatePegawai() { telepon: "", alamat: "", posisiId: "", - isActive: true, + isActive: true, }; }; @@ -52,15 +53,15 @@ function CreatePegawai() { } // 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"); @@ -72,130 +73,194 @@ function CreatePegawai() { }; return ( - - - - - - - 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 + Preview - )} - +
+ )} +
+ + (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.pegawai.create.form.posisiId} - onChange={(value) => { - if (value) stateOrganisasi.pegawai.create.form.posisiId = value; - }} - searchable - /> - + + +
); } -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..d56609de 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, Tooltip } 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,51 @@ function ListPegawai({ search }: { search: string }) { if (data.length === 0) { return ( - -

Tidak ada data pegawai yang tersedia

+ + + Daftar Pegawai BUMDesa + + + + +
+ Tidak ada data pegawai yang ditemukan +
); } return ( - - + + + Daftar Pegawai BUMDesa + + + + - +
- Nama - Gelar Akademik - Telepon - Posisi - Aktif - Detail + Nama Lengkap + Posisi + Status + Aksi @@ -106,10 +117,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 +152,15 @@ function ListPegawai({ search }: { search: string }) { - @@ -140,21 +168,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..0d8f7ed7 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,22 @@ +/* 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, 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 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 [formData, setFormData] = useState({ nama: '', @@ -32,13 +24,17 @@ function EditPosisiOrganisasi() { hierarki: 0, }); - useEffect(() => { - const loadPosisiOrganisasi = async () => { - if (!id) return; + // 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({ @@ -47,8 +43,8 @@ function EditPosisiOrganisasi() { 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'); } }; @@ -57,12 +53,13 @@ function EditPosisiOrganisasi() { }, [id]); const handleSubmit = async () => { - try { - if (!formData.nama.trim()) { - toast.error('Nama posisi organisasi tidak boleh kosong'); - return; - } + if (!formData.nama.trim()) { + toast.error('Nama posisi organisasi tidak boleh kosong'); + return; + } + try { + // Update global state hanya saat submit stateOrganisasi.edit.form = { nama: formData.nama.trim(), deskripsi: formData.deskripsi.trim(), @@ -70,19 +67,17 @@ 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 } }; @@ -90,17 +85,12 @@ function EditPosisiOrganisasi() { - - Edit Posisi Organisasi + Edit Posisi Organisasi BUMDes @@ -114,44 +104,40 @@ 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 /> - + - - - Tambah Posisi Organisasi - - + const handleSubmit = async () => { + try { + if (!stateOrganisasi.create.form.nama.trim()) { + return toast.error('Nama posisi tidak boleh kosong'); + } + + 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); + } + }; - {/* Form Card */} - + + + + + + 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 + /> + + + - - - - - ); + 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..ea181a8f 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, Tooltip } 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,70 @@ function ListPosisiOrganisasi({ search }: { search: string }) { - Daftar Posisi Organisasi - + Daftar Posisi Organisasi BumDes + - - Nama Posisi - Hierarki - Edit - Hapus + Nama Posisi + Deskripsi + Hierarki + Edit + Hapus {filteredData.length > 0 ? ( filteredData.map((item) => ( - + {item.nama} - - {item.hierarki ?? '-'} + + + + - - + + {item.hierarki || '-'} + + + - - - - + + + + )) @@ -170,8 +147,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..be24d78b --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx @@ -0,0 +1,133 @@ +/* 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, Tooltip } 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} + + {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..f1857aec 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 @@ -94,7 +94,7 @@ function DetailAjukanIdeInofativDesa() { Alamat - + @@ -104,12 +104,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 - - - - - + 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..05c93b59 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,21 @@ -'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 { + Box, + Button, + Group, + Image, + Paper, + Stack, + Text, + TextInput, + Title, + Tooltip, +} from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -12,20 +23,22 @@ 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 [formData, setFormData] = useState({ - name: stateDesaDigital.findUnique.data?.name || '', - deskripsi: stateDesaDigital.findUnique.data?.deskripsi || '', - imageId: stateDesaDigital.findUnique.data?.imageId || '', - }) + name: '', + deskripsi: '', + imageId: '', + }); useEffect(() => { - const loadPenghargaan = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; @@ -38,136 +51,160 @@ function EditPenghargaan() { imageId: data.imageId || '', }); - 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, - } + 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'); } - } + }; return ( - - - - - - - Edit Desa Digital Smart Village + + {/* Header */} + + + + + + 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/*': [] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar wajib + + + + + + {previewImage && ( + + + + )} + + + {/* 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 })) + } /> - + {/* Tombol 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..c82485e0 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,18 @@ '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, + Tooltip, +} from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -10,95 +20,136 @@ 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 ( - - - - - - - Detail Desa Digital Smart Village - {stateDesaDigital.findUnique.data ? ( - - - - Judul - {stateDesaDigital.findUnique.data?.name} - - - Deskripsi - - - - Gambar - - - + + {/* Tombol Kembali */} + + + {/* Card Utama */} + + + + Detail Desa Digital Smart Village + + + {/* Sub Card Detail */} + + + + Judul + {data?.name || '-'} + + + + Deskripsi + + + + + Gambar + {data?.image?.link ? ( + + ) : ( + Tidak ada gambar + )} + + + {/* Tombol Aksi */} + + + + + - - - - ) : null} + + + +
- {/* 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..6a00b669 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 @@ -22,7 +22,7 @@ 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); @@ -44,7 +44,6 @@ function CreateDesaDigital() { } try { - // Upload gambar dulu const uploadRes = await ApiFetch.api.fileStorage.create.post({ file, name: file.name, @@ -55,10 +54,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(); @@ -72,10 +69,16 @@ function CreateDesaDigital() { return ( - {/* Header */} - + {/* Header dengan tombol kembali */} + - @@ -84,28 +87,32 @@ function CreateDesaDigital() { - {/* Card */} + {/* Card Form */} - - {/* Nama */} + + {/* Input Nama */} (stateDesaDigital.create.form.name = e.target.value)} - required + radius="md" + withAsterisk /> {/* Deskripsi */} - + Deskripsi - - Gambar + + Gambar Desa Digital { @@ -134,6 +141,11 @@ function CreateDesaDigital() { accept={{ 'image/*': [] }} radius="md" p="xl" + style={{ + border: '2px dashed #cfd8dc', + backgroundColor: '#fafafa', + transition: 'background-color 0.2s ease, border-color 0.2s ease', + }} > @@ -153,15 +165,22 @@ function CreateDesaDigital() { {/* Preview */} {previewImage && ( - + @@ -170,7 +189,7 @@ function CreateDesaDigital() { {/* Tombol Submit */} - + - - - - Edit Jenis Layanan + + {/* Header */} + + + + + + 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, + })) + } /> - + + {/* Tombol Submit */} + + + 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..41990ab2 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 @@ -79,6 +79,7 @@ function DetailJenisLayanan() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> 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..b1b5a107 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 @@ -69,7 +69,7 @@ function CreateJenisLayanan() { > { statePasar.create.form.nama = val.target.value; }} @@ -78,7 +78,7 @@ function CreateJenisLayanan() { required /> { statePasar.create.form.deskripsi = val.target.value; }} 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..c64bb102 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 @@ -10,7 +10,7 @@ import { Stack, TextInput, Title, - Tooltip + Tooltip, } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -25,9 +25,10 @@ function EditJenisPengaduan() { const state = useProxy(layananonlineDesa.jenisPengaduan); const [formData, setFormData] = useState({ - nama: "", + nama: '', }); + // Load data sekali aja useEffect(() => { const loadJenisPengaduan = async () => { if (!id) return; @@ -36,51 +37,58 @@ 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 || '', }); } } 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 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 - } - 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 } }; return ( - {/* Header + tombol back */} + {/* Header */} @@ -115,7 +120,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 +128,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 +138,7 @@ function EditProgramKreatifDesa() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - stateProgramKreatif.update.form.deskripsi = htmlContent; - }} + onChange={handleChange('deskripsi')} /> @@ -146,10 +148,7 @@ function EditProgramKreatifDesa() { { - setFormData((prev) => ({ ...prev, icon: value })); - stateProgramKreatif.update.form.icon = value; - }} + onChange={handleChange('icon')} /> 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..e402bf15 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 @@ -86,12 +86,12 @@ function DetailProgramKreatifDesa() { Deskripsi Singkat - {data?.slug || '-'} + {data?.slug || '-'} Deskripsi - + 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..b5969bf7 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 @@ -64,7 +64,7 @@ function CreateProgramKreatifDesa() { Nama Program Kreatif Desa} placeholder="Masukkan nama program kreatif desa" - value={stateCreate.create.form.name || ""} + defaultValue={stateCreate.create.form.name || ""} onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)} required /> @@ -81,7 +81,7 @@ function CreateProgramKreatifDesa() { Deskripsi Singkat Program Kreatif Desa} placeholder="Masukkan deskripsi singkat program kreatif desa" - value={stateCreate.create.form.slug || ""} + defaultValue={stateCreate.create.form.slug || ""} onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)} required /> 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..c1a7d88e 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,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import { @@ -40,12 +39,12 @@ function EditKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); 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 + // Load data sekali pas mount useEffect(() => { const loadData = async () => { const id = params?.id as string; @@ -71,16 +70,16 @@ 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 handleSubmit = async () => { try { - keamananState.edit.form = { - ...keamananState.edit.form, - name: formData.name, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - }; + let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ @@ -93,9 +92,17 @@ 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"); @@ -186,7 +193,7 @@ function EditKeamananLingkungan() { {previewImage ? ( - + ) : (
@@ -195,9 +202,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,10 +214,7 @@ function EditKeamananLingkungan() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - keamananState.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => handleChange("deskripsi", htmlContent)} /> 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..1ce73db9 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 @@ -88,7 +88,7 @@ function DetailKeamananLingkungan() { Deskripsi - + {/* Aksi */} 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..4ff05394 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 @@ -172,7 +172,7 @@ function CreateKeamananLingkungan() { {/* Input Nama */} { keamananState.create.form.name = val.target.value; }} 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..8fde1a33 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 @@ -107,12 +107,16 @@ function ListKeamananLingkungan({ search }: { search: string }) { filteredData.map((item) => ( - - {item.name} - + + + {item.name} + + - + + + @@ -148,59 +148,59 @@ function CreatePolsekTerdekat() { > (polsekState.create.form.nama = val.target.value)} label={Nama Polsek Terdekat} placeholder="Masukkan nama Polsek Terdekat" required /> (polsekState.create.form.jarakKeDesa = val.target.value)} label={Jarak Polsek Terdekat} placeholder="Masukkan jarak Polsek Terdekat" required /> (polsekState.create.form.alamat = val.target.value)} label={Alamat Polsek Terdekat} placeholder="Masukkan alamat Polsek Terdekat" required /> (polsekState.create.form.nomorTelepon = val.target.value)} label={Nomor Telepon Polsek Terdekat} placeholder="Masukkan nomor telepon Polsek Terdekat" required /> (polsekState.create.form.jamOperasional = val.target.value)} label={Jam Operasional Polsek Terdekat} placeholder="Masukkan jam operasional Polsek Terdekat" /> (polsekState.create.form.embedMapUrl = val.target.value)} label={Embed Map URL} placeholder="Masukkan embed map url" /> (polsekState.create.form.namaTempatMaps = val.target.value)} label={Nama Tempat Maps} placeholder="Masukkan nama tempat maps" /> (polsekState.create.form.alamatMaps = val.target.value)} label={Alamat Maps} placeholder="Masukkan alamat maps" /> (polsekState.create.form.linkPetunjukArah = val.target.value)} label={Link Petunjuk Arah} placeholder="Masukkan link petunjuk arah" diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx index 270aec6c..2de3db48 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import { @@ -39,9 +38,9 @@ function EditTipsKeamanan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [formData, setFormData] = useState({ - judul: keamananState.update.form.judul || "", - deskripsi: keamananState.update.form.deskripsi || "", - imageId: keamananState.update.form.imageId || "", + judul: "", + deskripsi: "", + imageId: "", }); // Load data saat pertama kali @@ -70,14 +69,12 @@ function EditTipsKeamanan() { }; loadData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [params?.id]); const handleSubmit = async () => { try { - keamananState.update.form = { - ...keamananState.update.form, - ...formData, - }; + let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ @@ -87,10 +84,14 @@ function EditTipsKeamanan() { const uploaded = res.data?.data; if (!uploaded?.id) return toast.error("Gagal upload gambar"); - - keamananState.update.form.imageId = uploaded.id; + imageId = uploaded.id; } + keamananState.update.form = { + ...formData, + imageId, + }; + await keamananState.update.update(); toast.success("Tips Keamanan berhasil diperbarui!"); router.push("/admin/keamanan/tips-keamanan"); @@ -105,7 +106,12 @@ function EditTipsKeamanan() { {/* Header */} - @@ -137,7 +143,9 @@ function EditTipsKeamanan() { 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" @@ -145,7 +153,11 @@ function EditTipsKeamanan() { > - + @@ -200,7 +212,9 @@ function EditTipsKeamanan() { label="Nama Tips Keamanan" placeholder="Masukkan nama tips keamanan" value={formData.judul} - onChange={(e) => setFormData({ ...formData, judul: e.target.value })} + onChange={(e) => + setFormData((prev) => ({ ...prev, judul: e.target.value })) + } required /> @@ -211,10 +225,9 @@ function EditTipsKeamanan() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - keamananState.update.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx index 0b8ec263..61bb32c9 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx @@ -68,7 +68,7 @@ function DetailTipsKeamanan() { Nama Tips Keamanan - {data.judul || '-'} + {data.judul || '-'} @@ -76,6 +76,7 @@ function DetailTipsKeamanan() { diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx index c3227392..6f82636a 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx @@ -138,7 +138,7 @@ function CreateKeamananLingkungan() { (stateKeamanan.create.form.judul = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx index c01ec070..7305d94c 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx @@ -99,12 +99,16 @@ function ListTipsKeamanan({ search }: { search: string }) { filteredData.map((item) => ( - - {item.judul} - + + + {item.judul} + + - + + +
- Judul - Konten - Aksi + Judul + Konten + Aksi {filteredData.length > 0 ? ( filteredData.map((item) => ( - + {item.title} - + {item.content} - + + + + Edit Data Kelahiran + + - loadKelahiran(); - }, [params?.id]); + {/* Form */} + + + handleChange('nama', e.target.value)} + label="Nama" + placeholder="Masukkan nama" + required + /> + handleChange('tanggal', e.target.value)} + label="Tanggal" + placeholder="Masukkan tanggal" + required + /> + handleChange('jenisKelamin', e.target.value)} + label="Jenis Kelamin" + placeholder="Masukkan jenis kelamin" + required + /> + handleChange('alamat', e.target.value)} + label="Alamat" + placeholder="Masukkan alamat" + required + /> - - const handleSubmit = async () => { - try { - editState.edit.form = { - ...editState.edit.form, - nama: formData.nama, - tanggal: formData.tanggal, - jenisKelamin: formData.jenisKelamin, - alamat: formData.alamat, - }; - - - await editState.edit.update(); - toast.success('Data kelahiran berhasil diperbarui!'); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' - ); - } catch (error) { - console.error('Error updating data kelahiran:', error); - toast.error('Terjadi kesalahan saat memperbarui data kelahiran'); - } - }; - - - return ( - - {/* Header */} - - - - - - Edit Data Kelahiran - - - - - {/* Form */} - - - setFormData({ ...formData, nama: e.target.value })} - label="Nama" - placeholder="Masukkan nama" - required - /> - setFormData({ ...formData, tanggal: e.target.value })} - label="Tanggal" - placeholder="Masukkan tanggal" - required - /> - setFormData({ ...formData, jenisKelamin: e.target.value })} - label="Jenis Kelamin" - placeholder="Masukkan jenis kelamin" - required - /> - setFormData({ ...formData, alamat: e.target.value })} - label="Alamat" - placeholder="Masukkan alamat" - required - /> - - - - - - - - - ); + + + + + + + ); } - -export default EditKelahiran; \ No newline at end of file +export default EditKelahiran; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx index 2ce02278..18a2e587 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx @@ -105,7 +105,7 @@ function DetailKelahiran() { Alamat - {data.alamat || '-'} + {data.alamat || '-'} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx index e668ac72..b8476a66 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx @@ -74,7 +74,7 @@ function CreateKelahiran() { Nama} placeholder="Masukkan nama" - value={createState.create.form.nama} + defaultValue={createState.create.form.nama} onChange={(e) => (createState.create.form.nama = e.target.value)} required /> @@ -82,21 +82,21 @@ function CreateKelahiran() { type="date" label={Tanggal} placeholder="Masukkan tanggal" - value={createState.create.form.tanggal} + defaultValue={createState.create.form.tanggal} onChange={(e) => (createState.create.form.tanggal = e.target.value)} required /> Jenis Kelamin} placeholder="Masukkan jenis kelamin" - value={createState.create.form.jenisKelamin} + defaultValue={createState.create.form.jenisKelamin} onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)} required /> Alamat} placeholder="Masukkan alamat" - value={createState.create.form.alamat} + defaultValue={createState.create.form.alamat} onChange={(e) => (createState.create.form.alamat = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx index 510c60f3..9f7ada02 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[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 persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, + Box, + Button, + Group, + Paper, + Stack, + Text, + TextInput, + Title, + Tooltip, } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -20,160 +21,149 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - function EditKematian() { - const editState = useProxy(persentaseKelahiranKematian.kematian); - const router = useRouter(); - const params = useParams(); + const editState = useProxy(persentaseKelahiranKematian.kematian); + const router = useRouter(); + const params = useParams(); + const [formData, setFormData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyebab: '', + }); - const [formData, setFormData] = useState({ - nama: editState.edit.form.nama || '', - tanggal: editState.edit.form.tanggal || '', - jenisKelamin: editState.edit.form.jenisKelamin || '', - alamat: editState.edit.form.alamat || '', - penyebab: editState.edit.form.penyebab || '', - }); + // Load data saat mount + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) return; + try { + const data = await editState.edit.load(id); + if (data) { + setFormData({ + nama: data.nama || '', + tanggal: data.tanggal || '', + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyebab: data.penyebab || '', + }); + } + } catch (error) { + console.error('Error loading data kematian:', error); + toast.error('Gagal memuat data kematian'); + } + }; - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) return; + loadData(); + }, [params?.id]); + const handleChange = (key: keyof typeof formData, value: string) => { + setFormData(prev => ({ ...prev, [key]: value })); + }; - try { - const data = await editState.edit.load(id); - if (data) { - setFormData({ - nama: data.nama || '', - tanggal: data.tanggal || '', - jenisKelamin: data.jenisKelamin || '', - alamat: data.alamat || '', - penyebab: data.penyebab || '', - }); - } - } catch (error) { - console.error('Error loading data kematian:', error); - toast.error('Gagal memuat data kematian'); - } - }; + const handleSubmit = async () => { + try { + // Update global state saat submit + editState.edit.form = { ...editState.edit.form, ...formData }; + await editState.edit.update(); + toast.success('Data kematian berhasil diperbarui!'); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' + ); + } catch (error) { + console.error('Error updating data kematian:', error); + toast.error('Terjadi kesalahan saat memperbarui data kematian'); + } + }; + return ( + + {/* Header */} + + + + + + Edit Data Kematian + + - loadData(); - }, [params?.id]); + {/* Form Card */} + + + handleChange('nama', e.target.value)} + required + /> + handleChange('tanggal', e.target.value)} + required + /> - const handleSubmit = async () => { - try { - editState.edit.form = { ...editState.edit.form, ...formData }; - await editState.edit.update(); - toast.success('Data kematian berhasil diperbarui!'); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' - ); - } catch (error) { - console.error('Error updating data kematian:', error); - toast.error('Terjadi kesalahan saat memperbarui data kematian'); - } - }; + handleChange('jenisKelamin', e.target.value)} + required + /> + handleChange('alamat', e.target.value)} + required + /> - return ( - - {/* Header dengan tombol back */} - - - - - - Edit Data Kematian - - + + + Penyebab + + handleChange('penyebab', htmlContent)} + /> + - - {/* Card Form */} - - - setFormData({ ...formData, nama: e.target.value })} - required - /> - - - setFormData({ ...formData, tanggal: e.target.value })} - required - /> - - - setFormData({ ...formData, jenisKelamin: e.target.value })} - required - /> - - - setFormData({ ...formData, alamat: e.target.value })} - required - /> - - - - - Penyebab - - { - setFormData((prev) => ({ ...prev, penyebab: htmlContent })); - editState.edit.form.penyebab = htmlContent; - }} - /> - - - - - - - - - - ); + + + + + + + ); } - -export default EditKematian; \ No newline at end of file +export default EditKematian; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx index eeb7913b..41b4fa96 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx @@ -102,13 +102,13 @@ function DetailKematian() { Alamat - {data?.alamat || '-'} + {data?.alamat || '-'} Penyebab - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx index f9477dd5..2f1c92b7 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx @@ -83,7 +83,7 @@ function CreateKematian() { (createState.create.form.nama = e.target.value)} required /> @@ -91,21 +91,21 @@ function CreateKematian() { type="date" label="Tanggal" placeholder="Masukkan tanggal" - value={createState.create.form.tanggal} + defaultValue={createState.create.form.tanggal} onChange={(e) => (createState.create.form.tanggal = e.target.value)} required /> (createState.create.form.jenisKelamin = e.target.value)} required /> (createState.create.form.alamat = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx index 8d330f3c..6d751dd5 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx @@ -19,7 +19,7 @@ import { import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, ChangeEvent } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; @@ -28,15 +28,21 @@ function EditInfoWabahPenyakit() { const router = useRouter(); const params = useParams(); + const [formData, setFormData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsi: '', + imageId: '', + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); - const [formData, setFormData] = useState({ - name: infoWabahPenyakitState.edit.form.name || '', - deskripsiSingkat: infoWabahPenyakitState.edit.form.deskripsiSingkat || '', - deskripsi: infoWabahPenyakitState.edit.form.deskripsiLengkap || '', - imageId: infoWabahPenyakitState.edit.form.imageId || '', - }); + // Helper untuk update field formData + const updateField = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // Load data edit useEffect(() => { const loadInfoWabahPenyakit = async () => { const id = params?.id as string; @@ -52,12 +58,10 @@ function EditInfoWabahPenyakit() { imageId: data.imageId || '', }); - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + if (data.image?.link) setPreviewImage(data.image.link); } } catch (error) { - console.error('Error loading info wabah penyakit:', error); + console.error(error); toast.error('Gagal memuat data info wabah penyakit'); } }; @@ -67,34 +71,43 @@ function EditInfoWabahPenyakit() { const handleSubmit = async () => { try { + let uploadedImageId = formData.imageId; + + // Upload file kalau ada + 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'); + uploadedImageId = uploaded.id; + } + + // Update global state infoWabahPenyakitState.edit.form = { ...infoWabahPenyakitState.edit.form, name: formData.name, deskripsiSingkat: formData.deskripsiSingkat, deskripsiLengkap: formData.deskripsi, - imageId: formData.imageId, + imageId: uploadedImageId, }; - 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'); - } - - infoWabahPenyakitState.edit.form.imageId = uploaded.id; - } - await infoWabahPenyakitState.edit.update(); toast.success('Info wabah penyakit berhasil diperbarui!'); router.push('/admin/kesehatan/info-wabah-penyakit'); } catch (error) { - console.error('Error updating info wabah penyakit:', error); + console.error(error); toast.error('Terjadi kesalahan saat memperbarui info wabah penyakit'); } }; + const handleDrop = (files: File[]) => { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }; + return ( {/* Header */} @@ -121,19 +134,21 @@ function EditInfoWabahPenyakit() { setFormData({ ...formData, name: e.target.value })} + onChange={(e: ChangeEvent) => updateField('name', e.target.value)} label="Judul" placeholder="Masukkan judul" required /> - setFormData({ ...formData, deskripsiSingkat: e.target.value })} - label="Deskripsi Singkat" - placeholder="Masukkan deskripsi singkat" - required - /> + + + Deskripsi Singkat + + updateField('deskripsiSingkat', val)} + /> + @@ -141,7 +156,7 @@ function EditInfoWabahPenyakit() { setFormData({ ...formData, deskripsi: val })} + onChange={(val) => updateField('deskripsi', val)} /> @@ -150,13 +165,7 @@ function EditInfoWabahPenyakit() { Gambar { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} + onDrop={handleDrop} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} accept={{ 'image/*': [] }} diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx index 9e96bd7f..03fda087 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx @@ -84,7 +84,7 @@ function DetailInfoWabahPenyakit() { Deskripsi Singkat - {data.deskripsiSingkat || '-'} + @@ -93,6 +93,7 @@ function DetailInfoWabahPenyakit() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx index 0e0b6255..06a0d39c 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx @@ -91,7 +91,7 @@ function CreateInfoWabahPenyakit() { > { infoWabahPenyakitState.create.form.name = val.target.value; }} @@ -100,15 +100,15 @@ function CreateInfoWabahPenyakit() { required /> - { - infoWabahPenyakitState.create.form.deskripsiSingkat = val.target.value; - }} - label={Deskripsi Singkat} - placeholder="Masukkan deskripsi singkat" - required - /> + + Deskripsi Singkat + { + infoWabahPenyakitState.create.form.deskripsiSingkat = val; + }} + /> + Deskripsi diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx index aaa20847..e4215822 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx @@ -111,9 +111,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) { - - {item.deskripsiSingkat} - + diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx index c8562fe0..8ede9489 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx @@ -1,5 +1,4 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import colors from '@/con/colors'; @@ -23,7 +22,6 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - function EditKontakDarurat() { const kontakDaruratState = useProxy(kontakDarurat); const router = useRouter(); @@ -32,11 +30,14 @@ function EditKontakDarurat() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [formData, setFormData] = useState({ - name: kontakDaruratState.edit.form.name || '', - deskripsi: kontakDaruratState.edit.form.deskripsi || '', - imageId: kontakDaruratState.edit.form.imageId || '', + name: '', + deskripsi: '', + imageId: '', + whatsapp: '', }); + const [loading, setLoading] = useState(true); + // Load data sekali saat mount useEffect(() => { const loadKontakDarurat = async () => { const id = params?.id as string; @@ -49,41 +50,40 @@ function EditKontakDarurat() { name: data.name || '', deskripsi: data.deskripsi || '', imageId: data.imageId || '', + whatsapp: data.whatsapp || '', }); - - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { console.error("Error loading kontak darurat:", error); toast.error("Gagal memuat data kontak darurat"); + } finally { + setLoading(false); } }; loadKontakDarurat(); - }, [params?.id]); + }, [params?.id, kontakDaruratState.edit]); const handleSubmit = async () => { try { - kontakDaruratState.edit.form = { - ...kontakDaruratState.edit.form, - name: formData.name, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - }; + let imageId = formData.imageId; + // Upload file baru jika ada 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"); - } - - kontakDaruratState.edit.form.imageId = uploaded.id; + if (!uploaded?.id) return toast.error("Gagal upload gambar"); + imageId = uploaded.id; } + // Update global state sekaligus submit + kontakDaruratState.edit.form = { + ...kontakDaruratState.edit.form, + ...formData, + imageId, + }; + await kontakDaruratState.edit.update(); toast.success("Kontak darurat berhasil diperbarui!"); router.push("/admin/kesehatan/kontak-darurat"); @@ -93,9 +93,10 @@ function EditKontakDarurat() { } }; + if (loading) return Loading...; + return ( - {/* Header */} + + + Edit Posyandu + + - const handleSubmit = async () => { - try { - statePosyandu.edit.form = { - ...statePosyandu.edit.form, - ...formData, - }; + {/* Card utama */} + + + {/* Upload Gambar */} + + + Gambar Posyandu + + { + 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 && ( + + + + )} + - if (file) { - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = res.data?.data; + {/* Input Form */} + setFormData({ ...formData, name: e.target.value })} + required + /> + setFormData({ ...formData, nomor: e.target.value })} + required + /> - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } + + + Deskripsi Posyandu + + setFormData({ ...formData, deskripsi: htmlContent })} + /> + + + + Jadwal Pelayanan + + + setFormData({ ...formData, jadwalPelayanan: htmlContent }) + } + /> + - statePosyandu.edit.form.imageId = uploaded.id; - } - - - await statePosyandu.edit.update(); - toast.success("Posyandu berhasil diperbarui!"); - router.push("/admin/kesehatan/posyandu"); - } catch (error) { - console.error("Error updating posyandu:", error); - toast.error("Terjadi kesalahan saat memperbarui posyandu"); - } - }; - - - return ( - - {/* Tombol Back */} - - - - - - Edit Posyandu - - - - - {/* Card utama */} - - - {/* Upload Gambar */} - - - Gambar Posyandu - - { - 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 && ( - - - - )} - - - - {/* Input Form */} - setFormData({ ...formData, name: e.target.value })} - required - /> - - - setFormData({ ...formData, nomor: e.target.value })} - required - /> - - - - Deskripsi Posyandu - { - setFormData({ ...formData, deskripsi: htmlContent }); - statePosyandu.edit.form.deskripsi = htmlContent; - }} - /> - - - - - Jadwal Pelayanan - { - setFormData({ ...formData, jadwalPelayanan: htmlContent }); - statePosyandu.edit.form.jadwalPelayanan = htmlContent; - }} - /> - - - - {/* Tombol Submit */} - - - - - - - ); + {/* Tombol Submit */} + + + + + + + ); } - -export default EditPosyandu; \ No newline at end of file +export default EditPosyandu; diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx index 3629f800..75af2397 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx @@ -93,6 +93,7 @@ function DetailPosyandu() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -103,6 +104,7 @@ function DetailPosyandu() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx index ad5047be..9542eefe 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx @@ -155,14 +155,14 @@ function CreatePosyandu() { (statePosyandu.create.form.name = e.target.value)} required /> (statePosyandu.create.form.nomor = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx index 97b20c03..1f0dcbec 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan'; import colors from '@/con/colors'; @@ -31,73 +31,70 @@ function EditProgramKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [formData, setFormData] = useState({ - name: programKesehatanState.edit.form.name || '', - deskripsiSingkat: programKesehatanState.edit.form.deskripsiSingkat || '', - deskripsi: programKesehatanState.edit.form.deskripsi || '', - imageId: programKesehatanState.edit.form.imageId || '', + name: '', + deskripsiSingkat: '', + deskripsi: '', + imageId: '', }); + // Load data awal useEffect(() => { - const loadProgramKesehatan = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; try { const data = await programKesehatanState.edit.load(id); - if (data) { - setFormData({ - name: data.name || '', - deskripsiSingkat: data.deskripsiSingkat || '', - deskripsi: data.deskripsi || '', - imageId: data.imageId || '', - }); + if (!data) return; - if (data?.image?.link) { - setPreviewImage(data.image.link); - } - } - } catch (error) { - console.error('Error loading program kesehatan:', error); + setFormData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + }); + + if (data?.image?.link) setPreviewImage(data.image.link); + } catch (err) { + console.error(err); toast.error('Gagal memuat data program kesehatan'); } }; - loadProgramKesehatan(); + loadData(); }, [params?.id]); + // Handler input controlled + const handleChange = (key: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [key]: value })); + }; + + // Submit form const handleSubmit = async () => { try { - programKesehatanState.edit.form = { - ...programKesehatanState.edit.form, - name: formData.name, - deskripsiSingkat: formData.deskripsiSingkat, - deskripsi: formData.deskripsi, - imageId: formData.imageId, - }; + const updatedForm = { ...programKesehatanState.edit.form, ...formData }; + // Upload file kalau ada 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'); - } - - programKesehatanState.edit.form.imageId = uploaded.id; + updatedForm.imageId = uploaded.id; } + programKesehatanState.edit.form = updatedForm; await programKesehatanState.edit.update(); toast.success('Program kesehatan berhasil diperbarui!'); router.push('/admin/kesehatan/program-kesehatan'); - } catch (error) { - console.error('Error updating program kesehatan:', error); + } catch (err) { + console.error(err); toast.error('Terjadi kesalahan saat memperbarui program kesehatan'); } }; return ( - {/* Header dengan tombol back */} diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx index 80af1a3f..1925e0a5 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx @@ -218,14 +218,14 @@ function CreateAPBDes() { (stateAPBDes.create.form.name = e.target.value)} required /> (stateAPBDes.create.form.jumlah = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx index 5e22c5fe..48af5204 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/[id]/page.tsx @@ -1,11 +1,12 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' + import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; 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 { useEffect, useState, useCallback } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; @@ -15,28 +16,25 @@ export default function EditKategoriDesaAntiKorupsi() { const id = params?.id as string; const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi); - const [formData, setFormData] = useState({ - name: "", - }); + // state lokal untuk form + const [formData, setFormData] = useState({ name: '' }); const [isLoading, setIsLoading] = useState(false); + // load data kategori saat mount atau id berubah useEffect(() => { + if (!id) return; + const loadKategori = async () => { - if (!id) return; setIsLoading(true); - try { const data = await stateKategori.edit.load(id); - if (data) { stateKategori.edit.id = id; - setFormData({ - name: data.name || '', - }); + setFormData({ name: data.name || '' }); } - } catch (error) { - console.error("Error loading kategori desa anti korupsi:", error); - toast.error("Gagal memuat data kategori desa anti korupsi"); + } catch (err) { + console.error(err); + toast.error('Gagal memuat data kategori desa anti korupsi'); } finally { setIsLoading(false); } @@ -45,36 +43,40 @@ export default function EditKategoriDesaAntiKorupsi() { loadKategori(); }, [id]); - const handleSubmit = async () => { - if (!formData.name.trim()) { - return toast.error('Nama kategori tidak boleh kosong'); - } + // handler controlled input + const handleChange = useCallback( + (field: keyof typeof formData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }, + [] + ); + + // submit form + const handleSubmit = useCallback(async () => { + if (!formData.name.trim()) return toast.error('Nama kategori tidak boleh kosong'); try { setIsLoading(true); - stateKategori.edit.form = { - name: formData.name.trim(), - }; - if (!stateKategori.edit.id) { - stateKategori.edit.id = id; - } + // update global state hanya saat submit + stateKategori.edit.form = { name: formData.name.trim() }; + if (!stateKategori.edit.id) stateKategori.edit.id = id; await stateKategori.edit.update(); toast.success('Kategori berhasil diperbarui'); - router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"); - } catch (error) { - console.error("Error updating kategori desa anti korupsi:", error); - toast.error(error instanceof Error ? error.message : 'Gagal memperbarui kategori'); + router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi'); + } catch (err) { + console.error(err); + toast.error(err instanceof Error ? err.message : 'Gagal memperbarui kategori'); } finally { setIsLoading(false); } - }; + }, [formData.name, id, router, stateKategori.edit]); return ( - + @@ -96,8 +98,8 @@ export default function EditKategoriDesaAntiKorupsi() { setFormData({ ...formData, name: e.target.value })} + value={formData.name} // controlled + onChange={(e) => handleChange('name', e.currentTarget.value)} required disabled={isLoading} /> diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx index 2ecb3429..15e3645c 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create/page.tsx @@ -57,7 +57,7 @@ export default function CreateKategoriDesaAntiKorupsi() { (stateKategori.create.form.name = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx index 85592ca7..d60eb719 100644 --- a/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx @@ -1,18 +1,19 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; + import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, ChangeEvent } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +import { Dropzone } from '@mantine/dropzone'; +import { useShallowEffect } from '@mantine/hooks'; import colors from '@/con/colors'; import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi'; import ApiFetch from '@/lib/api-fetch'; -import { Dropzone } from '@mantine/dropzone'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import { useShallowEffect } from '@mantine/hooks'; interface FormDesaAntiKorupsi { name: string; @@ -23,12 +24,9 @@ interface FormDesaAntiKorupsi { export default function EditDesaAntiKorupsi() { const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi); - const [previewFile, setPreviewFile] = useState(null); - const [file, setFile] = useState(null); - const [isLoading, setIsLoading] = useState(false); const params = useParams(); const router = useRouter(); - + const [formData, setFormData] = useState({ name: '', deskripsi: '', @@ -36,10 +34,16 @@ export default function EditDesaAntiKorupsi() { fileId: '', }); + const [previewFile, setPreviewFile] = useState(null); + const [file, setFile] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // Load kategori useShallowEffect(() => { korupsiState.kategoriDesaAntiKorupsi.findMany.load(); }, []); + // Load data existing useEffect(() => { const loadDesaAntiKorupsi = async () => { const id = params?.id as string; @@ -47,29 +51,21 @@ export default function EditDesaAntiKorupsi() { try { const data = await desaAntiKorupsiState.edit.load(id); - if (data) { - desaAntiKorupsiState.edit.id = id; + if (!data) return; - desaAntiKorupsiState.edit.form = { - name: data.name, - deskripsi: data.deskripsi, - kategoriId: data.kategoriId, - fileId: data.fileId, - }; + desaAntiKorupsiState.edit.id = id; + desaAntiKorupsiState.edit.form = { ...data }; - setFormData({ - name: data.name, - deskripsi: data.deskripsi, - kategoriId: data.kategoriId, - fileId: data.fileId, - }); + setFormData({ + name: data.name, + deskripsi: data.deskripsi, + kategoriId: data.kategoriId, + fileId: data.fileId, + }); - if (data?.file?.link) { - setPreviewFile(data.file.link); - } - } - } catch (error) { - console.error('Error loading data:', error); + if (data.file?.link) setPreviewFile(data.file.link); + } catch (err) { + console.error(err); toast.error('Gagal memuat data Desa Anti Korupsi'); } }; @@ -77,42 +73,47 @@ export default function EditDesaAntiKorupsi() { loadDesaAntiKorupsi(); }, [params?.id]); + // Generic handler input + const handleInputChange = (key: keyof FormDesaAntiKorupsi) => (e: ChangeEvent | string | null) => { + const value = typeof e === 'string' ? e : e?.target?.value ?? ''; + setFormData((prev) => ({ ...prev, [key]: value || '' })); + }; + + // Special handler for Select component + const handleSelectChange = (key: keyof FormDesaAntiKorupsi) => (value: string | null) => { + setFormData((prev) => ({ ...prev, [key]: value || '' })); + }; + + const handleDrop = (files: File[]) => { + if (!files.length) return; + const selectedFile = files[0]; + setFile(selectedFile); + setPreviewFile(URL.createObjectURL(selectedFile)); + }; + const handleSubmit = async () => { - if (!formData.name) { - return toast.warn('Masukkan judul dokumen'); - } - if (!formData.kategoriId) { - return toast.warn('Pilih kategori dokumen'); - } + if (!formData.name) return toast.warn('Masukkan judul dokumen'); + if (!formData.kategoriId) return toast.warn('Pilih kategori dokumen'); setIsLoading(true); try { - // Update global state with form data - desaAntiKorupsiState.edit.form = { - ...desaAntiKorupsiState.edit.form, - ...formData, - kategoriId: formData.kategoriId || '', - }; + // Update global state + desaAntiKorupsiState.edit.form = { ...desaAntiKorupsiState.edit.form, ...formData }; - // Upload new file if exists + // Upload file jika ada 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) throw new Error('Gagal mengunggah dokumen'); - if (!uploaded?.id) { - throw new Error('Gagal mengunggah dokumen'); - } desaAntiKorupsiState.edit.form.fileId = uploaded.id; } await desaAntiKorupsiState.edit.update(); toast.success('Data berhasil diperbarui'); router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi'); - } catch (error) { - console.error('Error updating data:', error); + } catch (err) { + console.error(err); toast.error('Terjadi kesalahan saat memperbarui data'); } finally { setIsLoading(false); @@ -145,7 +146,7 @@ export default function EditDesaAntiKorupsi() { label="Judul Dokumen" placeholder="Masukkan judul dokumen" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={handleInputChange('name')} required /> @@ -153,23 +154,18 @@ export default function EditDesaAntiKorupsi() { Deskripsi - setFormData({ ...formData, deskripsi: val })} - /> + {label}} + value={value} + onChange={(val) => onChange(val || '')} + data={options} + placeholder={placeholder} + disabled={loading} + clearable + searchable + required + radius="md" + error={error} + /> + ); + }; return ( @@ -100,125 +140,62 @@ function EditResponden() { > - Nama Responden - - } - type='text' + label="Nama Responden" placeholder="Masukkan nama responden" value={formData.name} - onChange={(val) => { - setFormData({ - ...formData, - name: val.currentTarget.value - }) - }} + onChange={(e) => setFormData({ ...formData, name: e.currentTarget.value })} radius="md" required /> - Tanggal - - } + label="Tanggal" type="date" - placeholder='Pilih tanggal' value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''} - onChange={(e) => { - const selectedDate = e.currentTarget.value; - setFormData({ - ...formData, - tanggal: selectedDate, - }); - }} + onChange={(e) => setFormData({ ...formData, tanggal: e.currentTarget.value })} radius="md" required /> - setFormData({ ...formData, ratingId: val || "" })} - label={ - - Rating - - } - placeholder='Pilih rating' - data={ - (indeksKepuasanState.pilihanRatingResponden.findMany.data || []) - .filter(Boolean) - .map((v) => ({ - value: v.id || '', - label: typeof v.name === 'string' ? v.name : 'Tanpa Nama' - })) - } - disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading} - clearable - searchable - required - radius="md" - error={!formData.ratingId ? "Pilih rating" : undefined} - /> - { - setFormData({ - ...formData, - kategoriId: val ?? "" - }); - }} - data={ - prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({ - value: v.id, - label: v.name, - })) || [] - } + onChange={(val) => setFormData({ ...formData, kategoriId: val ?? '' })} + data={prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({ value: v.id, label: v.name })) || []} required /> - - Gambar Prestasi - + Gambar Prestasi { const selectedFile = files[0]; @@ -197,22 +145,12 @@ function EditPrestasiDesa() { p="xl" > - - - - - - - - - + + + - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB, format gambar wajib - + Seret gambar atau klik untuk memilih file + Maksimal 5MB, format gambar wajib @@ -224,7 +162,7 @@ function EditPrestasiDesa() { alt="Preview Gambar" radius="md" style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} - loading='lazy' + loading="lazy" /> )} @@ -249,6 +187,3 @@ function EditPrestasiDesa() { ); } - - -export default EditPrestasiDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/page.tsx index 97f0f67a..d7c3ecba 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/[id]/page.tsx @@ -82,6 +82,7 @@ function DetailPrestasiDesa() { fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi || '-' }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx index 25025d19..8462f93d 100644 --- a/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/prestasi-desa/list-prestasi-desa/create/page.tsx @@ -141,7 +141,7 @@ function CreatePrestasiDesa() { (stateCreate.create.form.name = e.target.value)} required /> @@ -170,12 +170,11 @@ function CreatePrestasiDesa() { required /> - + diff --git a/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx index d2dd5ab9..24595d4b 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/_lib/layoutTabs.tsx @@ -66,7 +66,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { return ( - Profil Desa + Profile Desa (null); const [file, setFile] = useState(null); const [formData, setFormData] = useState({ - name: stateMediaSosial.update.form.name || '', - iconUrl: stateMediaSosial.update.form.iconUrl || '', - imageId: stateMediaSosial.update.form.imageId || '', + name: '', + iconUrl: '', + imageId: '', }); + // Load data by ID useEffect(() => { const id = params?.id as string; if (!id) return; - const loadMediaSosial = async () => { + const loadData = async () => { try { const data = await stateMediaSosial.update.load(id); @@ -59,11 +60,16 @@ function EditMediaSosial() { } }; - loadMediaSosial(); + loadData(); }, [params?.id]); + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { + // update global state hanya saat submit stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData }; if (file) { @@ -85,7 +91,10 @@ function EditMediaSosial() { }; return ( - + @@ -170,74 +202,70 @@ function EditPejabatDesa() { Nama Perbekel} placeholder="Masukkan nama perbekel" - value={allState.edit.form.name} - onChange={(e) => handleFieldChange('name', e.currentTarget.value)} - error={!allState.edit.form.name && "Nama wajib diisi"} + value={formData.name} + onChange={(e) => handleChange('name', e.currentTarget.value)} + error={!formData.name && "Nama wajib diisi"} /> {/* Posisi Field */} Posisi} placeholder="Masukkan posisi" - value={allState.edit.form.position} - onChange={(e) => handleFieldChange('position', e.currentTarget.value)} - error={!allState.edit.form.position && "Posisi wajib diisi"} + value={formData.position} + onChange={(e) => handleChange('position', e.currentTarget.value)} + error={!formData.position && "Posisi wajib diisi"} /> {/* File Upload */} Gambar - - handleFileChange(files[0])} - onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} // Maks 5MB - accept={{ 'image/*': [] }} - > - - - - - - - - - - + handleFileChange(files[0])} + onReject={() => toast.error('File tidak valid.')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': [] }} + > + + + + + + + + + + -
- - Drag gambar ke sini atau klik untuk pilih file - - - Maksimal 5MB dan harus format gambar - -
-
-
+
+ + Drag gambar ke sini atau klik untuk pilih file + + + Maksimal 5MB dan harus format gambar + +
+
+
- {/* Tampilkan preview kalau ada */} - {previewImage && ( - - - - )} - -
+ {previewImage && ( + + + + )}
- {/* Preview Gambar */} + {/* Preview */} Preview Gambar {previewImage ? ( @@ -252,13 +280,13 @@ function EditPejabatDesa() { )} - {/* Submit Button */} + {/* Submit */} @@ -278,4 +306,4 @@ function EditPejabatDesa() { ); } -export default EditPejabatDesa; \ No newline at end of file +export default EditPejabatDesa; diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx index 4d72ce2b..d4b44eb3 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/[id]/edit/page.tsx @@ -31,10 +31,10 @@ function EditProgramInovasi() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [formData, setFormData] = useState({ - name: stateProgramInovasi.update.form.name || "", - description: stateProgramInovasi.update.form.description || "", - imageId: stateProgramInovasi.update.form.imageId || "", - link: stateProgramInovasi.update.form.link || "", + name: "", + description: "", + imageId: "", + link: "", }) useEffect(() => { @@ -51,9 +51,12 @@ function EditProgramInovasi() { imageId: data.imageId || "", link: data.link || "" }); - // Tampilkan preview gambar + + // Preview image if (data.image?.link) { setPreviewImage(data.image.link); + } else { + setPreviewImage(null); } } } catch (error) { @@ -69,24 +72,25 @@ function EditProgramInovasi() { const handleSubmit = async () => { try { + // Upload file 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"); + } + imageId = uploaded.id; + } + + // Update global state form (baru di sini) stateProgramInovasi.update.form = { ...stateProgramInovasi.update.form, name: formData.name, description: formData.description, - imageId: formData.imageId, + imageId, link: formData.link, } - if (file) { - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - // Update imageId in global state - stateProgramInovasi.update.form.imageId = uploaded.id; - } await stateProgramInovasi.update.update(); toast.success("Program Inovasi berhasil diperbarui!"); @@ -170,7 +174,9 @@ function EditProgramInovasi() {
)}
+ - Deskripsi + Deskripsi { - setFormData((prev) => ({ ...prev, description: htmlContent })); - stateProgramInovasi.update.form.description = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, description: htmlContent })) + } /> Deskripsi - + diff --git a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx index e214827a..de402dd6 100644 --- a/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/profile/program-inovasi/create/page.tsx @@ -144,7 +144,7 @@ function CreateProgramInovasi() { (stateProgramInovasi.create.form.name = e.target.value)} required /> @@ -162,7 +162,7 @@ function CreateProgramInovasi() { (stateProgramInovasi.create.form.link = e.target.value)} /> diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx index b3003d71..dd65bf5a 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx @@ -14,7 +14,7 @@ import { TextInput, Title, Tooltip, - Image + Image, } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; @@ -23,37 +23,35 @@ import { useEffect, useState } from "react"; import { toast } from "react-toastify"; import { useProxy } from "valtio/utils"; -function EditKolaborasiInovasi() { +export default function EditKolaborasiInovasi() { const sdgsState = useProxy(sdgsDesa); const router = useRouter(); const params = useParams(); + const [formData, setFormData] = useState({ + name: "", + jumlah: "", + imageId: "", + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); - const [formData, setFormData] = useState({ - name: sdgsState.edit.form.name || '', - jumlah: sdgsState.edit.form.jumlah || '', - imageId: sdgsState.edit.form.imageId || '' - }); - // Load sdgs desa by id saat pertama kali + // Load data sdgs desa by id useEffect(() => { const loadKolaborasi = async () => { const id = params?.id as string; if (!id) return; try { - const data = await sdgsState.edit.load(id); // akses langsung, bukan dari proxy + const data = await sdgsState.edit.load(id); if (data) { setFormData({ - name: data.name || '', - jumlah: data.jumlah || '', - imageId: data.imageId || '', + name: data.name || "", + jumlah: data.jumlah || "", + imageId: data.imageId || "", }); - if (data.image) { - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + if (data.image?.link) { + setPreviewImage(data.image.link); } } } catch (error) { @@ -65,31 +63,26 @@ function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + const handleInputChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - try { - // edit global state with form data - sdgsState.edit.form = { - ...sdgsState.edit.form, - name: formData.name, - jumlah: formData.jumlah, - imageId: formData.imageId // Keep existing imageId if not changed - }; + let imageId = formData.imageId; - // Jika ada file baru, upload + // Upload file baru jika ada 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"); - } - - // edit imageId in global state - sdgsState.edit.form.imageId = uploaded.id; + if (!uploaded?.id) return toast.error("Gagal upload gambar"); + imageId = uploaded.id; } + // Update global state hanya saat submit + sdgsState.edit.form = { ...sdgsState.edit.form, ...formData, imageId }; await sdgsState.edit.update(); + toast.success("sdgs desa berhasil diperbarui!"); router.push("/admin/landing-page/sdgs-desa"); } catch (error) { @@ -99,11 +92,11 @@ function EditKolaborasiInovasi() { }; return ( - + @@ -112,12 +105,12 @@ function EditKolaborasiInovasi() { </Group> <Paper - w={{ base: '100%', md: '50%' }} - bg={colors['white-1']} + w={{ base: "100%", md: "50%" }} + bg={colors["white-1"]} p="lg" radius="md" shadow="sm" - style={{ border: '1px solid #e0e0e0' }} + style={{ border: "1px solid #e0e0e0" }} > <Stack gap="md"> <Box> @@ -132,15 +125,15 @@ function EditKolaborasiInovasi() { 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/*': [] }} + accept={{ "image/*": [] }} radius="md" p="xl" > <Group justify="center" gap="xl" mih={180}> <Dropzone.Accept> - <IconUpload size={48} color={colors['blue-button']} stroke={1.5} /> + <IconUpload size={48} color={colors["blue-button"]} stroke={1.5} /> </Dropzone.Accept> <Dropzone.Reject> <IconX size={48} color="red" stroke={1.5} /> @@ -160,12 +153,12 @@ function EditKolaborasiInovasi() { </Dropzone> {previewImage && ( - <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> + <Box mt="sm" style={{ display: "flex", justifyContent: "center" }}> <Image src={previewImage} alt="Preview Gambar" radius="md" - style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} + style={{ maxHeight: 220, objectFit: "contain", border: `1px solid ${colors["blue-button"]}` }} loading="lazy" /> </Box> @@ -176,7 +169,7 @@ function EditKolaborasiInovasi() { label="Nama Sdgs Desa" placeholder="Masukkan nama Sdgs Desa" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleInputChange("name", e.target.value)} required /> @@ -184,7 +177,7 @@ function EditKolaborasiInovasi() { label="Jumlah" placeholder="Masukkan jumlah" value={formData.jumlah} - onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })} + onChange={(e) => handleInputChange("jumlah", e.target.value)} required type="number" /> @@ -197,9 +190,9 @@ function EditKolaborasiInovasi() { radius="md" size="md" style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, - color: '#fff', - boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', + background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`, + color: "#fff", + boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)", }} > Simpan @@ -210,5 +203,3 @@ function EditKolaborasiInovasi() { </Box> ); } - -export default EditKolaborasiInovasi; diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx b/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx index 0acf75f9..0b1b6517 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/sdgs-desa/create/page.tsx @@ -159,7 +159,7 @@ function CreateSDGsDesa() { </Text> } placeholder="Masukkan jumlah" - value={stateSDGSDesa.create.form.jumlah} + defaultValue={stateSDGSDesa.create.form.jumlah} onChange={(val) => { stateSDGSDesa.create.form.jumlah = val.target.value; }} @@ -167,14 +167,6 @@ function CreateSDGsDesa() { min={0} radius="md" /> - <TextInput - label="Test" - onChange={(val) => { - console.log(val.target.value) - }} - - /> - <Group justify="flex-end" mt="md"> <Button variant="light" diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx index 33dbccb4..6f66ec96 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/edit/page.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import dataLingkunganDesaState from '@/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa'; import colors from '@/con/colors'; @@ -46,7 +47,7 @@ type IconKey = | 'pohon' | 'air'; -function EditDataLingkunganDesa() { +export default function EditDataLingkunganDesa() { const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState); const params = useParams(); const router = useRouter(); @@ -58,8 +59,9 @@ function EditDataLingkunganDesa() { icon: '', }); + // Load data saat komponen mount useEffect(() => { - const loadProgramKreatif = async () => { + const loadData = async () => { const id = params?.id as string; if (!id) return; @@ -67,14 +69,6 @@ function EditDataLingkunganDesa() { const data = await stateDataLingkunganDesa.update.load(id); if (data) { stateDataLingkunganDesa.update.id = id; - - stateDataLingkunganDesa.update.form = { - name: data.name, - deskripsi: data.deskripsi, - jumlah: data.jumlah, - icon: data.icon, - }; - setFormData({ name: data.name, deskripsi: data.deskripsi, @@ -88,13 +82,14 @@ function EditDataLingkunganDesa() { } }; - loadProgramKreatif(); + loadData(); }, [params?.id]); const handleSubmit = async () => { try { + // Update global state hanya saat submit stateDataLingkunganDesa.update.form = { - ...stateDataLingkunganDesa.update.form, + ...formData, name: formData.name.trim(), deskripsi: formData.deskripsi.trim(), jumlah: formData.jumlah.trim(), @@ -135,11 +130,8 @@ function EditDataLingkunganDesa() { value={formData.name} label={<Text fz="sm" fw="bold">Nama Data Lingkungan Desa</Text>} placeholder="Masukkan nama data lingkungan desa" - onChange={(val) => - setFormData({ - ...formData, - name: val.target.value, - }) + onChange={(e) => + setFormData((prev) => ({ ...prev, name: e.target.value })) } required /> @@ -148,11 +140,8 @@ function EditDataLingkunganDesa() { value={formData.jumlah} label={<Text fz="sm" fw="bold">Jumlah Data Lingkungan Desa</Text>} placeholder="Masukkan jumlah data lingkungan desa" - onChange={(val) => - setFormData({ - ...formData, - jumlah: val.target.value, - }) + onChange={(e) => + setFormData((prev) => ({ ...prev, jumlah: e.target.value })) } required /> @@ -163,10 +152,9 @@ function EditDataLingkunganDesa() { </Text> <EditEditor value={formData.deskripsi} - onChange={(htmlContent) => { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - stateDataLingkunganDesa.update.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> </Box> @@ -176,10 +164,9 @@ function EditDataLingkunganDesa() { </Text> <SelectIconProgramEdit value={formData.icon as IconKey} - onChange={(value) => { - setFormData((prev) => ({ ...prev, icon: value })); - stateDataLingkunganDesa.update.form.icon = value; - }} + onChange={(value) => + setFormData((prev) => ({ ...prev, icon: value })) + } /> </Box> @@ -202,5 +189,3 @@ function EditDataLingkunganDesa() { </Box> ); } - -export default EditDataLingkunganDesa; diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/page.tsx index 5f1d6095..4e985dce 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/[id]/page.tsx @@ -131,7 +131,7 @@ function DetailDataLingkunganDesa() { <Box> <Text fz="lg" fw="bold">Deskripsi</Text> - <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} /> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} /> </Box> {/* Action Buttons */} diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx index 54dd409d..c2bff29e 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/create/page.tsx @@ -69,7 +69,7 @@ function CreateDataLingkunganDesa() { <TextInput label={<Text fw="bold" fz="sm">Nama Data Lingkungan Desa</Text>} placeholder="Masukkan nama data lingkungan desa" - value={stateCreate.create.form.name || ''} + defaultValue={stateCreate.create.form.name || ''} onChange={(val) => (stateCreate.create.form.name = val.target.value)} required /> @@ -86,7 +86,7 @@ function CreateDataLingkunganDesa() { <TextInput label={<Text fw="bold" fz="sm">Jumlah Data Lingkungan Desa</Text>} placeholder="Masukkan jumlah data lingkungan desa" - value={stateCreate.create.form.jumlah || ''} + defaultValue={stateCreate.create.form.jumlah || ''} onChange={(e) => (stateCreate.create.form.jumlah = e.currentTarget.value)} required /> diff --git a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/page.tsx b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/page.tsx index acc56ac6..b7addeaf 100644 --- a/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/data-lingkungan-desa/page.tsx @@ -115,7 +115,7 @@ function ListDataLingkunganDesa({ search }: { search: string }) { <Group justify="space-between" mb="md"> <Title order={4}>Daftar Data Lingkungan Desa - diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx index e43c4f1e..78447cc6 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan'; import colors from '@/con/colors'; import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; @@ -10,48 +11,67 @@ import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; const EdukasiLingkunganTextEditor = dynamic( - () => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), + () => + import('../../_lib/edukasiLingkunganTextEditor').then( + (mod) => mod.EdukasiLingkunganTextEditor + ), { ssr: false } ); -function EditContohKegiatanDesaDarmasaba() { +export default function EditContohKegiatanDesaDarmasaba() { const router = useRouter(); - const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + const contohEdukasiState = useProxy( + stateEdukasiLingkungan.stateContohEdukasiLingkungan + ); + // state lokal untuk form + const [formData, setFormData] = useState({ + judul: '', + deskripsi: '', + }); + + // load data awal useShallowEffect(() => { if (!contohEdukasiState.findById.data) { contohEdukasiState.findById.initialize(); } }, []); + // update state lokal saat data global sudah ada useEffect(() => { if (contohEdukasiState.findById.data) { - setJudul(contohEdukasiState.findById.data.judul ?? ''); - setContent(contohEdukasiState.findById.data.deskripsi ?? ''); + setFormData({ + judul: contohEdukasiState.findById.data.judul ?? '', + deskripsi: contohEdukasiState.findById.data.deskripsi ?? '', + }); } }, [contohEdukasiState.findById.data]); - const submit = () => { + // handler perubahan input + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // submit update + const handleSubmit = () => { if (contohEdukasiState.findById.data) { - contohEdukasiState.findById.data.judul = judul; - contohEdukasiState.findById.data.deskripsi = content; - contohEdukasiState.update.save(contohEdukasiState.findById.data); + const updatedData = { + ...contohEdukasiState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi, + }; + contohEdukasiState.update.save(updatedData); } - router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba'); + router.push( + '/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba' + ); }; return ( {/* Header */} - @@ -75,8 +95,8 @@ function EditContohKegiatanDesaDarmasaba() { </Text> <EdukasiLingkunganTextEditor showSubmit={false} - onChange={setJudul} - initialContent={judul} + onChange={(value) => handleChange('judul', value)} + initialContent={formData.judul} /> </Box> @@ -86,14 +106,14 @@ function EditContohKegiatanDesaDarmasaba() { </Text> <EdukasiLingkunganTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={(value) => handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> </Box> <Group justify="right" mt="md"> <Button - onClick={submit} + onClick={handleSubmit} radius="md" size="md" style={{ @@ -111,5 +131,3 @@ function EditContohKegiatanDesaDarmasaba() { </Box> ); } - -export default EditContohKegiatanDesaDarmasaba; diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/page.tsx index 1e13f6df..a7b7a396 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/page.tsx @@ -57,6 +57,7 @@ function Page() { fw={600} c="black" dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> <Box> @@ -66,6 +67,7 @@ function Page() { c="dimmed" lineClamp={10} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> </Paper> diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx index ad78350d..6f363765 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit/page.tsx @@ -14,30 +14,43 @@ const EdukasiLingkunganTextEditor = dynamic( { ssr: false } ); -function EditMateriEdukasiYangDiberikan() { +export default function EditMateriEdukasiYangDiberikan() { const router = useRouter(); const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // State lokal gabungan untuk form + const [formData, setFormData] = useState({ judul: '', content: '' }); + + // Initialize data kalau belum ada useShallowEffect(() => { if (!materiEdukasiState.findById.data) { materiEdukasiState.findById.initialize(); } }, []); + // Set formData saat data tersedia useEffect(() => { if (materiEdukasiState.findById.data) { - setJudul(materiEdukasiState.findById.data.judul ?? ''); - setContent(materiEdukasiState.findById.data.deskripsi ?? ''); + setFormData({ + judul: materiEdukasiState.findById.data.judul ?? '', + content: materiEdukasiState.findById.data.deskripsi ?? '', + }); } }, [materiEdukasiState.findById.data]); + // Handler perubahan form + const handleChange = (field: 'judul' | 'content', value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const submit = () => { if (materiEdukasiState.findById.data) { - materiEdukasiState.findById.data.judul = judul; - materiEdukasiState.findById.data.deskripsi = content; - materiEdukasiState.update.save(materiEdukasiState.findById.data); + const updatedData = { + ...materiEdukasiState.findById.data, + judul: formData.judul, + deskripsi: formData.content, + }; + materiEdukasiState.update.save(updatedData); } router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan'); }; @@ -66,14 +79,22 @@ function EditMateriEdukasiYangDiberikan() { <Text fw="bold" mb={6}> Judul </Text> - <EdukasiLingkunganTextEditor showSubmit={false} onChange={setJudul} initialContent={judul} /> + <EdukasiLingkunganTextEditor + showSubmit={false} + onChange={(val) => handleChange('judul', val)} + initialContent={formData.judul} + /> </Box> <Box> <Text fw="bold" mb={6}> Konten </Text> - <EdukasiLingkunganTextEditor showSubmit={false} onChange={setContent} initialContent={content} /> + <EdukasiLingkunganTextEditor + showSubmit={false} + onChange={(val) => handleChange('content', val)} + initialContent={formData.content} + /> </Box> <Group justify="right" mt="md"> @@ -96,5 +117,3 @@ function EditMateriEdukasiYangDiberikan() { </Box> ); } - -export default EditMateriEdukasiYangDiberikan; diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/page.tsx index 330a50ca..942d3d78 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/page.tsx @@ -53,6 +53,7 @@ function Page() { fw={600} c="black" dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> <Box> @@ -62,6 +63,7 @@ function Page() { c="dimmed" lineClamp={10} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> </Paper> diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx index b196c099..0c60469d 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan'; import colors from '@/con/colors'; import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; @@ -14,30 +15,44 @@ const EdukasiLingkunganTextEditor = dynamic( { ssr: false } ); -function EditTujuanEdukasiLingkungan() { +export default function EditTujuanEdukasiLingkungan() { const router = useRouter(); const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // State lokal untuk form, gak tergantung global state + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + + // Initialize global state useShallowEffect(() => { if (!tujuanEdukasiState.findById.data) { tujuanEdukasiState.findById.initialize(); } }, []); + // Sync initial values dari global state ke form lokal useEffect(() => { if (tujuanEdukasiState.findById.data) { - setJudul(tujuanEdukasiState.findById.data.judul ?? ''); - setContent(tujuanEdukasiState.findById.data.deskripsi ?? ''); + setFormData({ + judul: tujuanEdukasiState.findById.data.judul ?? '', + deskripsi: tujuanEdukasiState.findById.data.deskripsi ?? '', + }); } }, [tujuanEdukasiState.findById.data]); + // Handler input + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const submit = () => { if (tujuanEdukasiState.findById.data) { - tujuanEdukasiState.findById.data.judul = judul; - tujuanEdukasiState.findById.data.deskripsi = content; - tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data); + // Update global state hanya saat submit + const updatedData = { + ...tujuanEdukasiState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi, + }; + tujuanEdukasiState.update.save(updatedData); } router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan'); }; @@ -73,8 +88,8 @@ function EditTujuanEdukasiLingkungan() { </Text> <EdukasiLingkunganTextEditor showSubmit={false} - onChange={setJudul} - initialContent={judul} + onChange={(value) => handleChange('judul', value)} + initialContent={formData.judul} /> </Box> @@ -84,8 +99,8 @@ function EditTujuanEdukasiLingkungan() { </Text> <EdukasiLingkunganTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={(value) => handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> </Box> @@ -109,5 +124,3 @@ function EditTujuanEdukasiLingkungan() { </Box> ); } - -export default EditTujuanEdukasiLingkungan; diff --git a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/page.tsx b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/page.tsx index 49fd7dd5..48b88aa8 100644 --- a/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/page.tsx @@ -53,6 +53,7 @@ function Page() { fw={600} c='black' dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> <Box> @@ -62,6 +63,7 @@ function Page() { c="dimmed" lineClamp={10} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> </Paper> diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx index f6b81aa7..dd2e34a7 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/[id]/page.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import colors from '@/con/colors'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; @@ -15,53 +16,59 @@ function EditKategoriKegiatan() { const id = params?.id as string; const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan); - const [formData, setFormData] = useState({ - nama: '', - }); + const [formData, setFormData] = useState({ nama: '' }); + const [loading, setLoading] = useState(true); + // Load data once useEffect(() => { - const loadKategorikegiatan = async () => { - if (!id) return; + if (!id) return; + const loadKategori = async () => { try { const data = await stateKategori.edit.load(id); - if (data) { stateKategori.edit.id = id; - setFormData({ - nama: data.nama || '', - }); + setFormData({ nama: data.nama || '' }); } - } catch (error) { - console.error('Error loading kategori kegiatan:', error); + } catch (err) { + console.error('Error loading kategori kegiatan:', err); toast.error('Gagal memuat data kategori kegiatan'); + } finally { + setLoading(false); } }; - loadKategorikegiatan(); + loadKategori(); }, [id]); - const handleSubmit = async () => { - try { - if (!formData.nama.trim()) { - toast.error('Nama kategori kegiatan tidak boleh kosong'); - return; - } + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; - stateKategori.edit.form = { nama: formData.nama.trim() }; + const handleSubmit = async () => { + const trimmedNama = formData.nama.trim(); + if (!trimmedNama) { + toast.error('Nama kategori kegiatan tidak boleh kosong'); + return; + } + + try { + stateKategori.edit.form = { nama: trimmedNama }; if (!stateKategori.edit.id) stateKategori.edit.id = id; const success = await stateKategori.edit.update(); - if (success) { toast.success('Kategori kegiatan berhasil diperbarui!'); router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan'); } - } catch (error) { - console.error('Error updating kategori kegiatan:', error); + } catch (err) { + console.error('Error updating kategori kegiatan:', err); + toast.error('Gagal memperbarui kategori kegiatan'); } }; + if (loading) return <Text>Loading...</Text>; + return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> <Group mb="md" align="center"> @@ -86,7 +93,7 @@ function EditKategoriKegiatan() { <Stack gap="md"> <TextInput value={formData.nama} - onChange={(e) => setFormData({ ...formData, nama: e.target.value })} + onChange={(e) => handleChange('nama', e.target.value)} label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>} placeholder="Masukkan nama kategori kegiatan" required diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx index 3930a195..46d44b95 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kategori-kegiatan/create/page.tsx @@ -53,7 +53,7 @@ function CreateKategoriKegiatan() { > <Stack gap="md"> <TextInput - value={stateKategori.create.form.nama} + defaultValue={stateKategori.create.form.nama} onChange={(val) => (stateKategori.create.form.nama = val.target.value)} label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>} placeholder="Masukkan nama kategori kegiatan" diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx index 73545702..15b5c48a 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/edit/page.tsx @@ -4,7 +4,19 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { + Box, + Button, + Group, + Image, + Paper, + Select, + Stack, + Text, + TextInput, + Title, + Tooltip, +} from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -23,12 +35,10 @@ interface FormKegiatanDesa { kategoriKegiatanId: string; } -function EditGotongRoyong() { - const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa) - const params = useParams() - const router = useRouter() - const [previewImage, setPreviewImage] = useState<string | null>(null); - const [file, setFile] = useState<File | null>(null); +export default function EditKegiatanDesa() { + const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa); + const params = useParams(); + const router = useRouter(); const [formData, setFormData] = useState<FormKegiatanDesa>({ judul: '', @@ -39,16 +49,19 @@ function EditGotongRoyong() { partisipan: 0, imageId: '', kategoriKegiatanId: '', - }) + }); + const [file, setFile] = useState<File | null>(null); + const [previewImage, setPreviewImage] = useState<string | null>(null); const formatDateForInput = (dateString: string) => { if (!dateString) return ''; - const date = new Date(dateString); - return date.toISOString().split('T')[0]; + return new Date(dateString).toISOString().split('T')[0]; }; + // Load kategori & data kegiatan useEffect(() => { - const loadKegiatanDesa = async () => { + const loadData = async () => { + gotongRoyongState.kategoriKegiatan.findMany.load(); const id = params?.id as string; if (!id) return; @@ -65,43 +78,48 @@ function EditGotongRoyong() { imageId: data.imageId || '', kategoriKegiatanId: data.kategoriKegiatanId || '', }); + if (data.imageId) { + // Optional: bisa fetch URL image dari backend + setPreviewImage(`/api/file/${data.imageId}`); + } } } catch (error) { - console.error("Error loading kegiatan desa:", error); - toast.error("Gagal memuat data kegiatan desa"); + console.error(error); + toast.error('Gagal memuat data kegiatan desa'); } - } - gotongRoyongState.kategoriKegiatan.findMany.load() - loadKegiatanDesa(); + }; + loadData(); }, [params?.id]); const handleSubmit = async () => { try { - kegiatanDesaState.edit.form = { - ...kegiatanDesaState.edit.form, - judul: formData.judul.trim(), - deskripsiSingkat: formData.deskripsiSingkat.trim(), - deskripsiLengkap: formData.deskripsiLengkap.trim(), - tanggal: new Date(formData.tanggal.trim()), - lokasi: formData.lokasi.trim(), - partisipan: formData.partisipan, - imageId: formData.imageId, - kategoriKegiatanId: formData.kategoriKegiatanId, - } + 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"); - kegiatanDesaState.edit.form.imageId = uploaded.id; + if (!uploaded?.id) return toast.error('Gagal upload gambar'); + imageId = uploaded.id; } - await kegiatanDesaState.edit.update() - toast.success("Kegiatan desa berhasil diperbarui!") - router.push("/admin/lingkungan/gotong-royong/kegiatan-desa"); + + kegiatanDesaState.edit.form = { + judul: formData.judul.trim(), + deskripsiSingkat: formData.deskripsiSingkat.trim(), + deskripsiLengkap: formData.deskripsiLengkap.trim(), + tanggal: new Date(formData.tanggal), + lokasi: formData.lokasi.trim(), + partisipan: formData.partisipan, + imageId, + kategoriKegiatanId: formData.kategoriKegiatanId, + }; + + await kegiatanDesaState.edit.update(); + toast.success('Kegiatan desa berhasil diperbarui!'); + router.push('/admin/lingkungan/gotong-royong/kegiatan-desa'); } catch (error) { - console.error("Error updating kegiatan desa:", error); - toast.error("Terjadi kesalahan saat memperbarui kegiatan desa"); + console.error(error); + toast.error('Terjadi kesalahan saat memperbarui kegiatan desa'); } - } + }; return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> @@ -132,13 +150,13 @@ function EditGotongRoyong() { onChange={(e) => setFormData({ ...formData, judul: e.target.value })} required /> - <TextInput - value={formData.deskripsiSingkat} - label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>} - placeholder="masukkan deskripsi singkat kegiatan desa" - onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })} - required - /> + <Box> + <Text fw="bold" fz="sm">Deskripsi Singkat Kegiatan Desa</Text> + <EditEditor + value={formData.deskripsiSingkat} + onChange={(htmlContent) => setFormData(prev => ({ ...prev, deskripsiSingkat: htmlContent }))} + /> + </Box> <Select label="Kategori Kegiatan" data={gotongRoyongState.kategoriKegiatan.findMany.data?.map(k => ({ @@ -148,22 +166,16 @@ function EditGotongRoyong() { value={formData.kategoriKegiatanId} onChange={(val) => setFormData({ ...formData, kategoriKegiatanId: val ?? '' })} /> - <Box> <Text fw="bold" fz="sm">Deskripsi Lengkap Kegiatan Desa</Text> <EditEditor value={formData.deskripsiLengkap} - onChange={(htmlContent) => { - setFormData((prev) => ({ ...prev, deskripsiLengkap: htmlContent })); - kegiatanDesaState.edit.form.deskripsiLengkap = htmlContent; - }} + onChange={(htmlContent) => setFormData(prev => ({ ...prev, deskripsiLengkap: htmlContent }))} /> </Box> - <TextInput - label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>} - placeholder="masukkan tanggal kegiatan desa" type="date" + label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>} value={formatDateForInput(formData.tanggal)} onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })} /> @@ -187,11 +199,10 @@ function EditGotongRoyong() { <Text fw="bold" fz="sm" mb={6}>Gambar Kegiatan Desa</Text> <Dropzone onDrop={(files) => { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } + const selected = files[0]; + if (!selected) return; + setFile(selected); + setPreviewImage(URL.createObjectURL(selected)); }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} @@ -248,5 +259,3 @@ function EditGotongRoyong() { </Box> ); } - -export default EditGotongRoyong; diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/page.tsx index 42855333..11090d90 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/[id]/page.tsx @@ -85,13 +85,13 @@ function DetailKegiatanDesa() { {/* Deskripsi Singkat */} <Box> <Text fz="lg" fw="bold">Deskripsi Singkat</Text> - <Text fz="md" c="dimmed">{data.deskripsiSingkat || '-'}</Text> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat || '-' }} /> </Box> {/* Deskripsi Lengkap */} <Box> <Text fz="lg" fw="bold">Deskripsi</Text> - <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} /> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }} /> </Box> {/* Kategori */} @@ -109,7 +109,7 @@ function DetailKegiatanDesa() { {/* Lokasi */} <Box> <Text fz="lg" fw="bold">Lokasi</Text> - <Text fz="md" c="dimmed">{data.lokasi || '-'}</Text> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.lokasi || '-'}</Text> </Box> {/* Gambar */} diff --git a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx index 0ebb868c..2c4fbc49 100644 --- a/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/gotong-royong/kegiatan-desa/create/page.tsx @@ -155,21 +155,23 @@ function CreateKegiatanDesa() { <TextInput label="Judul Kegiatan" placeholder="Masukkan judul kegiatan" - value={stateKegiatanDesa.create.form.judul} + defaultValue={stateKegiatanDesa.create.form.judul} onChange={(e) => (stateKegiatanDesa.create.form.judul = e.target.value)} required /> - <TextInput - label="Deskripsi Singkat" - placeholder="Masukkan deskripsi singkat" - value={stateKegiatanDesa.create.form.deskripsiSingkat} - onChange={(e) => (stateKegiatanDesa.create.form.deskripsiSingkat = e.target.value)} - required - /> + <Box> + <Text fw="bold" fz="sm" mb={6}> + Deskripsi Singkat + </Text> + <CreateEditor + value={stateKegiatanDesa.create.form.deskripsiSingkat} + onChange={(val) => (stateKegiatanDesa.create.form.deskripsiSingkat = val)} + /> + </Box> <TextInput type="number" min={0} - value={stateKegiatanDesa.create.form.partisipan} + defaultValue={stateKegiatanDesa.create.form.partisipan} onChange={(e) => { const value = Number(e.target.value); if (value >= 0) { @@ -184,7 +186,7 @@ function CreateKegiatanDesa() { label="Tanggal" type="date" placeholder="Contoh: 2022-01-01" - value={ + defaultValue={ stateKegiatanDesa.create.form.tanggal ? stateKegiatanDesa.create.form.tanggal.toISOString().split('T')[0] : '' @@ -198,7 +200,7 @@ function CreateKegiatanDesa() { <TextInput label="Lokasi" placeholder="Masukkan lokasi kegiatan" - value={stateKegiatanDesa.create.form.lokasi} + defaultValue={stateKegiatanDesa.create.form.lokasi} onChange={(e) => (stateKegiatanDesa.create.form.lokasi = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx index 4008f466..aca07a0c 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/edit/page.tsx @@ -17,9 +17,11 @@ const KonservasiAdatBaliTextEditor = dynamic( function EditBentukKonservasiBerdasarkanAdat() { const router = useRouter(); const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // Gabung semua field form jadi satu object + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + + // Initialize data dari global state useShallowEffect(() => { if (!bentukKonservasiState.findById.data) { bentukKonservasiState.findById.initialize(); @@ -28,16 +30,22 @@ function EditBentukKonservasiBerdasarkanAdat() { useEffect(() => { if (bentukKonservasiState.findById.data) { - setJudul(bentukKonservasiState.findById.data.judul ?? ''); - setContent(bentukKonservasiState.findById.data.deskripsi ?? ''); + setFormData({ + judul: bentukKonservasiState.findById.data.judul ?? '', + deskripsi: bentukKonservasiState.findById.data.deskripsi ?? '', + }); } }, [bentukKonservasiState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const submit = () => { if (bentukKonservasiState.findById.data) { - bentukKonservasiState.findById.data.judul = judul; - bentukKonservasiState.findById.data.deskripsi = content; - bentukKonservasiState.update.save(bentukKonservasiState.findById.data); + // Update global state cuma pas submit + const updatedData = { ...bentukKonservasiState.findById.data, ...formData }; + bentukKonservasiState.update.save(updatedData); } router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat'); }; @@ -46,12 +54,7 @@ function EditBentukKonservasiBerdasarkanAdat() { <Box px={{ base: 'sm', md: 'lg' }} py="md"> {/* Header */} <Group mb="md"> - <Button - variant="subtle" - onClick={() => router.back()} - p="xs" - radius="md" - > + <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <IconArrowBack color={colors['blue-button']} size={24} /> </Button> <Title order={4} ml="sm" c="dark"> @@ -75,8 +78,8 @@ function EditBentukKonservasiBerdasarkanAdat() { </Text> <KonservasiAdatBaliTextEditor showSubmit={false} - onChange={setJudul} - initialContent={judul} + onChange={(value: string) => handleChange('judul', value)} + initialContent={formData.judul} /> </Box> @@ -86,8 +89,8 @@ function EditBentukKonservasiBerdasarkanAdat() { </Text> <KonservasiAdatBaliTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={(value: string) => handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> </Box> diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/page.tsx index a9b8330d..72b62a5f 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat/page.tsx @@ -64,6 +64,7 @@ function Page() { dangerouslySetInnerHTML={{ __html: listBentukKonservasiBerdasarkanAdat.findById.data.judul, }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> <Box> @@ -75,6 +76,7 @@ function Page() { dangerouslySetInnerHTML={{ __html: listBentukKonservasiBerdasarkanAdat.findById.data.deskripsi, }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> </Paper> diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx index 0ec3cd14..3ed20d1e 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali'; import colors from '@/con/colors'; import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core'; @@ -20,26 +21,35 @@ const KonservasiAdatBaliTextEditor = dynamic( function EditFilosofiTriHitaKarana() { const router = useRouter(); const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // Local state form + const [formData, setFormData] = useState({ judul: '', content: '' }); + + // Load data dari global state kalau belum ada useShallowEffect(() => { if (!filosofiTriHitaState.findById.data) { filosofiTriHitaState.findById.initialize(); } }, []); + // Set formData dari global state saat data tersedia useEffect(() => { if (filosofiTriHitaState.findById.data) { - setJudul(filosofiTriHitaState.findById.data.judul ?? ''); - setContent(filosofiTriHitaState.findById.data.deskripsi ?? ''); + setFormData({ + judul: filosofiTriHitaState.findById.data.judul ?? '', + content: filosofiTriHitaState.findById.data.deskripsi ?? '', + }); } }, [filosofiTriHitaState.findById.data]); - const submit = () => { + const handleChange = (field: 'judul' | 'content', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = () => { if (filosofiTriHitaState.findById.data) { - filosofiTriHitaState.findById.data.judul = judul; - filosofiTriHitaState.findById.data.deskripsi = content; + filosofiTriHitaState.findById.data.judul = formData.judul; + filosofiTriHitaState.findById.data.deskripsi = formData.content; filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data); } router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana'); @@ -49,12 +59,7 @@ function EditFilosofiTriHitaKarana() { <Box px={{ base: 'sm', md: 'lg' }} py="md"> {/* Header */} <Group mb="md"> - <Button - variant="subtle" - onClick={() => router.back()} - p="xs" - radius="md" - > + <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <IconArrowBack color={colors['blue-button']} size={24} /> </Button> <Title order={4} ml="sm" c="dark"> @@ -78,8 +83,8 @@ function EditFilosofiTriHitaKarana() { </Text> <KonservasiAdatBaliTextEditor showSubmit={false} - onChange={setJudul} - initialContent={judul} + onChange={(val) => handleChange('judul', val)} + initialContent={formData.judul} /> </Box> @@ -89,14 +94,14 @@ function EditFilosofiTriHitaKarana() { </Text> <KonservasiAdatBaliTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={(val) => handleChange('content', val)} + initialContent={formData.content} /> </Box> <Group justify="right" mt="md"> <Button - onClick={submit} + onClick={handleSubmit} radius="md" size="md" style={{ diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/page.tsx index 559270c3..cbacceb3 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/page.tsx @@ -57,6 +57,7 @@ function Page() { fw={600} c="black" dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> <Box px={{ base: 0, md: 20 }}> @@ -66,6 +67,7 @@ function Page() { c="dimmed" lineClamp={10} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> </Paper> diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx index 6f6271a8..1df50f0c 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/edit/page.tsx @@ -17,26 +17,36 @@ const KonservasiAdatBaliTextEditor = dynamic( function EditNilaiKonservasiAdat() { const router = useRouter(); const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // state lokal untuk form + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + + // load data awal useShallowEffect(() => { if (!nilaiKonservasiState.findById.data) { nilaiKonservasiState.findById.initialize(); } }, []); + // sync state lokal saat data backend siap useEffect(() => { if (nilaiKonservasiState.findById.data) { - setJudul(nilaiKonservasiState.findById.data.judul ?? ''); - setContent(nilaiKonservasiState.findById.data.deskripsi ?? ''); + setFormData({ + judul: nilaiKonservasiState.findById.data.judul ?? '', + deskripsi: nilaiKonservasiState.findById.data.deskripsi ?? '', + }); } }, [nilaiKonservasiState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const submit = () => { if (nilaiKonservasiState.findById.data) { - nilaiKonservasiState.findById.data.judul = judul; - nilaiKonservasiState.findById.data.deskripsi = content; + // update global state saat submit + nilaiKonservasiState.findById.data.judul = formData.judul; + nilaiKonservasiState.findById.data.deskripsi = formData.deskripsi; nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data); } router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat'); @@ -46,12 +56,7 @@ function EditNilaiKonservasiAdat() { <Box px={{ base: 'sm', md: 'lg' }} py="md"> {/* Header */} <Group mb="md"> - <Button - variant="subtle" - onClick={() => router.back()} - p="xs" - radius="md" - > + <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <IconArrowBack color={colors['blue-button']} size={24} /> </Button> <Title order={4} ml="sm" c="dark"> @@ -75,8 +80,8 @@ function EditNilaiKonservasiAdat() { </Text> <KonservasiAdatBaliTextEditor showSubmit={false} - onChange={setJudul} - initialContent={judul} + onChange={val => handleChange('judul', val)} + initialContent={formData.judul} /> </Box> @@ -86,8 +91,8 @@ function EditNilaiKonservasiAdat() { </Text> <KonservasiAdatBaliTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={val => handleChange('deskripsi', val)} + initialContent={formData.deskripsi} /> </Box> diff --git a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/page.tsx b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/page.tsx index 644e8d90..32893221 100644 --- a/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/konservasi-adat-bali/nilai-konservasi-adat/page.tsx @@ -55,6 +55,7 @@ function Page() { fw={600} c="black" dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> <Box> @@ -64,6 +65,7 @@ function Page() { c="dimmed" lineClamp={10} dangerouslySetInnerHTML={{ __html: listNilaiKonservasiAdat.findById.data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Box> </Paper> diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx index 49034b5c..56c9035d 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/edit/page.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; + import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import colors from '@/con/colors'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; @@ -13,9 +14,9 @@ import { useProxy } from 'valtio/utils'; const LeafletMapEdit = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapEdit'), { ssr: false }); function EditKeteranganBankSampahTerdekat() { - const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) + const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah); const router = useRouter(); - const params = useParams() + const params = useParams(); const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null); const [formData, setFormData] = useState({ @@ -24,10 +25,11 @@ function EditKeteranganBankSampahTerdekat() { namaTempatMaps: '', lat: 0, lng: 0, - }) + }); + // Load data ketika component mount useEffect(() => { - const loadKeteranganBankSampahTerdekat = async () => { + const loadKeterangan = async () => { const id = params?.id as string; if (!id) return; @@ -35,14 +37,8 @@ function EditKeteranganBankSampahTerdekat() { const data = await keteranganState.edit.load(id); if (data) { keteranganState.edit.id = id; - keteranganState.edit.form = { - name: data.name, - alamat: data.alamat, - namaTempatMaps: data.namaTempatMaps, - lat: data.lat, - lng: data.lng, - }; + // Update local formData dan markerPosition setFormData({ name: data.name, alamat: data.alamat, @@ -50,51 +46,47 @@ function EditKeteranganBankSampahTerdekat() { lat: data.lat, lng: data.lng, }); - setMarkerPosition({ lat: data.lat, lng: data.lng }); } } catch (error) { - console.error("Error loading pengelolaan sampah:", error); - toast.error("Gagal memuat data pengelolaan sampah"); + console.error(error); + toast.error('Gagal memuat data pengelolaan sampah'); } - } + }; - loadKeteranganBankSampahTerdekat(); + loadKeterangan(); }, [params?.id]); + const handleInputChange = (field: keyof typeof formData, value: string | number) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { - if (!formData.name.trim()) { - return toast.error('Nama bank sampah harus diisi'); - } - if (!formData.alamat.trim()) { - return toast.error('Alamat harus diisi'); - } - if (!formData.namaTempatMaps.trim()) { - return toast.error('Nama tempat di Maps harus diisi'); - } - if (!markerPosition) { - return toast.error('Silakan pilih lokasi di peta'); - } + if (!formData.name.trim()) return toast.error('Nama bank sampah harus diisi'); + if (!formData.alamat.trim()) return toast.error('Alamat harus diisi'); + if (!formData.namaTempatMaps.trim()) return toast.error('Nama tempat di Maps harus diisi'); + if (!markerPosition) return toast.error('Silakan pilih lokasi di peta'); + // Update global state hanya saat submit keteranganState.edit.form = { - ...keteranganState.edit.form, name: formData.name.trim(), alamat: formData.alamat.trim(), namaTempatMaps: formData.namaTempatMaps.trim(), lat: markerPosition.lat, lng: markerPosition.lng, }; - + await keteranganState.edit.update(); toast.success('Data bank sampah berhasil diperbarui'); keteranganState.findUnique.data = null; router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"); } catch (error) { - console.error("Error updating pengelolaan sampah:", error); + console.error(error); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah'); } - } + }; + return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> <Group mb="md"> @@ -121,44 +113,34 @@ function EditKeteranganBankSampahTerdekat() { label="Nama Bank Sampah" placeholder="Masukkan nama bank sampah" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleInputChange('name', e.target.value)} required /> - <TextInput label="Alamat" placeholder="Masukkan alamat lengkap" value={formData.alamat} - onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} + onChange={(e) => handleInputChange('alamat', e.target.value)} required /> - <TextInput label="Nama Tempat di Maps" placeholder="Masukkan nama tempat yang terdaftar di Google Maps" value={formData.namaTempatMaps} - onChange={(e) => setFormData({ ...formData, namaTempatMaps: e.target.value })} + onChange={(e) => handleInputChange('namaTempatMaps', e.target.value)} required /> <Box> - <Text fw="bold" fz="sm" mb={6}> - Pilih Lokasi di Peta - </Text> - <Text fz="xs" c="dimmed" mb={4}> - Klik pada peta untuk menandai lokasi - </Text> + <Text fw="bold" fz="sm" mb={6}>Pilih Lokasi di Peta</Text> + <Text fz="xs" c="dimmed" mb={4}>Klik pada peta untuk menandai lokasi</Text> <Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}> <LeafletMapEdit - key={markerPosition?.lat ?? 'default'} initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }} onChange={(pos) => { setMarkerPosition(pos); - setFormData(prev => ({ - ...prev, - lat: pos.lat, - lng: pos.lng, - })); + handleInputChange('lat', pos.lat); + handleInputChange('lng', pos.lng); }} /> </Box> diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/page.tsx index 417cd170..195861d8 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/[id]/page.tsx @@ -78,12 +78,12 @@ function DetailKeteranganBankSampahTerdekat() { <Box> <Text fz="sm" c="dimmed">Alamat</Text> - <Text fz="lg">{keteranganState.findUnique.data?.alamat}</Text> + <Text fz="lg" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{keteranganState.findUnique.data?.alamat}</Text> </Box> <Box> <Text fz="sm" c="dimmed">Nama Tempat di Maps</Text> - <Text fz="lg">{keteranganState.findUnique.data?.namaTempatMaps}</Text> + <Text fz="lg" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{keteranganState.findUnique.data?.namaTempatMaps}</Text> </Box> <Box> diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx index 883c89bf..ad8ae867 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create/page.tsx @@ -76,7 +76,7 @@ function CreateKeteranganBankSampahTerdekat() { <TextInput label="Nama Bank Sampah" placeholder="Masukkan nama bank sampah" - value={keteranganState.create.form.name} + defaultValue={keteranganState.create.form.name} onChange={(e) => (keteranganState.create.form.name = e.target.value)} required /> @@ -84,7 +84,7 @@ function CreateKeteranganBankSampahTerdekat() { <TextInput label="Alamat" placeholder="Masukkan alamat lengkap" - value={keteranganState.create.form.alamat} + defaultValue={keteranganState.create.form.alamat} onChange={(e) => (keteranganState.create.form.alamat = e.target.value)} required /> @@ -92,7 +92,7 @@ function CreateKeteranganBankSampahTerdekat() { <TextInput label="Nama Tempat di Maps" placeholder="Masukkan nama tempat yang terdaftar di Google Maps" - value={keteranganState.create.form.namaTempatMaps} + defaultValue={keteranganState.create.form.namaTempatMaps} onChange={(e) => (keteranganState.create.form.namaTempatMaps = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx index e0427dc8..683c410a 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/[id]/page.tsx @@ -17,15 +17,16 @@ interface FormProgramKreatif { type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash'; - function EditProgramKreatifDesa() { const stateSampah = useProxy(pengelolaanSampahState.pengelolaanSampah) const params = useParams() const router = useRouter(); + + // State lokal untuk form controlled const [formData, setFormData] = useState<FormProgramKreatif>({ name: '', icon: '', - }) + }); useEffect(() => { const loadProgramKreatif = async () => { @@ -35,14 +36,7 @@ function EditProgramKreatifDesa() { try { const data = await stateSampah.update.load(id); if (data) { - // ⬇️ FIX PENTING: tambahkan ini - stateSampah.update.id = id; - - stateSampah.update.form = { - name: data.name, - icon: data.icon, - }; - + stateSampah.update.id = id; // simpan id di global state setFormData({ name: data.name, icon: data.icon, @@ -52,21 +46,19 @@ function EditProgramKreatifDesa() { console.error("Error loading pengelolaan sampah:", error); toast.error("Gagal memuat data pengelolaan sampah"); } - } + }; loadProgramKreatif(); }, [params?.id]); - - const handleSubmit = async () => { try { + // Update global state HANYA saat submit stateSampah.update.form = { - ...stateSampah.update.form, name: formData.name.trim(), icon: formData.icon.trim(), }; - + await stateSampah.update.submit(); toast.success('Data pengelolaan sampah berhasil diperbarui!'); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"); @@ -74,17 +66,13 @@ function EditProgramKreatifDesa() { console.error("Error updating pengelolaan sampah:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah"); } - } + }; + return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> <Group mb="md"> <Tooltip label="Kembali ke halaman sebelumnya" withArrow> - <Button - variant="subtle" - onClick={() => router.back()} - p="xs" - radius="md" - > + <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <IconArrowBack color={colors['blue-button']} size={24} /> </Button> </Tooltip> @@ -106,14 +94,7 @@ function EditProgramKreatifDesa() { label="Nama Pengelolaan Sampah" placeholder="Masukkan nama pengelolaan sampah" value={formData.name} - onChange={(e) => { - const value = e.target.value; - setFormData(prev => ({ - ...prev, - name: value - })); - stateSampah.update.form.name = value; - }} + onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} required /> @@ -123,10 +104,7 @@ function EditProgramKreatifDesa() { </Text> <SelectIconProgramEdit value={formData.icon as IconKey} - onChange={(value) => { - setFormData(prev => ({ ...prev, icon: value })); - stateSampah.update.form.icon = value; - }} + onChange={(value) => setFormData(prev => ({ ...prev, icon: value }))} /> </Box> diff --git a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx index 563befe5..a413905b 100644 --- a/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create/page.tsx @@ -61,7 +61,7 @@ function CreatePengelolaanSampahBankSampah() { <TextInput label="Nama Pengelolaan Sampah" placeholder="Masukkan nama pengelolaan sampah" - value={stateCreate.create.form.name || ''} + defaultValue={stateCreate.create.form.name || ''} onChange={(e) => (stateCreate.create.form.name = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx index 274c1d16..141f920f 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/edit/page.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan'; @@ -50,13 +51,14 @@ function EditProgramPenghijauan() { const [formData, setFormData] = useState<FormProgramPenghijauan>({ name: '', - deskripsi: '', judul: '', + deskripsi: '', icon: '', }); + // Load data program penghijauan useEffect(() => { - const loadProgramPenghijauan = async () => { + const loadProgram = async () => { const id = params?.id as string; if (!id) return; @@ -64,14 +66,6 @@ function EditProgramPenghijauan() { const data = await stateProgramPenghijauan.update.load(id); if (data) { stateProgramPenghijauan.update.id = id; - - stateProgramPenghijauan.update.form = { - name: data.name, - judul: data.judul, - deskripsi: data.deskripsi, - icon: data.icon, - }; - setFormData({ name: data.name, judul: data.judul, @@ -85,19 +79,21 @@ function EditProgramPenghijauan() { } }; - loadProgramPenghijauan(); + loadProgram(); }, [params?.id]); + // Hanya update global state saat submit const handleSubmit = async () => { try { stateProgramPenghijauan.update.form = { - ...stateProgramPenghijauan.update.form, name: formData.name.trim(), - deskripsi: formData.deskripsi.trim(), judul: formData.judul.trim(), + deskripsi: formData.deskripsi.trim(), icon: formData.icon.trim(), }; + await stateProgramPenghijauan.update.submit(); + toast.success('Program penghijauan berhasil diperbarui'); router.push('/admin/lingkungan/program-penghijauan'); } catch (error) { console.error('Error updating program penghijauan:', error); @@ -107,7 +103,7 @@ function EditProgramPenghijauan() { return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> - {/* Header dengan back button */} + {/* Header */} <Group mb="md"> <Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Button @@ -138,11 +134,8 @@ function EditProgramPenghijauan() { value={formData.name} label="Nama Program Penghijauan" placeholder="Masukkan nama program penghijauan" - onChange={(val) => - setFormData({ - ...formData, - name: val.target.value, - }) + onChange={(e) => + setFormData((prev) => ({ ...prev, name: e.target.value })) } required /> @@ -151,11 +144,8 @@ function EditProgramPenghijauan() { value={formData.judul} label="Judul Deskripsi Program Penghijauan" placeholder="Masukkan judul deskripsi program penghijauan" - onChange={(val) => - setFormData({ - ...formData, - judul: val.target.value, - }) + onChange={(e) => + setFormData((prev) => ({ ...prev, judul: e.target.value })) } required /> @@ -166,10 +156,9 @@ function EditProgramPenghijauan() { </Text> <EditEditor value={formData.deskripsi} - onChange={(htmlContent) => { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - stateProgramPenghijauan.update.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } /> </Box> @@ -179,10 +168,9 @@ function EditProgramPenghijauan() { </Text> <SelectIconProgramEdit value={formData.icon as IconKey} - onChange={(value) => { - setFormData((prev) => ({ ...prev, icon: value })); - stateProgramPenghijauan.update.form.icon = value; - }} + onChange={(value) => + setFormData((prev) => ({ ...prev, icon: value })) + } /> </Box> diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx index 44df2081..a2857cfc 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/[id]/page.tsx @@ -112,7 +112,7 @@ function DetailProgramPenghijauan() { <Box> <Text fz="lg" fw="bold">Deskripsi</Text> - <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} /> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} /> </Box> {/* Tombol aksi */} diff --git a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx index a787090d..5b8e1ce5 100644 --- a/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx +++ b/src/app/admin/(dashboard)/lingkungan/program-penghijauan/create/page.tsx @@ -69,7 +69,7 @@ function CreateProgramPenghijauan() { <TextInput label={<Text fz="sm" fw="bold">Nama Program Penghijauan</Text>} placeholder="Masukkan nama program penghijauan" - value={stateCreate.create.form.name || ''} + defaultValue={stateCreate.create.form.name || ''} onChange={(e) => (stateCreate.create.form.name = e.target.value)} required /> @@ -86,7 +86,7 @@ function CreateProgramPenghijauan() { <TextInput label={<Text fz="sm" fw="bold">Judul Deskripsi Program Penghijauan</Text>} placeholder="Masukkan judul deskripsi program penghijauan" - value={stateCreate.create.form.judul || ''} + defaultValue={stateCreate.create.form.judul || ''} onChange={(e) => (stateCreate.create.form.judul = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/_lib/layoutTabs.tsx index 553daf17..2f2d3997 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { IconSchool, IconStar } from '@tabler/icons-react'; @@ -58,36 +58,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { radius="lg" keepMounted={false} > - <TabsList - p="sm" - style={{ - background: "linear-gradient(135deg, #e7ebf7, #f9faff)", - borderRadius: "1rem", - boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", + <ScrollArea type="auto" offsetScrollbars> + <TabsList + p="sm" + style={{ + background: "linear-gradient(135deg, #e7ebf7, #f9faff)", + borderRadius: "1rem", + boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", + display: "flex", + flexWrap: "nowrap", + gap: "0.5rem", + paddingInline: "0.5rem", // βœ… biar nggak nempel ke tepi }} - > - {tabs.map((tab, i) => ( - <Tooltip - key={i} - label={tab.tooltip} - position="bottom" - withArrow - transitionProps={{ transition: 'pop', duration: 200 }} - > - <TabsTab - value={tab.value} - leftSection={tab.icon} - style={{ - fontWeight: 600, - fontSize: "0.9rem", - transition: "all 0.2s ease", - }} + > + {tabs.map((tab, i) => ( + <Tooltip + key={i} + label={tab.tooltip} + position="bottom" + withArrow + transitionProps={{ transition: 'pop', duration: 200 }} > - {tab.label} - </TabsTab> - </Tooltip> - ))} - </TabsList> + <TabsTab + value={tab.value} + leftSection={tab.icon} + style={{ + fontWeight: 600, + fontSize: "0.9rem", + transition: "all 0.2s ease", + }} + > + {tab.label} + </TabsTab> + </Tooltip> + ))} + </TabsList> + </ScrollArea> {tabs.map((tab, i) => ( <TabsPanel diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/beasiswa-pendaftar/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/beasiswa-pendaftar/[id]/page.tsx index 16ffc1c8..f72394f9 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/beasiswa-pendaftar/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/beasiswa-pendaftar/[id]/page.tsx @@ -67,7 +67,7 @@ function DetailBeasiswaPendaftar() { <Stack gap="sm"> <Box> <Text fz="lg" fw="bold">Nama Lengkap</Text> - <Text fz="md" c="dimmed">{data.namaLengkap || '-'}</Text> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.namaLengkap || '-'}</Text> </Box> <Box> <Text fz="lg" fw="bold">NIK</Text> @@ -75,7 +75,7 @@ function DetailBeasiswaPendaftar() { </Box> <Box> <Text fz="lg" fw="bold">Tempat Lahir</Text> - <Text fz="md" c="dimmed">{data.tempatLahir || '-'}</Text> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.tempatLahir || '-'}</Text> </Box> <Box> <Text fz="lg" fw="bold">Tanggal Lahir</Text> @@ -97,11 +97,11 @@ function DetailBeasiswaPendaftar() { </Box> <Box> <Text fz="lg" fw="bold">Alamat KTP</Text> - <Text fz="md" c="dimmed">{data.alamatKTP || '-'}</Text> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.alamatKTP || '-'}</Text> </Box> <Box> <Text fz="lg" fw="bold">Alamat Domisili</Text> - <Text fz="md" c="dimmed">{data.alamatDomisili || '-'}</Text> + <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.alamatDomisili || '-'}</Text> </Box> <Box> <Text fz="lg" fw="bold">No HP</Text> diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx index 17188b39..3a745c50 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/[id]/page.tsx @@ -97,7 +97,7 @@ function EditProgramKreatifDesa() { <TextInput label="Judul" placeholder="Masukkan judul" - value={formData.judul} + defaultValue={formData.judul} onChange={(e) => { const value = e.target.value; setFormData(prev => ({ diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx index d0859b4b..f7fb5f93 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/create/page.tsx @@ -61,7 +61,7 @@ function CreateKeunggulanProgram() { <TextInput label="Judul" placeholder="Masukkan judul" - value={stateCreate.create.form.judul || ''} + defaultValue={stateCreate.create.form.judul || ''} onChange={(e) => (stateCreate.create.form.judul = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/page.tsx b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/page.tsx index 4d8bf0d4..f2f18216 100644 --- a/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/beasiswa-desa/keunggulan-program/page.tsx @@ -99,22 +99,22 @@ function ListKeunggulanProgram({ search }: { search: string }) { <Table highlightOnHover> <TableThead> <TableTr> - <TableTh style={{ width: '30%' }}>Nama Keunggulan Program</TableTh> - <TableTh style={{ width: '35%' }}>Deskripsi</TableTh> - <TableTh style={{ width: '15%' }}>Edit</TableTh> - <TableTh style={{ width: '15%' }}>Delete</TableTh> + <TableTh style={{ minWidth: 200 }}>Nama Keunggulan Program</TableTh> + <TableTh style={{ minWidth: 200 }}>Deskripsi</TableTh> + <TableTh style={{ minWidth: 200 }}>Edit</TableTh> + <TableTh style={{ minWidth: 200 }}>Delete</TableTh> </TableTr> </TableThead> <TableTbody> {filteredData.length > 0 ? ( filteredData.map((item) => ( <TableTr key={item.id}> - <TableTd> + <TableTd style={{ minWidth: 200 }}> <Text fw={500} truncate="end" lineClamp={1}> {item.judul} </Text> </TableTd> - <TableTd> + <TableTd style={{ minWidth: 200 }}> <Text fw={500} truncate="end" @@ -122,7 +122,7 @@ function ListKeunggulanProgram({ search }: { search: string }) { dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> </TableTd> - <TableTd> + <TableTd style={{ minWidth: 200 }}> <Tooltip label="Edit" withArrow> <Button variant="light" @@ -138,7 +138,7 @@ function ListKeunggulanProgram({ search }: { search: string }) { </Button> </Tooltip> </TableTd> - <TableTd> + <TableTd style={{ minWidth: 200 }}> <Tooltip label="Hapus" withArrow> <Button variant="light" diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/_lib/layoutTabs.tsx index 488a0d93..83e7d239 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { IconSchool, IconCalendar, IconBuildingCommunity } from '@tabler/icons-react'; @@ -65,36 +65,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { radius="lg" keepMounted={false} > - <TabsList - p="sm" - style={{ - background: "linear-gradient(135deg, #e7ebf7, #f9faff)", - borderRadius: "1rem", - boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", + <ScrollArea type="auto" offsetScrollbars> + <TabsList + p="sm" + style={{ + background: "linear-gradient(135deg, #e7ebf7, #f9faff)", + borderRadius: "1rem", + boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", + display: "flex", + flexWrap: "nowrap", + gap: "0.5rem", + paddingInline: "0.5rem", // βœ… biar nggak nempel ke tepi }} - > - {tabs.map((tab, i) => ( - <Tooltip - key={i} - label={tab.tooltip} - position="bottom" - withArrow - transitionProps={{ transition: 'pop', duration: 200 }} - > - <TabsTab - value={tab.value} - leftSection={tab.icon} - style={{ - fontWeight: 600, - fontSize: "0.9rem", - transition: "all 0.2s ease", - }} + > + {tabs.map((tab, i) => ( + <Tooltip + key={i} + label={tab.tooltip} + position="bottom" + withArrow + transitionProps={{ transition: 'pop', duration: 200 }} > - {tab.label} - </TabsTab> - </Tooltip> - ))} - </TabsList> + <TabsTab + value={tab.value} + leftSection={tab.icon} + style={{ + fontWeight: 600, + fontSize: "0.9rem", + transition: "all 0.2s ease", + }} + > + {tab.label} + </TabsTab> + </Tooltip> + ))} + </TabsList> + </ScrollArea> {tabs.map((tab, i) => ( <TabsPanel diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx index 29d4e159..3e446e1e 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa'; import colors from '@/con/colors'; import { @@ -33,8 +34,8 @@ function EditFasilitasYangDisediakan() { const router = useRouter(); const editState = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // === State lokal form === + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); // Load data pertama kali @@ -44,16 +45,22 @@ function EditFasilitasYangDisediakan() { } }, []); - // Sinkronkan state dengan data yang sudah di-load + // Sinkronkan formData saat data load useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '' + }); } }, [editState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } @@ -61,9 +68,13 @@ function EditFasilitasYangDisediakan() { setIsSubmitting(true); try { if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; - await editState.update.save(editState.findById.data); + // Update global state hanya saat submit + const payload = { + ...editState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi + }; + await editState.update.save(payload); toast.success('Berhasil menyimpan perubahan'); router.push('/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan'); @@ -78,7 +89,6 @@ function EditFasilitasYangDisediakan() { const handleBack = () => router.back(); - // Loading state if (editState.findById.loading) { return ( <Box> @@ -123,9 +133,9 @@ function EditFasilitasYangDisediakan() { <TextInput label={<Text fw="bold">Judul</Text>} placeholder="Masukkan judul fasilitas" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> {/* Deskripsi */} @@ -133,8 +143,8 @@ function EditFasilitasYangDisediakan() { <Text fz="sm" fw="bold" mb="xs">Deskripsi</Text> <BimbinganBelajarDesaTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={(value) => handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> </Box> @@ -144,7 +154,7 @@ function EditFasilitasYangDisediakan() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} </Button> diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/page.tsx index 6e129fa0..b568cefa 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/page.tsx @@ -81,6 +81,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Stack> </Box> diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx index b1ca2730..5e871dea 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa'; import colors from '@/con/colors'; import { @@ -33,27 +34,33 @@ function EditLokasiDanJadwal() { const router = useRouter(); const editState = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // state lokal untuk form, tidak langsung merubah global state + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); - // load data sekali + // Load data sekali useShallowEffect(() => { if (!editState.findById.data) { editState.findById.initialize(); } }, []); - // isi state ketika data loaded + // Isi state lokal setelah data global ter-load useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '' + }); } }, [editState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } @@ -61,9 +68,13 @@ function EditLokasiDanJadwal() { setIsSubmitting(true); try { if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; - await editState.update.save(editState.findById.data); + // update global state hanya saat submit + const updatedData = { + ...editState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi + }; + await editState.update.save(updatedData); toast.success('Berhasil menyimpan perubahan'); router.push('/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal'); @@ -78,7 +89,6 @@ function EditLokasiDanJadwal() { const handleBack = () => router.back(); - // loading state if (editState.findById.loading) { return ( <Box> @@ -92,15 +102,10 @@ function EditLokasiDanJadwal() { return ( <Box> <Stack gap="xs"> - {/* Header dengan tombol kembali */} + {/* Header */} <Group mb="md"> <Tooltip label="Kembali ke halaman sebelumnya" withArrow> - <Button - variant="subtle" - onClick={handleBack} - p="xs" - radius="md" - > + <Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <IconArrowBack color={colors['blue-button']} size={24} /> </Button> </Tooltip> @@ -125,9 +130,9 @@ function EditLokasiDanJadwal() { <TextInput label={<Text fw="bold">Judul</Text>} placeholder="Masukkan judul lokasi/jadwal" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> {/* Deskripsi Field */} @@ -135,8 +140,8 @@ function EditLokasiDanJadwal() { <Text fz="sm" fw="bold" mb="xs">Deskripsi</Text> <BimbinganBelajarDesaTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + onChange={(value) => handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> </Box> @@ -146,7 +151,7 @@ function EditLokasiDanJadwal() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} </Button> diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/page.tsx index 360465dd..f9de4d6a 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/page.tsx @@ -71,6 +71,7 @@ function Page() { fw="bold" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> <Divider my="md" color={colors['blue-button']} /> @@ -81,6 +82,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Stack> </Box> diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx index 2b318d89..55a0d1c6 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa'; import colors from '@/con/colors'; import { @@ -33,26 +34,31 @@ function EditTujuanProgram() { const router = useRouter(); const editState = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // gabung judul & content jadi formData + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); - // load data once + // load data sekali useShallowEffect(() => { - if (!editState.findById.data) { - editState.findById.initialize(); - } + if (!editState.findById.data) editState.findById.initialize(); }, []); + // sync data dari global state ke local formData sekali setelah load useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } @@ -60,8 +66,9 @@ function EditTujuanProgram() { setIsSubmitting(true); try { if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; + // update global state hanya saat submit + editState.findById.data.judul = formData.judul; + editState.findById.data.deskripsi = formData.deskripsi; await editState.update.save(editState.findById.data); toast.success('Berhasil menyimpan perubahan'); @@ -117,18 +124,20 @@ function EditTujuanProgram() { <TextInput label={<Text fw="bold">Judul</Text>} placeholder="Masukkan judul program" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> {/* Deskripsi Field */} <Box> - <Text fz="sm" fw="bold" mb="xs">Deskripsi</Text> + <Text fz="sm" fw="bold" mb="xs"> + Deskripsi + </Text> <BimbinganBelajarDesaTextEditor showSubmit={false} - onChange={setContent} - initialContent={content} + initialContent={formData.deskripsi} + onChange={(value) => handleChange('deskripsi', value)} /> </Box> @@ -138,7 +147,7 @@ function EditTujuanProgram() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} </Button> diff --git a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/page.tsx b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/page.tsx index 30544096..45b1c4db 100644 --- a/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/bimbingan-belajar-desa/tujuan-program/page.tsx @@ -93,6 +93,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> </Stack> </Box> diff --git a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx index edaad7da..114d6c66 100644 --- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/[id]/page.tsx @@ -5,7 +5,7 @@ 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 } from 'react'; +import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import dataPendidikan from '../../../_state/pendidikan/data-pendidikan'; @@ -15,23 +15,35 @@ export default function EditDataPendidikan() { const stateDPM = useProxy(dataPendidikan); const id = params.id; + // state lokal untuk form + const [formData, setFormData] = useState({ + name: '', + jumlah: '', + }); + // Load data saat mount useEffect(() => { if (id) { stateDPM.findUnique.load(id).then(() => { const data = stateDPM.findUnique.data; if (data) { - stateDPM.update.form = { + setFormData({ name: data.name || '', jumlah: data.jumlah || '', - }; + }); } }); } }, [id]); + const handleChange = (field: 'name' | 'jumlah', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { + // update global state hanya saat submit stateDPM.update.id = id; + stateDPM.update.form = { ...formData }; await stateDPM.update.submit(); router.push('/admin/pendidikan/data-pendidikan'); }; @@ -59,8 +71,8 @@ export default function EditDataPendidikan() { <TextInput label="Nama Pendidikan" placeholder="Contoh: SD, SMP, SMA" - value={stateDPM.update.form.name} - onChange={(e) => (stateDPM.update.form.name = e.currentTarget.value)} + value={formData.name} + onChange={(e) => handleChange('name', e.currentTarget.value)} radius="md" required /> @@ -68,8 +80,8 @@ export default function EditDataPendidikan() { label="Jumlah Peserta" type="number" placeholder="Masukkan jumlah peserta" - value={stateDPM.update.form.jumlah} - onChange={(e) => (stateDPM.update.form.jumlah = e.currentTarget.value)} + value={formData.jumlah} + onChange={(e) => handleChange('jumlah', e.currentTarget.value)} radius="md" required /> diff --git a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx index 602849f4..52f8e1f7 100644 --- a/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/data-pendidikan/create/page.tsx @@ -51,7 +51,7 @@ export default function CreateDataPendidikan() { <TextInput label="Nama Pendidikan" placeholder="Contoh: SD, SMP, SMA" - value={stateDPM.create.form.name} + defaultValue={stateDPM.create.form.name} onChange={(e) => (stateDPM.create.form.name = e.currentTarget.value)} radius="md" required @@ -59,7 +59,7 @@ export default function CreateDataPendidikan() { <TextInput label="Jumlah Peserta" placeholder="Masukkan jumlah peserta" - value={stateDPM.create.form.jumlah} + defaultValue={stateDPM.create.form.jumlah} onChange={(e) => (stateDPM.create.form.jumlah = e.currentTarget.value)} radius="md" type="number" diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/_lib/layoutTabs.tsx index e0493f56..e7991d96 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { IconBuilding, IconChalkboard, IconMicroscope, IconSchool } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; @@ -72,30 +72,36 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { radius="lg" keepMounted={false} > - <TabsList - p="sm" - style={{ - background: "linear-gradient(135deg, #e7ebf7, #f9faff)", - borderRadius: "1rem", - boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", + <ScrollArea type="auto" offsetScrollbars> + <TabsList + p="sm" + style={{ + background: "linear-gradient(135deg, #e7ebf7, #f9faff)", + borderRadius: "1rem", + boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)", + display: "flex", + flexWrap: "nowrap", + gap: "0.5rem", + paddingInline: "0.5rem", // βœ… biar nggak nempel ke tepi }} - > - {tabs.map((tab, i) => ( - <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}> - <TabsTab - value={tab.value} - leftSection={tab.icon} - style={{ - fontWeight: 600, - fontSize: "0.9rem", - transition: "all 0.2s ease", - }} - > - {tab.label} - </TabsTab> - </Tooltip> - ))} - </TabsList> + > + {tabs.map((tab, i) => ( + <Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}> + <TabsTab + value={tab.value} + leftSection={tab.icon} + style={{ + fontWeight: 600, + fontSize: "0.9rem", + transition: "all 0.2s ease", + }} + > + {tab.label} + </TabsTab> + </Tooltip> + ))} + </TabsList> + </ScrollArea> {tabs.map((tab, i) => ( <TabsPanel @@ -106,6 +112,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { background: "linear-gradient(180deg, #ffffff, #f5f6fa)", borderRadius: "1rem", boxShadow: "0 4px 16px rgba(0,0,0,0.05)", + minHeight: "60vh", }} > {children} @@ -121,4 +128,3 @@ export default LayoutTabs; - \ No newline at end of file diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx index ff1ce12c..02b1003b 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/[id]/page.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' + import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud'; import colors from '@/con/colors'; import { @@ -22,64 +23,62 @@ function EditJenjangPendidikan() { const router = useRouter(); const params = useParams(); const id = params?.id as string; + const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan); - const [formData, setFormData] = useState({ - nama: "", - }); + const [formData, setFormData] = useState({ nama: '' }); + const [loading, setLoading] = useState(true); + // Load data sekali saat component mount useEffect(() => { - const loadJenjangPendidikan = async () => { - if (!id) return; + if (!id) return; + const loadJenjang = async () => { try { const data = await stateJenjang.edit.load(id); - if (data) { - stateJenjang.edit.id = id; - setFormData({ - nama: data.nama || '', - }); + stateJenjang.edit.id = id; // tetap simpan di global state + setFormData({ nama: data.nama || '' }); } } catch (error) { - console.error("Error loading jenjang pendidikan:", error); - toast.error("Gagal memuat data jenjang pendidikan"); + console.error('Error loading jenjang pendidikan:', error); + toast.error('Gagal memuat data jenjang pendidikan'); + } finally { + setLoading(false); } }; - loadJenjangPendidikan(); + loadJenjang(); }, [id]); + const handleChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { + if (!formData.nama.trim()) { + toast.error('Nama jenjang pendidikan tidak boleh kosong'); + return; + } + try { - if (!formData.nama.trim()) { - toast.error('Nama jenjang pendidikan tidak boleh kosong'); - return; - } - - stateJenjang.edit.form = { - nama: formData.nama.trim(), - }; - - if (!stateJenjang.edit.id) { - stateJenjang.edit.id = id; - } + stateJenjang.edit.form = { nama: formData.nama.trim() }; + if (!stateJenjang.edit.id) stateJenjang.edit.id = id; const success = await stateJenjang.edit.update(); - if (success) { - toast.success("Jenjang pendidikan berhasil diperbarui!"); - router.push("/admin/pendidikan/info-sekolah/jenjang-pendidikan"); + toast.success('Jenjang pendidikan berhasil diperbarui!'); + router.push('/admin/pendidikan/info-sekolah/jenjang-pendidikan'); } } catch (error) { - console.error("Error updating jenjang pendidikan:", error); - toast.error("Terjadi kesalahan saat memperbarui jenjang pendidikan"); + console.error('Error updating jenjang pendidikan:', error); + toast.error('Terjadi kesalahan saat memperbarui jenjang pendidikan'); } }; return ( <Box px={{ base: 'sm', md: 'lg' }} py="md"> - {/* Header Back + Title */} + {/* Header */} <Group mb="md"> <Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> @@ -91,7 +90,7 @@ function EditJenjangPendidikan() { - {/* Form Container */} + {/* Form */} setFormData({ ...formData, nama: e.target.value })} + onChange={(e) => handleChange('nama', e.target.value)} required + disabled={loading} /> diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx index 62b33467..fe8b30d5 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/jenjang-pendidikan/create/page.tsx @@ -74,7 +74,7 @@ function CreateJenjangPendidikan() { (stateJenjang.create.form.nama = e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx index 620d13e1..e644a878 100644 --- a/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/info-sekolah/lembaga/[id]/edit/page.tsx @@ -31,6 +31,7 @@ export default function EditLembaga() { jenjangId: '', }); + // Load jenjang pendidikan dan data lembaga useEffect(() => { infoSekolahPaud.jenjangPendidikan.findMany.load(); @@ -46,12 +47,17 @@ export default function EditLembaga() { } }, [id]); + const handleChange = (field: 'nama' | 'jenjangId', value: string) => { + setForm((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { if (!form.nama || !form.jenjangId) { toast.warn('Nama dan jenjang pendidikan harus diisi'); return; } + // Update global state hanya saat submit state.edit.id = id; state.edit.form = form; @@ -65,7 +71,7 @@ export default function EditLembaga() { return ( - {/* Header dengan back button */} + {/* Header */} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/page.tsx index 010ef66b..d7afd755 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan/page.tsx @@ -83,6 +83,7 @@ function Page() { fw="bold" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -93,6 +94,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx index c04903e3..7fe96497 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal'; import colors from '@/con/colors'; import { @@ -33,8 +34,11 @@ function EditTempatKegiatan() { const router = useRouter(); const editState = useProxy(pendidikanNonFormalState.stateTempatKegiatan); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // state lokal form + const [formData, setFormData] = useState({ + judul: '', + deskripsi: '', + }); const [isSubmitting, setIsSubmitting] = useState(false); // load data pertama kali @@ -44,16 +48,22 @@ function EditTempatKegiatan() { } }, []); - // sync state dengan data hasil fetch + // sync state lokal ketika data fetch selesai useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } @@ -61,10 +71,13 @@ function EditTempatKegiatan() { setIsSubmitting(true); try { if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; - await editState.update.save(editState.findById.data); + const updatedData = { + ...editState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi, + }; + await editState.update.save(updatedData); toast.success('Berhasil menyimpan perubahan'); router.push( '/admin/pendidikan/pendidikan-non-formal/tempat-kegiatan' @@ -117,9 +130,9 @@ function EditTempatKegiatan() { Judul} placeholder="Masukkan judul tempat kegiatan" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> @@ -128,8 +141,8 @@ function EditTempatKegiatan() { handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> @@ -138,7 +151,7 @@ function EditTempatKegiatan() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/page.tsx index 49c6ce6f..d56908cf 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tempat-kegiatan/page.tsx @@ -71,6 +71,7 @@ function Page() { fw="bold" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -81,6 +82,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx index 35adde5f..fe91defd 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/edit/page.tsx @@ -1,4 +1,5 @@ 'use client' + import pendidikanNonFormalState from '@/app/admin/(dashboard)/_state/pendidikan/pendidikan-non-formal'; import colors from '@/con/colors'; import { @@ -30,41 +31,49 @@ function EditTujuanProgram() { const router = useRouter(); const editState = useProxy(pendidikanNonFormalState.stateTujuanPendidikanNonFormal); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); - // load data pertama kali + // Load data pertama kali useShallowEffect(() => { if (!editState.findById.data) { editState.findById.initialize(); } }, []); - // sync data hasil fetch ke local state + // Sync hasil fetch ke local state (sekali) useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '' + }); } }, [editState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } + if (!editState.findById.data) return; + setIsSubmitting(true); try { - if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; - await editState.update.save(editState.findById.data); + const payload = { + ...editState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi + }; - toast.success('Berhasil menyimpan perubahan'); - router.push('/admin/pendidikan/pendidikan-non-formal/tujuan-program'); - } + await editState.update.save(payload); + toast.success('Berhasil menyimpan perubahan'); + router.push('/admin/pendidikan/pendidikan-non-formal/tujuan-program'); } catch (err) { console.error('Error saving:', err); toast.error('Gagal menyimpan data'); @@ -88,16 +97,13 @@ function EditTujuanProgram() { return ( - {/* Back Button + Title */} - - Edit Tujuan Program - + Edit Tujuan Program Judul} placeholder="Masukkan judul program" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> Deskripsi handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> @@ -131,7 +137,7 @@ function EditTujuanProgram() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} diff --git a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/page.tsx b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/page.tsx index dd045c94..dcc6470e 100644 --- a/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/pendidikan-non-formal/tujuan-program/page.tsx @@ -85,6 +85,7 @@ function Page() { fw="bold" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -95,6 +96,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx index 17674fea..3954de64 100644 --- a/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/pendidikan/perpustakaan-digital/_lib/layoutTabs.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconBook2, IconCategory } from '@tabler/icons-react'; +import { IconBook2, IconCategory, IconUser } from '@tabler/icons-react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -25,6 +25,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { icon: , tooltip: "Atur kategori untuk buku digital", }, + { + label: "Peminjam", + value: "peminjam", + href: "/admin/pendidikan/perpustakaan-digital/peminjam", + icon: , + tooltip: "Data Peminjam Buku", + }, ]; const currentTab = tabs.find(tab => tab.href === pathname); @@ -58,36 +65,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { radius="lg" keepMounted={false} > - + - {tabs.map((tab, i) => ( - - + {tabs.map((tab, i) => ( + - {tab.label} - - - ))} - + + {tab.label} + + + ))} + + {tabs.map((tab, i) => ( (null); const [file, setFile] = useState(null); - const [formData, setFormData] = useState({ - judul: editState.update.form.judul || "", - deskripsi: editState.update.form.deskripsi || "", - imageId: editState.update.form.imageId || "", - kategoriId: editState.update.form.kategoriId || "", - }) + // Load kategori & data awal useEffect(() => { perpustakaanDigitalState.kategoriBuku.findMany.load(); - const loadDataPerpustakaan = async () => { - const id = params?.id as string; + + const loadData = async () => { + const id = Array.isArray(params?.id) ? params.id[0] : params?.id; if (!id) return; try { const data = await editState.update.load(id); - if (data) { - setFormData({ - judul: data.judul || "", - deskripsi: data.deskripsi || "", - imageId: data.imageId || "", - kategoriId: data.kategoriId || "", - }); - if (data.image?.link) setPreviewImage(data.image.link); - } - } catch (error) { - console.error("Error loading data perpustakaan:", error); - toast.error(error instanceof Error ? error.message : "Gagal mengambil data perpustakaan"); - } - } + if (!data) return; - loadDataPerpustakaan(); + setFormData({ + judul: data.judul || '', + deskripsi: data.deskripsi || '', + kategoriId: data.kategoriId || '', + imageId: data.imageId || '', + }); + + if (data.image?.link) setPreviewImage(data.image.link); + } catch (error) { + console.error(error); + toast.error(error instanceof Error ? error.message : 'Gagal memuat data perpustakaan'); + } + }; + + loadData(); }, [params?.id]); + const handleInputChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { - editState.update.form = { ...editState.update.form, ...formData } + // Update global state hanya pas submit + editState.update.form = { ...editState.update.form, ...formData }; + // Upload file jika ada 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'); editState.update.form.imageId = uploaded.id; } await editState.update.update(); - toast.success("Data perpustakaan berhasil diperbarui!"); - router.push("/admin/pendidikan/perpustakaan-digital/data-perpustakaan"); + toast.success('Data perpustakaan berhasil diperbarui!'); + router.push('/admin/pendidikan/perpustakaan-digital/data-perpustakaan'); } catch (error) { - console.error("Error updating data perpustakaan:", error); - toast.error("Terjadi kesalahan saat memperbarui data perpustakaan"); + console.error(error); + toast.error('Terjadi kesalahan saat memperbarui data perpustakaan'); } }; @@ -86,18 +98,26 @@ function EditPerpustakaanDigital() {
- {/* Form Paper */} - + {/* Dropzone Image */} - Gambar + + Gambar + { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); + const selected = files[0]; + if (selected) { + setFile(selected); + setPreviewImage(URL.createObjectURL(selected)); } }} onReject={() => toast.error('File tidak valid.')} @@ -117,8 +137,12 @@ function EditPerpustakaanDigital() { - Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar wajib +
@@ -141,27 +165,29 @@ function EditPerpustakaanDigital() { label="Judul" placeholder="Masukkan judul" value={formData.judul} - onChange={(e) => setFormData({ ...formData, judul: e.target.value })} + onChange={(e) => handleInputChange('judul', e.target.value)} required /> {/* Deskripsi */} - Deskripsi - setFormData({ ...formData, deskripsi: val })} /> + + Deskripsi + + handleInputChange('deskripsi', val)} /> {/* Kategori */} ({ value: p.id, label: p.judul })) || []} + value={formData.bukuId} + onChange={(value) => value && setFormData({ ...formData, bukuId: value })} + searchable + clearable + /> + + + { + perpustakaanDigitalState.peminjamanBuku.update.form.tanggalPinjam = + date ? new Date(date).toISOString() : ''; + }} + label={Tanggal Pinjam} + placeholder="Masukkan tanggal pinjam" + required + /> + + { + perpustakaanDigitalState.peminjamanBuku.update.form.tanggalKembali = + date ? new Date(date).toISOString() : ''; + }} + label={Tanggal Kembali} + placeholder="Masukkan tanggal kembali" + required + /> + + { + perpustakaanDigitalState.peminjamanBuku.update.form.batasKembali = + date ? new Date(date).toISOString() : ''; + }} + label={Batas Kembali} + placeholder="Masukkan batas kembali" + required + /> + +
+ + + No + Nama Peminjam + Status + Detail + + + + {filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + {index + 1} + + {item.nama} + + + {renderStatusBadge(item.status)} + + + + + + )) + ) : ( + + +
+ + Tidak ada data Peminjam buku yang cocok + +
+
+
+ )} +
+
+
+
+ + {totalPages > 1 && ( +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+ )} +
+ ); +} + +export default PeminjamBuku; diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/_lib/layoutTabs.tsx index d60d95cd..9c76abe1 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { IconSchool, IconTarget } from '@tabler/icons-react'; @@ -11,13 +11,6 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const tabs = [ - { - label: "Program Unggulan", - value: "program-unggulan", - href: "/admin/pendidikan/program-pendidikan-anak/program-unggulan", - icon: , - tooltip: "Lihat dan kelola program unggulan pendidikan anak", - }, { label: "Tujuan Program", value: "tujuan-program", @@ -25,6 +18,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { icon: , tooltip: "Atur tujuan program pendidikan anak", }, + { + label: "Program Unggulan", + value: "program-unggulan", + href: "/admin/pendidikan/program-pendidikan-anak/program-unggulan", + icon: , + tooltip: "Lihat dan kelola program unggulan pendidikan anak", + } ]; const currentTab = tabs.find(tab => tab.href === pathname); @@ -59,36 +59,42 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { radius="lg" keepMounted={false} > - + - {tabs.map((tab, i) => ( - - + {tabs.map((tab, i) => ( + - {tab.label} - - - ))} - + + {tab.label} + + + ))} + + {tabs.map((tab, i) => ( ({ + judul: '', + deskripsi: '' + }); const [isSubmitting, setIsSubmitting] = useState(false); // load data once useShallowEffect(() => { - if (!editState.findById.data) { - editState.findById.initialize(); - } + if (!editState.findById.data) editState.findById.initialize(); }, []); + // populate formData saat data global tersedia useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '' + }); } }, [editState.findById.data]); + const handleChange = useCallback( + (field: keyof FormData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }, + [] + ); + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } @@ -60,9 +77,13 @@ function EditTujuanProgram() { setIsSubmitting(true); try { if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; - await editState.update.save(editState.findById.data); + // update global state only on submit + const updatedData = { + ...editState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi + }; + await editState.update.save(updatedData); toast.success('Berhasil menyimpan perubahan'); router.push('/admin/pendidikan/program-pendidikan-anak/program-unggulan'); @@ -77,7 +98,6 @@ function EditTujuanProgram() { const handleBack = () => router.back(); - // loading state if (editState.findById.loading) { return ( @@ -93,12 +113,7 @@ function EditTujuanProgram() { - @@ -122,18 +137,20 @@ function EditTujuanProgram() { Judul} placeholder="Masukkan judul program" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> {/* Deskripsi Field */} - Deskripsi + + Deskripsi + handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> @@ -143,7 +160,7 @@ function EditTujuanProgram() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/page.tsx index 3bb0c725..57a19af4 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/program-unggulan/page.tsx @@ -71,6 +71,7 @@ function Page() { fw="bold" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -81,6 +82,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx index d2b0ca22..6929f9f2 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/edit/page.tsx @@ -11,7 +11,7 @@ import { Text, TextInput, Title, - Tooltip + Tooltip, } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; @@ -30,8 +30,8 @@ function EditTujuanProgram() { const router = useRouter(); const editState = useProxy(stateProgramPendidikanAnak.stateTujuanProgram); - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + // Menggunakan satu state lokal untuk form + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); const [isSubmitting, setIsSubmitting] = useState(false); // Load data pertama kali @@ -41,16 +41,22 @@ function EditTujuanProgram() { } }, []); - // Sync state dengan data hasil fetch + // Sync hanya sekali saat data fetch berhasil useEffect(() => { if (editState.findById.data) { - setJudul(editState.findById.data.judul ?? ''); - setContent(editState.findById.data.deskripsi ?? ''); + setFormData({ + judul: editState.findById.data.judul ?? '', + deskripsi: editState.findById.data.deskripsi ?? '', + }); } }, [editState.findById.data]); + const handleChange = (field: 'judul' | 'deskripsi', value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { - if (!judul.trim()) { + if (!formData.judul.trim()) { toast.error('Judul wajib diisi'); return; } @@ -58,9 +64,12 @@ function EditTujuanProgram() { setIsSubmitting(true); try { if (editState.findById.data) { - editState.findById.data.judul = judul; - editState.findById.data.deskripsi = content; - await editState.update.save(editState.findById.data); + const updatedData = { + ...editState.findById.data, + judul: formData.judul, + deskripsi: formData.deskripsi, + }; + await editState.update.save(updatedData); toast.success('Berhasil menyimpan perubahan'); router.push('/admin/pendidikan/program-pendidikan-anak/tujuan-program'); @@ -112,17 +121,17 @@ function EditTujuanProgram() { Judul} placeholder="Masukkan judul program" - value={judul} - onChange={(e) => setJudul(e.currentTarget.value)} - error={!judul && 'Judul wajib diisi'} + value={formData.judul} + onChange={(e) => handleChange('judul', e.currentTarget.value)} + error={!formData.judul && 'Judul wajib diisi'} /> Deskripsi handleChange('deskripsi', value)} + initialContent={formData.deskripsi} /> @@ -131,7 +140,7 @@ function EditTujuanProgram() { bg={colors['blue-button']} onClick={handleSubmit} loading={isSubmitting || editState.update.loading} - disabled={!judul} + disabled={!formData.judul} > {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} diff --git a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/page.tsx b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/page.tsx index 63562537..d003433d 100644 --- a/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/page.tsx +++ b/src/app/admin/(dashboard)/pendidikan/program-pendidikan-anak/tujuan-program/page.tsx @@ -71,6 +71,7 @@ function Page() { fw="bold" c={colors['blue-button']} dangerouslySetInnerHTML={{ __html: data.judul }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> @@ -81,6 +82,7 @@ function Page() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} />
diff --git a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/edit/page.tsx index c61cca2a..8a7b9a43 100644 --- a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/edit/page.tsx @@ -1,5 +1,6 @@ -'use client'; /* eslint-disable react-hooks/exhaustive-deps */ +'use client'; + import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik'; import colors from '@/con/colors'; @@ -17,15 +18,15 @@ interface FormDaftarInformasi { } function EditDaftarInformasiPublik() { - const daftarInformasi = useProxy(daftarInformasiPublik) - const router = useRouter() - const params = useParams() + const daftarInformasi = useProxy(daftarInformasiPublik); + const router = useRouter(); + const params = useParams(); const [formData, setFormData] = useState({ jenisInformasi: '', deskripsi: '', tanggal: '', - }) + }); const formatDateForInput = (dateString: string) => { if (!dateString) return ''; @@ -33,6 +34,7 @@ function EditDaftarInformasiPublik() { return date.toISOString().split('T')[0]; }; + // Load data once on mount / when ID changes useEffect(() => { const loadDaftarInformasi = async () => { const id = params?.id as string; @@ -48,14 +50,18 @@ function EditDaftarInformasiPublik() { }); } } catch (error) { - console.error("Error loading daftar informasi:", error); - toast.error("Gagal memuat data daftar informasi"); + console.error('Error loading daftar informasi:', error); + toast.error('Gagal memuat data daftar informasi'); } - } + }; loadDaftarInformasi(); }, [params?.id]); + const handleChange = (field: keyof FormDaftarInformasi, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + const handleSubmit = async () => { try { daftarInformasi.edit.form = { @@ -63,14 +69,14 @@ function EditDaftarInformasiPublik() { jenisInformasi: formData.jenisInformasi.trim(), deskripsi: formData.deskripsi.trim(), tanggal: formData.tanggal.trim(), - } - await daftarInformasi.edit.update() - router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba"); + }; + await daftarInformasi.edit.update(); + router.push('/admin/ppid/daftar-informasi-publik-desa-darmasaba'); } catch (error) { - console.error("Error updating berita:", error); - toast.error("Terjadi kesalahan saat memperbarui berita"); + console.error('Error updating berita:', error); + toast.error('Terjadi kesalahan saat memperbarui berita'); } - } + }; return ( @@ -98,12 +104,7 @@ function EditDaftarInformasiPublik() { label="Jenis Informasi" placeholder="Masukkan jenis informasi" value={formData.jenisInformasi} - onChange={(val) => { - setFormData({ - ...formData, - jenisInformasi: val.target.value - }); - }} + onChange={(e) => handleChange('jenisInformasi', e.target.value)} required /> @@ -113,10 +114,7 @@ function EditDaftarInformasiPublik() { { - setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); - daftarInformasi.edit.form.deskripsi = htmlContent; - }} + onChange={(htmlContent) => handleChange('deskripsi', htmlContent)} /> @@ -125,12 +123,7 @@ function EditDaftarInformasiPublik() { label="Tanggal Publikasi" placeholder="Pilih tanggal publikasi" value={formatDateForInput(formData.tanggal)} - onChange={(val) => { - setFormData({ - ...formData, - tanggal: val.target.value - }); - }} + onChange={(e) => handleChange('tanggal', e.target.value)} required /> diff --git a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx index dab10166..ac4f92d8 100644 --- a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/[id]/page.tsx @@ -63,7 +63,7 @@ function DetailDaftarInformasiPublik() { Detail Informasi Publik - + Jenis Informasi @@ -88,6 +88,7 @@ function DetailDaftarInformasiPublik() { c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} className="prose max-w-none" + style={{ wordBreak: "break-word", whiteSpace: "normal" }} /> diff --git a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/create/page.tsx b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/create/page.tsx index 626f0020..aadfe5d0 100644 --- a/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/create/page.tsx +++ b/src/app/admin/(dashboard)/ppid/daftar-informasi-publik-desa-darmasaba/create/page.tsx @@ -68,7 +68,7 @@ export default function CreateDaftarInformasi() { { daftarInformasi.create.form.jenisInformasi = e.target.value; }} @@ -96,7 +96,7 @@ export default function CreateDaftarInformasi() { { daftarInformasi.create.form.tanggal = e.target.value; }} diff --git a/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx b/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx index 5ca62fce..28fcaef5 100644 --- a/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx +++ b/src/app/admin/(dashboard)/ppid/dasar-hukum/edit/page.tsx @@ -9,37 +9,44 @@ import stateDasarHukumPPID from '../../../_state/ppid/dasar_hukum/dasarHukum'; import { useRouter } from 'next/navigation'; import { IconArrowBack } from '@tabler/icons-react'; -const PPIDTextEditor = dynamic(() => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor), { - ssr: false, -}); +const PPIDTextEditor = dynamic( + () => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor), + { ssr: false } +); function EditDasarHukum() { - const router = useRouter() - const dasarHukumState = useProxy(stateDasarHukumPPID) - const [judul, setJudul] = useState(''); - const [content, setContent] = useState(''); + const router = useRouter(); + const dasarHukumState = useProxy(stateDasarHukumPPID); + // Gabungkan judul & content jadi satu state lokal + const [formData, setFormData] = useState({ judul: '', content: '' }); + + // Load data awal sekali useShallowEffect(() => { if (!dasarHukumState.findById.data) { - dasarHukumState.findById.initialize(); // biar masuk ke `findFirst` route kamu + dasarHukumState.findById.initialize(); } }, []); + // Set state lokal saat data global sudah tersedia useEffect(() => { if (dasarHukumState.findById.data) { - setJudul(dasarHukumState.findById.data.judul ?? '') - setContent(dasarHukumState.findById.data.content ?? '') + setFormData({ + judul: dasarHukumState.findById.data.judul ?? '', + content: dasarHukumState.findById.data.content ?? '', + }); } - }, [dasarHukumState.findById.data]) + }, [dasarHukumState.findById.data]); - const submit = () => { + const handleSubmit = () => { if (dasarHukumState.findById.data) { - dasarHukumState.findById.data.judul = judul; - dasarHukumState.findById.data.content = content; - dasarHukumState.update.save(dasarHukumState.findById.data) + // Update global state hanya saat submit + const updated = { ...dasarHukumState.findById.data, ...formData }; + dasarHukumState.update.save(updated); } - router.push('/admin/ppid/dasar-hukum') - } + router.push('/admin/ppid/dasar-hukum'); + }; + return ( @@ -69,12 +76,12 @@ function EditDasarHukum() { setFormData(prev => ({ ...prev, judul: value }))} /> - + Konten @@ -82,15 +89,15 @@ function EditDasarHukum() { setFormData(prev => ({ ...prev, content: value }))} /> - - Edit Data Pegawai PPID - + Edit Data Pegawai PPID + {/* Nama Lengkap */} + setFormData({ ...formData, namaLengkap: e.target.value })} + required + /> + + {/* Gelar Akademik */} + setFormData({ ...formData, gelarAkademik: e.target.value })} + /> + + {/* Foto Profil */} - - Nama Lengkap - - setFormData({ ...formData, namaLengkap: e.target.value })} - required - /> - - - - Gelar Akademik - - setFormData({ ...formData, gelarAkademik: e.target.value })} - /> - - - - Foto Profil - + Foto Profil { const selectedFile = files[0]; @@ -198,22 +161,12 @@ export default function EditPegawaiPPID() { p="xl" > - - - - - - - - - + + + - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB, format gambar wajib - + Seret gambar atau klik untuk memilih file + Maksimal 5MB, format gambar wajib @@ -225,86 +178,74 @@ export default function EditPegawaiPPID() { alt="Preview Gambar" radius="md" style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} - loading='lazy' + loading="lazy" /> )}
+ + {/* Tanggal Masuk */} + setFormData({ ...formData, tanggalMasuk: e.target.value })} + /> + + {/* Email */} + setFormData({ ...formData, email: e.target.value })} + /> + + {/* Telepon */} + setFormData({ ...formData, telepon: e.target.value })} + /> + + {/* Alamat */} + setFormData({ ...formData, alamat: e.target.value })} + /> + + {/* Posisi */} - - Tanggal Masuk - - setFormData({ ...formData, tanggalMasuk: e.target.value })} - /> - - - - Email - - setFormData({ ...formData, email: e.target.value })} - /> - - - - Telepon - - setFormData({ ...formData, telepon: e.target.value })} - /> - - - - Alamat - - setFormData({ ...formData, alamat: e.target.value })} - /> - - - - Posisi - + Posisi { - setFormData({ ...formData, isActive: val === 'true' }); - }} + onChange={(val) => setFormData({ ...formData, isActive: val === 'true' })} clearable /> + {/* Submit Button */} + diff --git a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPendudukNonPermanent.tsx b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPendudukNonPermanent.tsx index 5cda8f40..c35a976b 100644 --- a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPendudukNonPermanent.tsx +++ b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPendudukNonPermanent.tsx @@ -16,7 +16,7 @@ function PelayananPendudukNonPermanent() { const loadData = async () => { try { setLoading(true); - await state.pelayananPendudukNonPermanen.findById.load('1'); + await state.pelayananPendudukNonPermanen.findById.load('edit'); } catch (error) { console.error('Gagal memuat data:', error); } finally { @@ -52,6 +52,7 @@ function PelayananPendudukNonPermanent() { ta="justify" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> ) : ( Deskripsi belum tersedia. diff --git a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx index 3507063c..08f63752 100644 --- a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx +++ b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx @@ -7,34 +7,52 @@ import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; function PelayananPerizinanBerusaha() { - const state = useProxy(stateLayananDesa) - const [loading, setLoading] = useState(false) - const [active, setActive] = useState(1); - const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current)); - const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); + const state = useProxy(stateLayananDesa); + const [loading, setLoading] = useState(false); + const [active, setActive] = useState(0); + + const totalSteps = 6; + + const nextStep = () => { + if (active < totalSteps - 1) { + setActive(active + 1); + } else if (active === totalSteps - 1) { + setActive(totalSteps); // Mark as completed + } + }; + + const prevStep = () => { + if (active > 0) { + setActive(active - 1); + } + }; useEffect(() => { const loadData = async () => { try { setLoading(true); - await state.pelayananPerizinanBerusaha.findById.load('1') + await state.pelayananPerizinanBerusaha.findById.load('edit'); } catch (error) { console.error('Gagal memuat data:', error); } finally { setLoading(false); } - } - loadData() - }, []) + }; + loadData(); + }, []); const data = state.pelayananPerizinanBerusaha.findById.data; - + if (!data && !loading) { return (
- Belum ada informasi layanan yang tersedia - + + Belum ada informasi layanan yang tersedia + +
); @@ -47,72 +65,111 @@ function PelayananPerizinanBerusaha() {
) : ( - - - - Perizinan Berusaha Berbasis Risiko melalui OSS - - - Sistem Online Single Submission (OSS) untuk pendaftaran NIB - - + + + + Perizinan Berusaha Berbasis Risiko melalui OSS + + + Sistem Online Single Submission (OSS) untuk pendaftaran NIB + + - + - - Alur pendaftaran NIB: - - - Membuat akun di portal OSS - - - Lengkapi informasi perusahaan, data pemegang saham, dan alamat - - - Menentukan kode KBLI sesuai jenis usaha - - - Unggah akta pendirian, surat izin, dan dokumen wajib lainnya - - - Menunggu verifikasi dan persetujuan dari pihak berwenang - - - Menerima NIB sebagai identitas resmi usaha - - -
- - - Proses pendaftaran selesai - -
-
-
+ + + Alur pendaftaran NIB: + + { + if (step <= active) { // Only allow clicking on previous or current steps + setActive(step); + } + }} + orientation="vertical" + color="blue" + radius="md" + styles={{ + step: { padding: '14px 0' }, + stepBody: { marginLeft: 8 } + }} + > + + Membuat akun di portal OSS + + + Lengkapi informasi perusahaan, data pemegang saham, dan alamat + + + Menentukan kode KBLI sesuai jenis usaha + + + Unggah akta pendirian, surat izin, dan dokumen wajib lainnya + + + Menunggu verifikasi dan persetujuan dari pihak berwenang + + + Menerima NIB sebagai identitas resmi usaha + + +
+ + + Proses pendaftaran selesai + +
+
+
+ {active < totalSteps && ( - - - -
- - Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "} -
oss.go.id atau hubungi instansi pemerintah terkait. -
- + {active < totalSteps ? ( + + ) : ( + + )} + + )} + + + + Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '} + + oss.go.id + {' '} + atau hubungi instansi pemerintah terkait. + + )} ); } -export default PelayananPerizinanBerusaha; +export default PelayananPerizinanBerusaha; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananSuratKeterangan.tsx b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananSuratKeterangan.tsx index 819ccdfd..e079820c 100644 --- a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananSuratKeterangan.tsx +++ b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananSuratKeterangan.tsx @@ -1,52 +1,59 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import colors from '@/con/colors'; -import { BackgroundImage, Box, Button, Center, Group, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { BackgroundImage, Box, Button, Center, Group, Pagination, SimpleGrid, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconFileDescription, IconInfoCircle } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import React, { useEffect, useMemo, useState } from 'react'; import { useProxy } from 'valtio/utils'; function PelayananSuratKeterangan({ search }: { search: string }) { - const [loading, setLoading] = useState(false); const router = useRouter(); const state = useProxy(stateLayananDesa); - const filteredData = useMemo(() => { - if (!state.suratKeterangan.findMany.data) return []; - return state.suratKeterangan.findMany.data.filter((item) => { - const keyword = search.toLowerCase(); - return item.name?.toLowerCase().includes(keyword); - }); - }, [state.suratKeterangan.findMany.data, search]); + const { + data, + page, + totalPages, + loading, + load + } = state.suratKeterangan.findMany; - useEffect(() => { - const loadData = async () => { - try { - setLoading(true); - await state.suratKeterangan.findMany.load(); - } catch (error) { - console.error('Gagal memuat data:', error); - } finally { - setLoading(false); - } - }; - loadData(); - }, []); + useShallowEffect(() => { + load(page, 9, search); + }, [page, search]); + + if (loading || !data) { + return Array.from({ length: 3 }).map((_, i) => ( + + )); + } + + if (data?.length === 0) { + return ( +
+ + + + Tidak ada layanan surat keterangan yang ditemukan + + +
+ ); + } return ( - + Layanan Surat Keterangan - + @@ -55,69 +62,68 @@ function PelayananSuratKeterangan({ search }: { search: string }) { cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" > - {loading ? ( - Array.from({ length: 3 }).map((_, i) => ( - - )) - ) : filteredData.length === 0 ? ( -
- - - - Tidak ada layanan surat keterangan yang ditemukan + {data?.map((v, k) => ( + + + + + {v.name} - -
- ) : ( - filteredData.map((v, k) => ( - - - - + - - - - )) - )} + Lihat Detail + + + + + ))} +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
); } diff --git a/src/app/darmasaba/(pages)/desa/pengumuman/[name]/[id]/page.tsx b/src/app/darmasaba/(pages)/desa/pengumuman/[name]/[id]/page.tsx index 037b6327..25bf241a 100644 --- a/src/app/darmasaba/(pages)/desa/pengumuman/[name]/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/desa/pengumuman/[name]/[id]/page.tsx @@ -42,7 +42,7 @@ function Page() { - + {new Date(detail.data?.createdAt).toLocaleDateString('id-ID', { weekday: 'long', diff --git a/src/app/darmasaba/(pages)/desa/pengumuman/page.tsx b/src/app/darmasaba/(pages)/desa/pengumuman/page.tsx index 218e82bf..f964f519 100644 --- a/src/app/darmasaba/(pages)/desa/pengumuman/page.tsx +++ b/src/app/darmasaba/(pages)/desa/pengumuman/page.tsx @@ -243,6 +243,7 @@ function Page() { lineClamp={4} dangerouslySetInnerHTML={{ __html: item.deskripsi }} mb="md" + style={{wordBreak: "break-word", whiteSpace: "normal"}} />
diff --git a/src/app/darmasaba/(pages)/desa/potensi/[id]/page.tsx b/src/app/darmasaba/(pages)/desa/potensi/[id]/page.tsx index 4b2ef6a2..f5a7cadc 100644 --- a/src/app/darmasaba/(pages)/desa/potensi/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/desa/potensi/[id]/page.tsx @@ -77,9 +77,7 @@ function Page() { fallbackSrc="https://placehold.co/800x400?text=Gambar+tidak+tersedia" loading="lazy" /> - - {state.findUnique.data?.deskripsi || 'Belum ada deskripsi untuk potensi desa ini.'} - + diff --git a/src/app/darmasaba/(pages)/desa/potensi/page.tsx b/src/app/darmasaba/(pages)/desa/potensi/page.tsx index aa1efa10..5f2e1236 100644 --- a/src/app/darmasaba/(pages)/desa/potensi/page.tsx +++ b/src/app/darmasaba/(pages)/desa/potensi/page.tsx @@ -43,7 +43,7 @@ function Page() { Potensi Desa Darmasaba - + Temukan berbagai potensi unggulan, peluang, dan daya tarik yang menjadikan Desa Darmasaba istimewa. diff --git a/src/app/darmasaba/(pages)/desa/profile/page.tsx b/src/app/darmasaba/(pages)/desa/profile/page.tsx index de4f921f..e2f08dd5 100644 --- a/src/app/darmasaba/(pages)/desa/profile/page.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/page.tsx @@ -10,31 +10,36 @@ import ProfilPerbekel from './ui/profilPerbekel'; // import LembagaDesa from './ui/lembagaDesa'; import MotoDesa from './ui/motoDesa'; import SemuaPerbekel from './ui/semuaPerbekel'; +import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'; function Page() { return ( - - - - - - - - Profile Desa - - - - - - - - - - - - - - + + + + + + + + + Profile Desa + + + + + + + + + + + + + + + {/* Tombol Scroll ke Atas */} + + ); } diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/lambangDesa.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/lambangDesa.tsx index 0622bcc9..baefc403 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/lambangDesa.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/lambangDesa.tsx @@ -2,7 +2,7 @@ 'use client' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile' import colors from '@/con/colors' -import { Box, Center, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core' +import { Box, Center, Image, Paper, Skeleton, Stack, Text } from '@mantine/core' import { useEffect } from 'react' import { useProxy } from 'valtio/utils' @@ -58,16 +58,14 @@ function LambangDesa() { borderColor: '#e0e9ff', }} > - - diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/maskotDesa.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/maskotDesa.tsx index da477ecf..1d58f079 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/maskotDesa.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/maskotDesa.tsx @@ -1,11 +1,11 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; -import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core'; +import colors from '@/con/colors'; +import { Box, Card, Center, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core'; +import { IconPhoto } from '@tabler/icons-react'; import { useEffect } from 'react'; import { useProxy } from 'valtio/utils'; -import { IconPhoto } from '@tabler/icons-react'; -import colors from '@/con/colors'; function MaskotDesa() { const state = useProxy(stateProfileDesa.maskotDesa); @@ -48,13 +48,14 @@ function MaskotDesa() { ta="justify" c="dark" dangerouslySetInnerHTML={{ __html: data.deskripsi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> {data.images.length > 0 ? ( data.images.map((img, index) => ( - - )) ) : ( diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx index e59f4d57..887ffdaf 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/profilPerbekel.tsx @@ -2,10 +2,10 @@ 'use client' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Box, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Divider, Tooltip } from '@mantine/core'; +import { Box, Divider, Image, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; +import { IconBriefcase, IconTargetArrow, IconUser, IconUsers } from '@tabler/icons-react'; import { useEffect } from 'react'; import { useProxy } from 'valtio/utils'; -import { IconUser, IconBriefcase, IconUsers, IconTargetArrow } from '@tabler/icons-react'; function ProfilPerbekel() { const state = useProxy(stateProfileDesa.profilPerbekel) @@ -27,10 +27,10 @@ function ProfilPerbekel() { return ( - @@ -41,11 +41,11 @@ function ProfilPerbekel() { - @@ -70,9 +70,9 @@ function ProfilPerbekel() { Perbekel Desa Darmasaba - @@ -83,58 +83,56 @@ function ProfilPerbekel() { - - - - - - Biodata - - + + + + Biodata - + + - - - - - Pengalaman - - + + + + Pengalaman - + + - @@ -143,25 +141,27 @@ function ProfilPerbekel() { Pengalaman Organisasi - - + Program Kerja Unggulan - diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/sejarahDesa.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/sejarahDesa.tsx index 0fab6754..185f4606 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/sejarahDesa.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/sejarahDesa.tsx @@ -64,7 +64,7 @@ function SejarahDesa() { fz={{ base: 'md', md: 'lg' }} lh={1.8} ta="justify" - style={{ color: '#2a2a2a' }} + style={{ color: '#2a2a2a', wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsi }} /> diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/semuaPerbekel.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/semuaPerbekel.tsx index 3ba7b404..46f6594b 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/semuaPerbekel.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/semuaPerbekel.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; -import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Center, Image, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconUser } from '@tabler/icons-react'; import { useProxy } from 'valtio/utils'; @@ -77,23 +77,17 @@ function SemuaPerbekel() { - {v.nama} - - - + {v.daerah} - - - + {v.periode} - diff --git a/src/app/darmasaba/(pages)/desa/profile/ui/visimisiDesa.tsx b/src/app/darmasaba/(pages)/desa/profile/ui/visimisiDesa.tsx index cf8111f5..b8a15c93 100644 --- a/src/app/darmasaba/(pages)/desa/profile/ui/visimisiDesa.tsx +++ b/src/app/darmasaba/(pages)/desa/profile/ui/visimisiDesa.tsx @@ -59,6 +59,7 @@ function VisiMisiDesa() { fw={500} lh={1.6} dangerouslySetInnerHTML={{ __html: data.visi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> @@ -86,6 +87,7 @@ function VisiMisiDesa() { fw={500} lh={1.6} dangerouslySetInnerHTML={{ __html: data.misi }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> diff --git a/src/app/darmasaba/(pages)/ekonomi/PADesa-pendapatan-asli-desa/page.tsx b/src/app/darmasaba/(pages)/ekonomi/PADesa-pendapatan-asli-desa/page.tsx index 75553230..de14a1e1 100644 --- a/src/app/darmasaba/(pages)/ekonomi/PADesa-pendapatan-asli-desa/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/PADesa-pendapatan-asli-desa/page.tsx @@ -1,11 +1,10 @@ 'use client' import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import colors from '@/con/colors'; -import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; +import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; -import { useShallowEffect } from '@mantine/hooks'; - function Page() { const state = useProxy(PendapatanAsliDesa.ApbDesa); @@ -28,6 +27,9 @@ function Page() { const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0; const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0; + // Hasil akhir + const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan; + return ( @@ -44,28 +46,37 @@ function Page() { Pendapatan - {PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => ( + {latestApb?.pendapatan?.map((item) => ( - + {item.name} - - {new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0 - }).format(item.value)} + + + {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)} + ))} - + Total Pendapatan - - + + {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', @@ -81,18 +92,28 @@ function Page() { Belanja - {PendapatanAsliDesa.belanja.findMany.data?.map((item) => ( + {latestApb?.belanja?.map((item) => ( - + {item.name} - - {new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0 - }).format(item.value)} + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(item.value)} + @@ -118,18 +139,28 @@ function Page() { Pembiayaan - {PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => ( + {latestApb?.pembiayaan?.map((item) => ( - + {item.name} - - {new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0 - }).format(item.value)} + + + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(item.value)} + @@ -150,13 +181,54 @@ function Page() { - + + + {/* πŸ”½ Tambahan Ringkasan Anggaran */} + + Ringkasan Anggaran + + + + Keterangan + Jumlah + + + + + Total Pendapatan + + {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPendapatan)} + + + + Total Belanja + + {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalBelanja)} + + + + Total Pembiayaan + + {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPembiayaan)} + + + + Sisa Anggaran + = 0 ? "blue" : "red"}> + + {new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(sisaAnggaran)} + + + + +
+
); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx index 18d9dcca..61db456e 100644 --- a/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/demografi-pekerjaan/page.tsx @@ -48,7 +48,7 @@ function Page() { p={10} mb={50} h={400} - w={150} + w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data data={data.map((item) => ({ id: item.id, Pekerjaan: item.pekerjaan, diff --git a/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/page.tsx b/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/page.tsx index ce69d06f..c3a8636e 100644 --- a/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/page.tsx @@ -72,52 +72,55 @@ function Page() { ) } return ( - - + + - + Jumlah Penduduk Usia Kerja Yang Menganggur - + Pengangguran Berdasarkan Usia {mounted && donutGrafikNganggurData.length > 0 ? ( - + + + ) : } - + - + 18-25 - + 26-35 - + 36-45 - + 46+ @@ -127,44 +130,47 @@ function Page() { Pengangguran Berdasarkan Pendidikan {mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (
- + + +
) : } - + - + SD - + SMP - + SMA/SMK - + D3 - + S1 diff --git a/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx b/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx index a57ffda6..ba8b5920 100644 --- a/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/jumlah-pengangguran/page.tsx @@ -199,7 +199,7 @@ function Page() { {item.totalUnemployment} {item.educatedUnemployment} {item.uneducatedUnemployment} - {item.percentageChange} + {item.percentageChange}% ))} diff --git a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx new file mode 100644 index 00000000..53d29443 --- /dev/null +++ b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx @@ -0,0 +1,136 @@ +'use client' + +import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja'; +import colors from '@/con/colors'; +import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + +function DetailLowonganKerjaUser() { + const state = useProxy(lowonganKerjaState); + const router = useRouter(); + const params = useParams(); + const [loading, setLoading] = useState(true); + + useShallowEffect(() => { + const loadData = async () => { + await state.findUnique.load(params?.id as string); + setLoading(false); + }; + loadData(); + }, []); + + const data = state.findUnique.data; + + if (loading || !data) { + return ( +
+ +
+ ); + } + + return ( + + + + + + + {/* Judul */} + + {data.posisi} + + + Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric' + })} + + + {/* Info Ringkas */} + + + + {data.namaPerusahaan} + + + + {data.lokasi} + + + + {data.notelp} + + + + {data.gaji || '-'} + + + + {data.tipePekerjaan} + + + + + + Deskripsi Pekerjaan + + + + + + + Kualifikasi + + + + +
+ +
+
+
+
+
+ ); +} + +export default DetailLowonganKerjaUser; diff --git a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx index ad4d3b04..00ee1683 100644 --- a/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/lowongan-kerja-lokal/page.tsx @@ -12,13 +12,13 @@ import BackButton from '../../desa/layanan/_com/BackButto'; const formatCurrency = (value: string | number) => { // Convert to string if it's a number const numStr = typeof value === 'number' ? value.toString() : value; - + // Remove all non-digit characters const digitsOnly = numStr.replace(/\D/g, ''); - + // Format with thousand separators const formatted = digitsOnly.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); - + return `Rp.${formatted}`; }; @@ -103,7 +103,7 @@ function Page() {
- +
) diff --git a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx new file mode 100644 index 00000000..c013d69f --- /dev/null +++ b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/[id]/page.tsx @@ -0,0 +1,157 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core'; +import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react'; +import { useRouter, useParams } from 'next/navigation'; +import React from 'react'; +import { useProxy } from 'valtio/utils'; +import { useShallowEffect } from '@mantine/hooks'; +import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa'; + +function DetailProdukPasarUser() { + const router = useRouter(); + const params = useParams(); + const statePasar = useProxy(pasarDesaState); + + useShallowEffect(() => { + statePasar.pasarDesa.findUnique.load(params?.id as string); + }, []); + + const data = statePasar.pasarDesa.findUnique.data; + + if (!data) { + return ( + + + + ); + } + + return ( + + {/* Tombol kembali */} + + + + + + + {/* Gambar Produk */} + {data.image?.link ? ( + {data.nama} + ) : ( + + Tidak ada gambar + + )} + + {/* Detail Produk */} + + + {data.nama || 'Produk Tanpa Nama'} + + + + Rp {data.harga?.toLocaleString('id-ID')} + + {data.rating && ( + + + {data.rating} + + )} + + + + + + {/* Info Tambahan */} + + + Kategori + + {data.KategoriToPasar && data.KategoriToPasar.length > 0 ? ( + data.KategoriToPasar.map((kategori) => ( + + {kategori.kategori.nama} + + )) + ) : ( + Tidak ada kategori + )} + + + + {data.alamatUsaha && ( + + + {data.alamatUsaha} + + )} + + {data.kontak && ( + + + {data.kontak} + + )} + + + + + {/* Deskripsi */} + + Deskripsi Produk + + Tidak ada deskripsi. + + + + {/* Tombol Aksi User */} + {data.kontak && ( + + )} + + + + ); +} + +export default DetailProdukPasarUser; diff --git a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx index ef19511f..1aef38f4 100644 --- a/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx +++ b/src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx @@ -3,7 +3,7 @@ import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pa import colors from '@/con/colors'; import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; -import { IconMapPinFilled, IconSearch, IconShoppingCartFilled, IconStarFilled } from '@tabler/icons-react'; +import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from '@tabler/icons-react'; import { motion } from 'motion/react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -25,14 +25,14 @@ function Page() { } = state.findMany useShallowEffect(() => { - pasarDesaState.kategoriProduk.findMany.load() + pasarDesaState.kategoriProduk.findManyAll.load() }, []) // Filter data based on selected category const filteredData = selectedCategory - ? data?.filter(item => - item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory) - ) + ? data?.filter(item => + item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory) + ) : data; useShallowEffect(() => { @@ -71,8 +71,11 @@ function Page() { /> - - Pasar Desa Online merupakan Media Promosi yang bertujuan untuk membantu warga desa dalam memasarkan dan memperkenalkan produknya kepada masyarakat. + + Pasar Desa Online adalah media promosi untuk membantu warga memasarkan + + + dan memperkenalkan produk mereka.
@@ -87,7 +90,7 @@ function Page() { { if (val) beasiswaDesa.create.form.jenisKelamin = val }} /> - { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} /> - { if (val) beasiswaDesa.create.form.statusPernikahan = val }} /> - { if (val) beasiswaDesa.create.form.jenisKelamin = val }} /> + { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} /> + { if (val) beasiswaDesa.create.form.statusPernikahan = val }} /> + { if (val) beasiswaDesa.create.form.jenisKelamin = val }} /> + { beasiswaDesa.create.form.kewarganegaraan = val.target.value }} /> + { if (val) beasiswaDesa.create.form.statusPernikahan = val }} /> + ({ value: item.id, diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/memperoleh_informasi/memperolehInfromasi.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/memperoleh_informasi/memperolehInfromasi.tsx index e521caf0..e7cad592 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/memperoleh_informasi/memperolehInfromasi.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/memperoleh_informasi/memperolehInfromasi.tsx @@ -28,7 +28,7 @@ function MemperolehInformasi({ onChange }: { return ( ({ value: item.id, diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx index c771b04e..4ef775be 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx @@ -178,7 +178,7 @@ function Page() { - - - - - - Profil PPID Desa Darmasaba - - - {dataArray.map((item) => ( - - - - - Logo Desa - - Pejabat Pengelola Informasi Publik - - - - - - - - - - -
- Foto Pimpinan -
- - - {item.name} - - -
-
-
- - - - - - - Biografi - - - - - - - Riwayat Karir - - - - - -
-
- - - - - Pengalaman Organisasi - - - - - - - - - - - - Program Unggulan - - - - - - - -
+ + + + - ))} - + + + Profil PPID Desa Darmasaba + + + {dataArray.map((item) => ( + + + +
+ Logo Desa +
+ + Pejabat Pengelola Informasi dan Dokumentasi + +
+ + + + + + + + Foto Pimpinan e.currentTarget.src = "/perbekel.png"} + loading="lazy" + /> + + + {item.name} + + + + + + + + + + + + Biografi + + + + + + + Riwayat Karir + + + + + + + + + + + + Pengalaman Organisasi + + + + + + + + + + + + Program Unggulan + + + + + + + +
+
+ ))} +
+ {/* Tombol Scroll ke Atas */} + +
) } diff --git a/src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx b/src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx new file mode 100644 index 00000000..36a02bc3 --- /dev/null +++ b/src/app/darmasaba/(pages)/ppid/struktur-ppid/[id]/page.tsx @@ -0,0 +1,157 @@ +'use client'; +import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; +import colors from '@/con/colors'; +import { + Box, + Divider, + Group, + Image, + Paper, + Skeleton, + Stack, + Text, + Title, +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; + +function DetailPegawaiUser() { + const statePegawai = useProxy(stateStrukturPPID.pegawai); + const params = useParams(); + const router = useRouter(); + + useShallowEffect(() => { + stateStrukturPPID.posisiOrganisasi.findMany.load(); + statePegawai.findUnique.load(params?.id as string); + }, []); + + + if (!statePegawai.findUnique.data) { + return ( + + + + ); + } + + const data = statePegawai.findUnique.data; + + return ( + + {/* Back button */} + + router.back()} + style={{ + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: 8, + }} + > + + + Kembali + + + + + + + {/* Foto Profil */} + {data.namaLengkap + + {/* Nama & Jabatan */} + + + {data.namaLengkap || '-'} {data.gelarAkademik || ''} + + + {data.posisi?.nama || 'Posisi tidak tersedia'} + + + + + + + {/* Informasi Detail */} + + + + + + + + + + ); +} + +/* Komponen kecil untuk menampilkan baris informasi */ +function InfoRow({ + label, + value, + valueColor, + multiline = false, +}: { + label: string; + value?: string | null; + valueColor?: string; + multiline?: boolean; +}) { + return ( + + + {label} + + + {value || '-'} + + + ); +} + +export default DetailPegawaiUser; diff --git a/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx b/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx index 6c3d3b05..17637cb4 100644 --- a/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/struktur-ppid/page.tsx @@ -1,129 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-explicit-any */ -// /* eslint-disable react-hooks/exhaustive-deps */ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// 'use client' -// import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'; -// import colors from '@/con/colors'; -// import { Box, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; -// import { OrganizationChart } from 'primereact/organizationchart'; -// import { useEffect } from 'react'; -// import { useProxy } from 'valtio/utils'; -// import BackButton from '../../desa/layanan/_com/BackButto'; - -// function Page() { -// return ( -// -// -// -// -// Struktur PPID -// - -// -// ); -// } - -// function StrukturOrganisasiPPID() { -// const stateOrganisasi = useProxy(stateStrukturPPID.pegawai) - -// useEffect(() => { -// stateOrganisasi.findMany.load() -// }, []) - -// if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) { -// return ( -// -// -// -// ); -// } - -// // Step 1: Group pegawai berdasarkan posisiId -// const posisiMap = new Map(); - -// for (const pegawai of stateOrganisasi.findMany.data) { -// const posisiId = pegawai.posisi.id; -// if (!posisiMap.has(posisiId)) { -// posisiMap.set(posisiId, { -// ...pegawai.posisi, -// pegawaiList: [], -// children: [] -// }); -// } -// posisiMap.get(posisiId)!.pegawaiList.push(pegawai); -// } - - -// // Step 2: Buat struktur pohon berdasarkan parentId -// const root: any[] = []; - -// posisiMap.forEach((posisi) => { -// if (posisi.parentId) { -// const parent = posisiMap.get(posisi.parentId); -// if (parent) { -// parent.children.push(posisi); -// } -// } else { -// root.push(posisi); -// } -// }); - -// // Step 3: Ubah struktur ke format OrganizationChart -// function toOrgChartFormat(node: any): any { -// return { -// expanded: true, -// type: 'person', -// styleClass: 'p-person', -// data: { -// name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak 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} -// {name} -// {status} -// -// -// ); -// } - -// export default Page; - 'use client' + import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID' +import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton' +import colors from '@/con/colors' import { Box, Button, @@ -136,16 +17,25 @@ import { Paper, Stack, Text, + TextInput, Title, - Tooltip, - Transition, + Transition } from '@mantine/core' -import { IconRefresh, IconSearch, IconUsers } from '@tabler/icons-react' +import { + IconArrowsMaximize, + IconArrowsMinimize, + IconRefresh, + IconSearch, + IconUsers, + IconZoomIn, + IconZoomOut, +} from '@tabler/icons-react' +import { debounce } from 'lodash' +import { useTransitionRouter } from 'next-view-transitions' import { OrganizationChart } from 'primereact/organizationchart' -import { useEffect } from 'react' +import { useEffect, useRef, useState } from 'react' import { useProxy } from 'valtio/utils' import BackButton from '../../desa/layanan/_com/BackButto' -import colors from '@/con/colors' export default function Page() { return ( @@ -167,7 +57,6 @@ export default function Page() { ta="center" c={colors['blue-button']} fz={{ base: 28, md: 36, lg: 44 }} - > Struktur Organisasi PPID @@ -180,20 +69,34 @@ export default function Page() { + + {/* Tombol Scroll ke Atas */} + ) } function StrukturOrganisasiPPID() { const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai) + const router = useTransitionRouter() + const chartContainerRef = useRef(null) + const [scale, setScale] = useState(1) + const [isFullscreen, setFullscreen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + + // debounce untuk pencarian + const debouncedSearch = useRef( + debounce((value: string) => { + setSearchQuery(value) + }, 400) + ).current useEffect(() => { void stateOrganisasi.findMany.load() }, []) const isLoading = - !stateOrganisasi.findMany.data && - stateOrganisasi.findMany.loading !== false + !stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false if (isLoading) { return ( @@ -209,10 +112,7 @@ function StrukturOrganisasiPPID() { ) } - if ( - !stateOrganisasi.findMany.data || - stateOrganisasi.findMany.data.length === 0 - ) { + if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) { return (
@@ -233,8 +133,7 @@ function StrukturOrganisasiPPID() { Data pegawai belum tersedia - Belum ada data pegawai yang tercatat untuk PPID. Silakan coba - muat ulang atau periksa sumber data. + Belum ada data pegawai yang tercatat untuk PPID. - @@ -261,8 +151,11 @@ function StrukturOrganisasiPPID() { ) } + // Buat struktur organisasi const posisiMap = new Map() - for (const pegawai of stateOrganisasi.findMany.data) { + const aktifPegawai = stateOrganisasi.findMany.data.filter((p: any) => p.isActive) + + for (const pegawai of aktifPegawai) { const posisiId = pegawai.posisi.id if (!posisiMap.has(posisiId)) { posisiMap.set(posisiId, { @@ -278,25 +171,22 @@ function StrukturOrganisasiPPID() { posisiMap.forEach((posisi) => { if (posisi.parentId) { const parent = posisiMap.get(posisi.parentId) - if (parent) { - parent.children.push(posisi) - } else { - root.push(posisi) - } - } else { - root.push(posisi) - } + if (parent) parent.children.push(posisi) + else root.push(posisi) + } else root.push(posisi) }) function toOrgChartFormat(node: any): any { + const pegawai = node.pegawaiList?.[0] return { expanded: true, type: 'person', styleClass: 'p-person', data: { - name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ditugaskan', + id: pegawai?.id || null, + name: pegawai?.namaLengkap || 'Belum ditugaskan', title: node.nama || 'Tanpa jabatan', - image: node.pegawaiList?.[0]?.image?.link || '/img/default.png', + image: pegawai?.image?.link || '/img/default.png', description: node.deskripsi || '', positionId: node.id || null, }, @@ -304,36 +194,120 @@ function StrukturOrganisasiPPID() { } } - const chartData = root.map(toOrgChartFormat) + let chartData = root.map(toOrgChartFormat) + + // πŸ” filter by search + if (searchQuery) { + const filterNodes = (nodes: any[]): any[] => + nodes + .map((n) => ({ + ...n, + children: filterNodes(n.children || []), + })) + .filter( + (n) => + n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) || + n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) || + n.children.length > 0 + ) + chartData = filterNodes(chartData) + } + + // 🧭 fungsi fullscreen + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + chartContainerRef.current?.requestFullscreen() + setFullscreen(true) + } else { + document.exitFullscreen() + setFullscreen(false) + } + } + + // 🧭 fungsi zoom + const handleZoomIn = () => setScale((prev) => Math.min(prev + 0.1, 2)) + const handleZoomOut = () => setScale((prev) => Math.max(prev - 0.1, 0.5)) + const resetZoom = () => setScale(1) return ( - - + {/* πŸ” Search + Zoom + Fullscreen controls */} + + } + onChange={(e) => debouncedSearch(e.target.value)} + /> + + + + {/* πŸ” Tambahkan indikator zoom di sini */} + {/* Floating Zoom Indicator */} + + {Math.round(scale * 100)}% + + + + + + + + + + + + {/* Chart Container */} + nodeTemplate(node, router)} /> - - + + ) } -function nodeTemplate(node: any) { +function nodeTemplate(node: any, router: ReturnType) { const imageSrc = node?.data?.image || '/img/default.png' const name = node?.data?.name || 'Tanpa Nama' const title = node?.data?.title || 'Tanpa Jabatan' const description = node?.data?.description || '' return ( - + {(styles) => ( {name} @@ -371,26 +345,19 @@ function nodeTemplate(node: any) { {description || 'Belum ada deskripsi.'} - - )} ) } - - - diff --git a/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx b/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx index 5047e88a..85e50080 100644 --- a/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx @@ -54,12 +54,11 @@ function Page() { ta="center" fz={{ base: 28, md: 36 }} fw={800} - variant="gradient" - gradient={{ from: colors['blue-button'], to: 'cyan', deg: 45 }} + c={colors['blue-button']} > Moto PPID Desa Darmasaba - + Memberikan informasi yang cepat, mudah, tepat, dan transparan @@ -67,28 +66,31 @@ function Page() { } /> - + Visi PPID - + Misi PPID diff --git a/src/app/darmasaba/(tambahan)/penghargaan/[id]/page.tsx b/src/app/darmasaba/(tambahan)/penghargaan/[id]/page.tsx index 28cf98f7..3168a9bc 100644 --- a/src/app/darmasaba/(tambahan)/penghargaan/[id]/page.tsx +++ b/src/app/darmasaba/(tambahan)/penghargaan/[id]/page.tsx @@ -85,6 +85,7 @@ function Page() { fz="md" lh={1.6} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi || '' }} + style={{wordBreak: "break-word", whiteSpace: "normal"}} /> diff --git a/src/app/darmasaba/(tambahan)/penghargaan/page.tsx b/src/app/darmasaba/(tambahan)/penghargaan/page.tsx index 948cadbb..7111c9ac 100644 --- a/src/app/darmasaba/(tambahan)/penghargaan/page.tsx +++ b/src/app/darmasaba/(tambahan)/penghargaan/page.tsx @@ -2,11 +2,11 @@ 'use client'; import penghargaanState from "@/app/admin/(dashboard)/_state/desa/penghargaan"; import colors from "@/con/colors"; -import { Carousel, CarouselSlide } from "@mantine/carousel"; -import { Box, Button, Container, Group, Paper, Stack, Text, useMantineTheme, Skeleton } from "@mantine/core"; +import { Carousel } from "@mantine/carousel"; +import { Box, Button, Container, Group, Paper, Skeleton, Stack, Text, useMantineTheme } from "@mantine/core"; import { useMediaQuery } from "@mantine/hooks"; +import { IconArrowRight, IconAward } from "@tabler/icons-react"; import Autoplay from "embla-carousel-autoplay"; -import { IconAward, IconArrowRight } from "@tabler/icons-react"; import { useTransitionRouter } from "next-view-transitions"; import { useEffect, useRef } from "react"; import { useProxy } from "valtio/utils"; @@ -18,7 +18,8 @@ export default function Page() { - + + @@ -37,11 +38,10 @@ export default function Page() { } function Slider() { - const height = 500; - const width = 1200; const theme = useMantineTheme(); const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`); - const autoplay = useRef(Autoplay({ delay: 3000 })); + const tablet = useMediaQuery(`(max-width: ${theme.breakpoints.md})`); + const autoplay = useRef(Autoplay({ delay: 3000, stopOnInteraction: false })); const state = useProxy(penghargaanState); const router = useTransitionRouter(); @@ -54,7 +54,7 @@ function Slider() { if (loading) { return ( - + @@ -74,31 +74,49 @@ function Slider() { } const slides = data.map((item) => ( - + { + e.currentTarget.style.transform = "translateY(-4px)"; + e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.2)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = "translateY(0)"; + e.currentTarget.style.boxShadow = "none"; }} > - + {item.name} + diff --git a/src/app/darmasaba/_com/NavBarSearch.tsx b/src/app/darmasaba/_com/NavBarSearch.tsx index 4585fa6e..8c8d625d 100644 --- a/src/app/darmasaba/_com/NavBarSearch.tsx +++ b/src/app/darmasaba/_com/NavBarSearch.tsx @@ -1,27 +1,77 @@ +import { useRef, useState, useEffect } from 'react'; import stateNav from "@/state/state-nav"; -import { Container, Stack, TextInput, Tooltip } from "@mantine/core"; -import { IconSearch } from "@tabler/icons-react"; +import { Container, Stack, ActionIcon, Box } from "@mantine/core"; +import { IconX } from '@tabler/icons-react'; +import GlobalSearch from "./globalSearch"; export function NavbarSearch() { + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + const isNavigatingRef = useRef(false); + + // Close when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + const target = event.target as HTMLElement; + + // Jangan close jika klik di search result item (biar handleSelect yang urus) + if (target.closest('.search-result-item')) { + return; + } + + // Close jika klik di luar container + if (containerRef.current && !containerRef.current.contains(target)) { + setIsOpen(false); + stateNav.clear(); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // Reset navigation flag saat component unmount atau route change + useEffect(() => { + return () => { + isNavigatingRef.current = false; + }; + }, []); + return ( - - - - } - /> - - - + + + + + {isOpen && ( + { + setIsOpen(false); + stateNav.clear(); + }} + style={{ + position: 'absolute', + right: 10, + top: '50%', + transform: 'translateY(-50%)', + zIndex: 1000 + }} + > + + + )} + + + + ); -} +} \ No newline at end of file diff --git a/src/app/darmasaba/_com/Navbar.tsx b/src/app/darmasaba/_com/Navbar.tsx index 95c14d56..65da725e 100644 --- a/src/app/darmasaba/_com/Navbar.tsx +++ b/src/app/darmasaba/_com/Navbar.tsx @@ -5,7 +5,7 @@ import stateNav from "@/state/state-nav"; import { ActionIcon, Box, Burger, Group, Image, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core"; import { IconSquareArrowRight } from "@tabler/icons-react"; import { motion } from "framer-motion"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useSnapshot } from "valtio"; import { MenuItem } from "../../../../types/menu-item"; import { NavbarMainMenu } from "./NavbarMainMenu"; @@ -19,14 +19,18 @@ export function Navbar() { - + {/* Desktop navbar (muncul mulai 992px ke atas) */} + + + - + {/* Mobile navbar (muncul di bawah 992px, termasuk iPad Mini) */} + - - Village Logo + + Village Logo - + + + {mobileOpen && ( )} + {(item || isSearch) && } ); @@ -76,35 +95,105 @@ export function Navbar() { function NavbarMobile({ listNavbar }: { listNavbar: MenuItem[] }) { const router = useRouter(); + const pathname = usePathname(); // πŸ‘ˆ untuk cek path aktif + + // fungsi bantu: cek apakah path sekarang sama dengan menu / sub-menu + const isActive = (href?: string) => href && pathname.startsWith(href); + return ( - - - {listNavbar.map((item, k) => ( - - { - router.push(item.href); - stateNav.mobileOpen = false; - }} - style={{ cursor: "pointer" }} - > - - {item.name} - - - - {item.children && ( - - - - )} - - ))} + + + {listNavbar.map((item, k) => { + const active = isActive(item.href); + return ( + + { + if (item.href) { + router.push(item.href); + stateNav.mobileOpen = false; + } + }} + style={{ + cursor: item.href ? "pointer" : "default", + transition: "background 0.15s ease", + borderLeft: active ? "4px solid #1e66f5" : "4px solid transparent", + }} + > + + + {item.name} + + {item.href && ( + + )} + + + + {/* Submenu */} + {item.children && ( + + {item.children.map((child, j) => { + const childActive = isActive(child.href); + return ( + { + if (child.href) { + router.push(child.href); + stateNav.mobileOpen = false; + } + }} + style={{ + cursor: child.href ? "pointer" : "default", + opacity: child.href ? 1 : 0.8, + borderRadius: "0.5rem", + backgroundColor: childActive ? "#e7f0ff" : "transparent", + borderLeft: childActive ? "3px solid #1e66f5" : "3px solid transparent", + transition: "background 0.15s ease", + }} + > + + {child.name} + + + + ); + })} + + )} + + ); + })} ); } + diff --git a/src/app/darmasaba/_com/NavbarMainMenu.tsx b/src/app/darmasaba/_com/NavbarMainMenu.tsx index 4882b6a0..5d796d23 100644 --- a/src/app/darmasaba/_com/NavbarMainMenu.tsx +++ b/src/app/darmasaba/_com/NavbarMainMenu.tsx @@ -2,15 +2,14 @@ import colors from "@/con/colors" import stateNav from "@/state/state-nav" -import { ActionIcon, Button, Container, Flex, Image, Stack, Tooltip } from "@mantine/core" -import { useHover } from "@mantine/hooks" +import { ActionIcon, Button, Container, Flex, Image, Menu, MenuTarget, Stack, Tooltip } from "@mantine/core" import { IconSearch, IconUser } from "@tabler/icons-react" import { useTransitionRouter } from 'next-view-transitions' +import { usePathname, useRouter } from "next/navigation" import { useSnapshot } from "valtio" import { MenuItem } from "../../../../types/menu-item" import { NavbarSearch } from "./NavBarSearch" import { NavbarSubMenu } from "./NavbarSubMenu" -import { useRouter } from "next/navigation" // contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu) const stateAuth = { @@ -21,12 +20,13 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { const { item, isSearch } = useSnapshot(stateNav) const router = useTransitionRouter() const next = useRouter() + const pathname = usePathname(); return ( - + {listNavbar.map((item, k) => ( - + child.href && pathname.startsWith(child.href)))} + /> ))} - + + { next.push("/admin/landing-page/profile/program-inovasi") @@ -88,27 +93,45 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { ) } -function MenuItemCom({ item }: { item: MenuItem }) { - const { ref, hovered } = useHover() +function MenuItemCom({ item, isActive = false }: { item: MenuItem, isActive?: boolean }) { const router = useTransitionRouter() return ( - + + + + ) } diff --git a/src/app/darmasaba/_com/NavbarSubMenu.tsx b/src/app/darmasaba/_com/NavbarSubMenu.tsx index be7235ba..db816ec0 100644 --- a/src/app/darmasaba/_com/NavbarSubMenu.tsx +++ b/src/app/darmasaba/_com/NavbarSubMenu.tsx @@ -7,10 +7,11 @@ import { IconArrowRight } from "@tabler/icons-react"; import { MenuItem } from "../../../../types/menu-item"; import { useTransitionRouter } from "next-view-transitions"; import colors from "@/con/colors"; +import { usePathname } from "next/navigation"; export function NavbarSubMenu({ item }: { item: MenuItem[] | null }) { const router = useTransitionRouter(); - + const pathname = usePathname(); return ( {item.map((link, index) => ( + } + }} + rightSection={} + styles={(theme) => ({ + root: { + background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[0] : 'transparent', + color: link.href && pathname.startsWith(link.href) ? theme.colors.blue[7] : colors['blue-button'], + fontWeight: link.href && pathname.startsWith(link.href) ? 600 : 500, + transition: "all 0.2s ease", + "&:hover": { + background: link.href && pathname.startsWith(link.href) ? theme.colors.blue[1] : theme.colors.gray[0], + } + }, + })} + > + {link.name} + ))} ) : ( diff --git a/src/app/darmasaba/_com/globalSearch.tsx b/src/app/darmasaba/_com/globalSearch.tsx new file mode 100644 index 00000000..fde9645b --- /dev/null +++ b/src/app/darmasaba/_com/globalSearch.tsx @@ -0,0 +1,184 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; + +import searchState, { debouncedFetch } from '@/app/api/[[...slugs]]/_lib/search/searchState'; +import { Box, Center, Loader, Popover, Text, TextInput } from '@mantine/core'; +import { IconX } from '@tabler/icons-react'; +import { useEffect, useState } from 'react'; +import { useSnapshot } from 'valtio'; +import getDetailUrl from './searchUrl'; + +export default function GlobalSearch() { + const snap = useSnapshot(searchState); + const [opened, setOpened] = useState(false); + const [isNavigating, setIsNavigating] = useState(false); + + // Buka popover saat ada query + useEffect(() => { + setOpened(!!snap.query); + }, [snap.query]); + + // Infinite scroll handler + useEffect(() => { + const handleScroll = () => { + const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200; + if (nearBottom && !snap.loading) searchState.next(); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [snap.loading]); + + const handleSelect = async (e: React.MouseEvent, item: any) => { + e.preventDefault(); + e.stopPropagation(); + + if (isNavigating) return; + setIsNavigating(true); + + try { + // πŸ”₯ pastikan objek udah β€œdikeluarkan” dari Proxy valtio + const rawItem = JSON.parse(JSON.stringify(item)); + + // πŸ”₯ pastikan type-nya string murni + const type = String(rawItem.type || '').trim().toLowerCase(); + + // πŸ”₯ panggil getDetailUrl pakai type yang fix + let url = getDetailUrl({ ...rawItem, type }); + + // kalau hasil undefined atau default, fallback ke link eksternal + if (!url || url === '/darmasaba') { + if (rawItem.link && rawItem.link.startsWith('http')) { + url = rawItem.link; + } + } + + if (!url) { + console.warn('URL tidak ditemukan untuk item:', rawItem); + setIsNavigating(false); + return; + } + + console.log('Navigating to:', url); + + // tutup popover dulu + setOpened(false); + searchState.query = ''; + searchState.results = []; + searchState.loading = false; + + // kasih delay biar UI nutup dulu + await new Promise((r) => setTimeout(r, 100)); + + // navigasi + if (url.startsWith('http')) { + window.location.href = url; + } else { + window.location.href = url; + } + + } catch (err) { + console.error('Error saat navigasi:', err); + setIsNavigating(false); + } + }; + + + const clearSearch = () => { + searchState.query = ''; + searchState.results = []; + searchState.page = 1; + searchState.nextPage = null; + setOpened(false); + setIsNavigating(false); + }; + + return ( + + { + if (!isOpen) clearSearch(); + setOpened(isOpen); + }} + width="target" + position="bottom" + shadow="md" + withinPortal + radius="md" + zIndex={2000} + closeOnClickOutside={true} + closeOnEscape={true} + styles={{ + dropdown: { + zIndex: 2000, + borderRadius: 12, + overflow: 'hidden', + }, + }} + > + + { + searchState.query = e.currentTarget.value; + debouncedFetch(); + }} + radius="xl" + size="md" + rightSection={ + snap.query ? ( + + ) : undefined + } + /> + + + + {[...snap.results].length > 0 ? ( + [...snap.results].map((item: any, i: number) => ( + !isNavigating && (e.currentTarget.style.background = '#f9f9f9')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'white')} + onClick={(e) => handleSelect(e, item)} + > + + {item.name ?? item.nama ?? item.namaPasar ?? item.judul ?? '(Tanpa nama)'} + + + dari modul: {item.type || '-'} + + + )) + ) : ( +
+ {snap.loading ? : Tidak ada hasil} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/darmasaba/_com/main-page/apbdes/index.tsx b/src/app/darmasaba/_com/main-page/apbdes/index.tsx index a62b963b..7dc89bb0 100644 --- a/src/app/darmasaba/_com/main-page/apbdes/index.tsx +++ b/src/app/darmasaba/_com/main-page/apbdes/index.tsx @@ -34,13 +34,13 @@ function Apbdes() { const data = (state.findMany.data || []).slice(0, 3) return ( - + - + {textHeading.title} - + {textHeading.des} @@ -117,7 +117,7 @@ function Apbdes() { )} - +
- + @@ -185,7 +191,7 @@ function Kepuasan() { h={window.innerWidth < 480 ? 200 : 300} data={barChartData} dataKey="month" - series={[{ name: 'count', color: colors['blue-button'] }]} + series={[{ name: 'Responden', color: colors['blue-button'] }]} tickLine="y" xAxisLabel="Bulan" yAxisLabel="Jumlah Responden" @@ -332,7 +338,7 @@ function Kepuasan() { { state.create.form.name = val.currentTarget.value; @@ -416,16 +422,22 @@ function Kepuasan() { } return ( - +
- Indeks Kepuasan Masyarakat + Indeks Kepuasan Masyarakat
Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!
- + @@ -448,7 +460,7 @@ function Kepuasan() { h={300} data={barChartData} dataKey="month" - series={[{ name: 'count', color: colors['blue-button'] }]} + series={[{ name: 'Responden', color: colors['blue-button'] }]} tickLine="y" xAxisLabel="Bulan" yAxisLabel="Jumlah Responden" @@ -599,16 +611,16 @@ function Kepuasan() { label="Nama" type='text' placeholder="masukkan nama" - value={state.create.form.name} + defaultValue={state.create.form.name} onChange={(val) => { state.create.form.name = val.currentTarget.value; }} /> { state.create.form.tanggal = val.currentTarget.value; }} @@ -617,7 +629,7 @@ function Kepuasan() { key={"jenisKelamin"} label={"Jenis Kelamin"} placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'} - value={state.create.form.jenisKelaminId || ""} + defaultValue={state.create.form.jenisKelaminId || ""} onChange={(val) => { state.create.form.jenisKelaminId = val ?? ""; }} @@ -635,7 +647,7 @@ function Kepuasan() { key={"rating_responden"} label={"Rating"} placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'} - value={state.create.form.ratingId || ""} + defaultValue={state.create.form.ratingId || ""} onChange={(val) => { state.create.form.ratingId = val ?? ""; }} @@ -653,7 +665,7 @@ function Kepuasan() { key={"kelompokUmur"} label={"Kelompok Umur"} placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'} - value={state.create.form.kelompokUmurId || ""} + defaultValue={state.create.form.kelompokUmurId || ""} onChange={(val) => { state.create.form.kelompokUmurId = val ?? ""; }} diff --git a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx index d8269302..4d67390f 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx @@ -6,20 +6,19 @@ import { Center, Image, Paper, + ScrollArea, SimpleGrid, + Skeleton, Stack, Text, - Tooltip, - Skeleton, - useMantineColorScheme, - ScrollArea, + useMantineColorScheme } from "@mantine/core"; import { useShallowEffect } from "@mantine/hooks"; +import { Prisma } from "@prisma/client"; +import { IconPhotoOff } from "@tabler/icons-react"; import { motion } from "framer-motion"; import { useTransitionRouter } from "next-view-transitions"; import { useProxy } from "valtio/utils"; -import { Prisma } from "@prisma/client"; -import { IconPhotoOff } from "@tabler/icons-react"; type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>; @@ -30,44 +29,42 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) { return ( - - router.push(`/darmasaba/program-inovasi/${data.id}`)} - p="lg" - radius="xl" - shadow="sm" - role="button" - tabIndex={0} - className="cursor-pointer transition-all" - bg={isDark ? "dark.6" : "white"} - > -
- {data.image?.link ? ( - {data.name} - ) : ( - - - - Belum ada gambar - - - )} -
- - - {data.name} - - -
-
+ router.push(`/darmasaba/program-inovasi/${data.id}`)} + p="lg" + radius="xl" + shadow="sm" + role="button" + tabIndex={0} + className="cursor-pointer transition-all" + bg={isDark ? "dark.6" : "white"} + > +
+ {data.image?.link ? ( + {data.name} + ) : ( + + + + Belum ada gambar + + + )} +
+ + + {data.name} + + +
); } @@ -113,11 +110,11 @@ function ModuleView() { viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet }} > - - {listImageState.findMany.data?.map((item) => ( - - ))} - + + {listImageState.findMany.data?.map((item) => ( + + ))} + ); } diff --git a/src/app/darmasaba/_com/main-page/landing-page/ProfileView.tsx b/src/app/darmasaba/_com/main-page/landing-page/ProfileView.tsx index 5081dcd0..3520b0f9 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/ProfileView.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/ProfileView.tsx @@ -30,17 +30,41 @@ export default function ProfileView({ data }: ProfileViewProps) { justify="end" align="end" pos="relative" - w={{ base: '100%', md: '40%' }} - px="xl" + w={{ + base: '100%', // mobile: full width + xs: '100%', // small mobile + sm: '85%', // tablet: 85% + md: '60%', // laptop: 60% + lg: '55%', // laptop large: 55% + xl: '50%' // extra large (4K): 50% + }} + px={{ base: 'md', sm: 'lg', md: 'xl', xl: '2xl' }} + h={{ base: 'auto', sm: '500px', md: '600px', lg: '650px', xl: '700px' }} > {data.image?.link ? ( - {data.name + + {data.name + ) : ( @@ -49,24 +73,48 @@ export default function ProfileView({ data }: ProfileViewProps) {
)} - + + {/* Box nama dan jabatan - responsive positioning */} + - + {data.position || 'Tidak ada jabatan'} - + {data.name} ); -} +} \ No newline at end of file diff --git a/src/app/darmasaba/_com/main-page/landing-page/index.tsx b/src/app/darmasaba/_com/main-page/landing-page/index.tsx index 0c43440e..470ecfe4 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/index.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/index.tsx @@ -1,26 +1,27 @@ "use client"; import colors from "@/con/colors"; -import { Prisma } from "@prisma/client"; import { + Badge, Box, Card, - Skeleton, + Center, Flex, Grid, GridCol, + Group, Image, Paper, + Skeleton, Stack, Text, - Center, Tooltip, - Badge, } from "@mantine/core"; +import { Prisma } from "@prisma/client"; import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react"; import { useEffect, useState } from "react"; import ModuleView from "./ModuleView"; -import SosmedView from "./SosmedView"; import ProfileView from "./ProfileView"; +import SosmedView from "./SosmedView"; const getDayOfWeek = () => { const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]; @@ -120,23 +121,21 @@ function LandingPage() { }, []); return ( - + + + + Logo Darmasaba + + + Logo Pudak + + - - - Logo Darmasaba - - - - - Logo Pudak - - - Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. + Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
diff --git a/src/app/darmasaba/_com/main-page/layanan/index.tsx b/src/app/darmasaba/_com/main-page/layanan/index.tsx index b682c2d8..128bc2e3 100644 --- a/src/app/darmasaba/_com/main-page/layanan/index.tsx +++ b/src/app/darmasaba/_com/main-page/layanan/index.tsx @@ -30,17 +30,13 @@ const textHeading = { function Layanan() { return ( - - + + - + {textHeading.title} - + {textHeading.des} diff --git a/src/app/darmasaba/_com/main-page/penghargaan/index.tsx b/src/app/darmasaba/_com/main-page/penghargaan/index.tsx index 9f2d1496..46ef6f5c 100644 --- a/src/app/darmasaba/_com/main-page/penghargaan/index.tsx +++ b/src/app/darmasaba/_com/main-page/penghargaan/index.tsx @@ -68,7 +68,7 @@ function Penghargaan() { variant="gradient" gradient={{ from: "cyan", to: "blue", deg: 60 }} > - Penghargaan & Prestasi Desa + Penghargaan Desa {loading ? ( diff --git a/src/app/darmasaba/_com/main-page/potensi/index.tsx b/src/app/darmasaba/_com/main-page/potensi/index.tsx index e9b3593c..ff2bf284 100644 --- a/src/app/darmasaba/_com/main-page/potensi/index.tsx +++ b/src/app/darmasaba/_com/main-page/potensi/index.tsx @@ -6,6 +6,7 @@ import { BackgroundImage, Box, Button, + Container, Divider, Group, Loader, @@ -48,15 +49,15 @@ function Potensi() { const data = (state.findMany.data || []).slice(0, 4); return ( - - - + + + {textHeading.title} - + {textHeading.des} - + {loading ? ( @@ -104,9 +105,7 @@ function Potensi() { {v.name} - - {v.deskripsi} - + diff --git a/src/app/darmasaba/_com/main-page/prestasi/index.tsx b/src/app/darmasaba/_com/main-page/prestasi/index.tsx index d9222063..13ff8133 100644 --- a/src/app/darmasaba/_com/main-page/prestasi/index.tsx +++ b/src/app/darmasaba/_com/main-page/prestasi/index.tsx @@ -35,7 +35,7 @@ function Prestasi() { - + Prestasi Desa @@ -55,7 +55,7 @@ function Prestasi() { - + {loading ? (
@@ -101,6 +101,8 @@ function Prestasi() { fz={{ base: "1.5rem", md: "2rem", lg: "2.5rem" }} ta="center" dangerouslySetInnerHTML={{ __html: v.deskripsi }} + style={{ wordBreak: "break-word", whiteSpace: "normal" }} + lineClamp={5} />
- - SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan: dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan. + + SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan. - + {sdgsDesa && sdgsDesa.length > 0 ? ( - {sdgsDesa.map((item) => ( - -
- - {item.name} - -
+ {sdgsDesa.map((item) => ( + +
+ + {item.name} + +
+ + {/* Stack isi teks & angka */} + - + {item.name} + {item.jumlah} -
- ))} -
+
+ + ))} + + ) : (
@@ -141,7 +153,7 @@ export default function SDGS() { href="/darmasaba/sdgs-desa" radius="xl" size="lg" - mt={40} + mt="md" variant="gradient" gradient={{ from: "#26667F", to: "#124170" }} style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}} diff --git a/src/app/darmasaba/_com/scrollToTopButton.tsx b/src/app/darmasaba/_com/scrollToTopButton.tsx new file mode 100644 index 00000000..c2b66fe7 --- /dev/null +++ b/src/app/darmasaba/_com/scrollToTopButton.tsx @@ -0,0 +1,36 @@ +'use client' +import { useWindowScroll } from '@mantine/hooks'; +import { ActionIcon, Transition } from '@mantine/core'; +import { IconArrowUp } from '@tabler/icons-react'; +import colors from '@/con/colors'; + +function ScrollToTopButton() { + const [scroll, scrollTo] = useWindowScroll(); + + return ( + 300} + transition="slide-up" + duration={300} + timingFunction="ease" + > + {(styles) => ( + scrollTo({ y: 0 })} + pos="fixed" + bottom={24} + right={24} + aria-label="Kembali ke atas" + > + + + )} + + ); +} +export default ScrollToTopButton \ No newline at end of file diff --git a/src/app/darmasaba/_com/searchUrl.tsx b/src/app/darmasaba/_com/searchUrl.tsx new file mode 100644 index 00000000..6350b8dc --- /dev/null +++ b/src/app/darmasaba/_com/searchUrl.tsx @@ -0,0 +1,92 @@ +const getDetailUrl = (item: { type?: string; id: string | number; [key: string]: unknown }) => { + const { type, id, kategori } = item; + const map: Record string> = { + programinovasi: (id) => `/darmasaba/program-inovasi/${id}`, + desaantikorupsi: () => '/darmasaba/desa-anti-korupsi', + sdgsdesa: () => '/darmasaba/sdgs-desa', + apbdes: () => '/darmasaba/apbdes', + prestasidesa: () => '/darmasaba/prestasi-desa', + pejabatdesa: () => '/darmasaba/ppid/profile-ppid', + strukturppid: () => '/darmasaba/ppid/struktur-ppid', + visimisippid: () => '/darmasaba/ppid/visi-misi', + dasarhukumppid: () => '/darmasaba/ppid/dasar-hukum', + profileppid: () => '/darmasaba/ppid/profile', + daftarinformasipublik: () => '/darmasaba/ppid/daftar-informasi-publik', + perbekeldarmasaba: () => '/darmasaba/desa/profile', + berita: (id, kategori) => `/darmasaba/desa/berita/${kategori}/${id}`, + pengumuman: (id, kategori) => `/darmasaba/desa/pengumuman/${kategori}/${id}`, + sejarahdesa: () => '/darmasaba/desa/profile', + visimisidesa: () => '/darmasaba/desa/profile', + lambangdesa: () => '/darmasaba/desa/profile', + maskotdesa: () => '/darmasaba/desa/profile', + profilperbekel: () => '/darmasaba/desa/profile', + potensi: () => '/darmasaba/desa/potensi-desa', + galleryFoto: () => '/darmasaba/desa/gallery/foto', + galleryVideo: () => '/darmasaba/desa/gallery/video', + pelayananSuratKeterangan: () => '/darmasaba/desa/layanan', + pelayananPerizinanBerusaha: () => '/darmasaba/desa/layanan', + pelayananTelunjukSaktiDesa: () => '/darmasaba/desa/layanan', + pelayananPendudukNonPermanent: () => '/darmasaba/desa/layanan', + penghargaan: () => '/darmasaba/desa/penghargaan', + posyandu: (id) => `/darmasaba/kesehatan/posyandu/${id}`, + fasilitasKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga', + jadwalKegiatan: () => '/darmasaba/kesehatan/data-kesehatan-warga', + artikelKesehatan: () => '/darmasaba/kesehatan/data-kesehatan-warga', + puskesmas: () => '/darmasaba/kesehatan/puskesmas', + programKesehatan: () => '/darmasaba/kesehatan/program-kesehatan', + penangananDarurat: () => '/darmasaba/kesehatan/penanganan-darurat', + kontakDarurat: () => '/darmasaba/kesehatan/kontak-darurat', + infoWabahPenyakit: () => '/darmasaba/kesehatan/info-wabah-penyakit', + keamananLingkungan: () => '/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal', + polsekTerdekat: () => '/darmasaba/keamanan/polsek-terdekat', + kontakDaruratKeamanan: () => '/darmasaba/keamanan/kontak-darurat', + pencegahanKriminalitas: () => '/darmasaba/keamanan/pencegahan-kriminalitas', + laporanPublik: () => '/darmasaba/keamanan/laporan-publik', + tipsKeamanan: () => '/darmasaba/keamanan/tips-keamanan', + pasarDesa: () => '/darmasaba/ekonomi/pasar-desa', + lowonganKerjaLokal: () => '/darmasaba/ekonomi/lowongan-kerja-lokal', + strukturOrganisasi: () => '/darmasaba/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa', + jumlahPendudukUsiaKerjaYangMenganggurUsia: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur', + jumlahPendudukUsiaKerjaYangMenganggurPendidikan: () => '/darmasaba/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur', + jumlahPendudukMiskin: () => '/darmasaba/ekonomi/jumlah-penduduk-miskin', + programKemiskinan: () => '/darmasaba/ekonomi/program-kemiskinan', + sektorUnggulanDesa: () => '/darmasaba/ekonomi/sektor-unggulan-desa', + demografiPekerjaan: () => '/darmasaba/ekonomi/demografi-pekerjaan', + desaDigital: () => '/darmasaba/inovasi/desa-digital-smart-village', + programKreatif: () => '/darmasaba/inovasi/program-kreatif-desa', + kolaborasiInovasi: () => '/darmasaba/inovasi/kolaborasi-inovasi', + mitraKolaborasi: () => '/darmasaba/inovasi/kolaborasi-inovasi', + infoTekno: () => '/darmasaba/inovasi/info-teknologi-tepat-guna', + pengelolaanSampah: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah', + keteranganBankSampahTerdekat: () => '/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah', + programPenghijauan: () => '/darmasaba/lingkungan/program-penghijauan', + dataLingkunganDesa: () => '/darmasaba/lingkungan/data-lingkungan-desa', + gotongRoyong: (id, kategori) => `/darmasaba/lingkungan/gotong-royong/${kategori}/${id}`, + tujuanEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan', + materiEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan', + contohEdukasiLingkungan: () => '/darmasaba/lingkungan/edukasi-lingkungan', + filosofiTriHita: () => '/darmasaba/lingkungan/konservasi-adat-bali', + bentukKonservasiBerdasarkanAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali', + nilaiKonservasiAdat: () => '/darmasaba/lingkungan/konservasi-adat-bali', + jenjangPendidikan: () => '/darmasaba/pendidikan/info-sekolah/semua', + lembaga: () => '/darmasaba/pendidikan/info-sekolah/semua/lembaga', + siswa: () => '/darmasaba/pendidikan/info-sekolah/semua/siswa', + pengajar: () => '/darmasaba/pendidikan/info-sekolah/semua/pengajar', + keunggulanProgram: () => '/darmasaba/pendidikan/beasiswa-desa', + tujuanProgram: () => '/darmasaba/pendidikan/program-pendidikan-anak', + programUnggulan: () => '/darmasaba/pendidikan/program-pendidikan-anak', + lokasiJadwalBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa', + fasilitasBimbinganBelajarDesa: () => '/darmasaba/pendidikan/bimbingan-belajar-desa', + tujuanPendidikanNonFormal: () => '/darmasaba/pendidikan/pendidikan-non-formal', + tempatKegiatan: () => '/darmasaba/pendidikan/pendidikan-non-formal', + jenisProgramYangDiselenggarakan: () => '/darmasaba/pendidikan/pendidikan-non-formal', + dataPerpustakaan: () => '/darmasaba/pendidikan/perpustakaan-digital/semua', + dataPendidikan: () => '/darmasaba/pendidikan/data-pendidikan', + + }; + + if (type && map[type]) return map[type](id, kategori as string | undefined); + return '/darmasaba'; +}; + +export default getDetailUrl; diff --git a/src/app/darmasaba/page.tsx b/src/app/darmasaba/page.tsx index 092b49cb..e601eb4d 100644 --- a/src/app/darmasaba/page.tsx +++ b/src/app/darmasaba/page.tsx @@ -8,23 +8,31 @@ import colors from "@/con/colors"; import SDGS from "./_com/main-page/sdgs"; // import ApiFetch from "@/lib/api-fetch"; -import { Stack } from "@mantine/core"; +import { Box, Stack } from "@mantine/core"; import Apbdes from "./_com/main-page/apbdes"; import Prestasi from "./_com/main-page/prestasi"; +import ScrollToTopButton from "./_com/scrollToTopButton"; export default function Page() { return ( - - - - - - - - - - - + + + + + + + + + + + + + {/* Tombol Scroll ke Atas */} + + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 873061a9..5c98d487 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -11,7 +11,7 @@ -webkit-backdrop-filter: blur(40px); backdrop-filter: blur(40px); position: fixed; - z-index: 1; + z-index: 50; width: 100%; height: 100vh; } diff --git a/src/con/colors.ts b/src/con/colors.ts index 16cbba8e..250d01aa 100644 --- a/src/con/colors.ts +++ b/src/con/colors.ts @@ -1,6 +1,14 @@ const colors = { "orange": "#FCAE00", "blue-button": "#0A4E78", + "blue-button-1": "#E5F2FA", + "blue-button-2": "#B8DAEF", + "blue-button-3": "#8AC1E3", + "blue-button-4": "#5DA9D8", + "blue-button-5": "#2F91CC", + "blue-button-6": "#083F61", + "blue-button-7": "#062F49", + "blue-button-8": "#041F32", "blue-button-trans": "#628EC6", "white-1": "#FBFBFC", "white-trans-1": "rgba(255, 255, 255, 0.5)", diff --git a/src/con/navbar-list-menu.ts b/src/con/navbar-list-menu.ts index 031ccac0..28eaf9c2 100644 --- a/src/con/navbar-list-menu.ts +++ b/src/con/navbar-list-menu.ts @@ -2,7 +2,6 @@ const navbarListMenu = [ { id: "1", name: "PPID", - href: "/darmasaba/ppid/profile-ppid", children: [ { id: "1.1", @@ -51,7 +50,6 @@ const navbarListMenu = [ { id: "2", name: "Desa", - href: "/darmasaba/desa/profile", children: [ { id: "2.1", @@ -94,7 +92,6 @@ const navbarListMenu = [ { id: "3", name: "Kesehatan", - href: "/darmasaba/kesehatan/posyandu", children: [ { id: "3.1", @@ -136,7 +133,6 @@ const navbarListMenu = [ { id: "4", name: "Keamanan", - href: "/darmasaba/keamanan/keamanan-lingkungan-pecalang-patwal", children: [ { id: "4.1", @@ -173,7 +169,6 @@ const navbarListMenu = [ { id: "5", name: "Ekonomi", - href: "/darmasaba/ekonomi/pasar-desa", children: [ { id: "5.1", @@ -229,7 +224,6 @@ const navbarListMenu = [ }, { id: "6", name: "Inovasi", - href: "/darmasaba/inovasi/desa-digital-smart-village", children: [ { id: "6.1", @@ -266,7 +260,6 @@ const navbarListMenu = [ }, { id: "7", name: "Lingkungan", - href: "/darmasaba/lingkungan/pengelolaan-sampah-bank-sampah", children: [ { id: "7.1", @@ -302,7 +295,6 @@ const navbarListMenu = [ }, { id: "8", name: "Pendidikan", - href: "/darmasaba/pendidikan/info-sekolah", children: [ { id: "8.1", diff --git a/types/env.d.ts b/types/env.d.ts index d4dbd193..2e636b1a 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -3,6 +3,8 @@ declare namespace NodeJS { DATABASE_URL?: string; WIBU_UPLOAD_DIR?: string; NEXT_PUBLIC_BASE_URL?: string; + NEXTAUTH_SECRET?: string; + NEXTAUTH_URL?: string; + WIBU_DOWNLOAD_DIR?: string; } } - diff --git a/types/menu-item.ts b/types/menu-item.ts index dfc957bc..a6ecf87f 100644 --- a/types/menu-item.ts +++ b/types/menu-item.ts @@ -1,6 +1,9 @@ export type MenuItem = { - id: string, - name: string, - href: string, - children?: MenuItem[] -} \ No newline at end of file + id: string; + name: string; + href?: string; + children?: MenuItem[]; +} & ( + { href: string; children?: MenuItem[] } | + { children: MenuItem[] } +) \ No newline at end of file diff --git a/xx b/xx new file mode 100755 index 00000000..c4eb993d --- /dev/null +++ b/xx @@ -0,0 +1 @@ +curl -o assets.zip -s https://cld-dkr-makuro-seafile.wibudev.com/f/f425a33d23b54f0c892a/?dl=1 \ No newline at end of file diff --git a/xx.ts b/xx.ts new file mode 100644 index 00000000..2a1248a8 --- /dev/null +++ b/xx.ts @@ -0,0 +1,4 @@ +import 'colors' + + +console.log("halo".blue) \ No newline at end of file